הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

מתי "לכתוב את זה לבד" הופך לחוב טכני?

07/08/2025

נתון קוד HTML/JavaScript שמציג תיבת קלט ואת מספר התווים בתיבה:

<input type="text" id="input" />
<p id="count">0</p>
input.addEventListener('input', (e) => {
  count.textContent = e.target.value.length;
})

ואנחנו רוצים שמספר התווים יתעדכן רק פעם אחת אחרי שמשתמש מפסיק להקליד לשניה או שתיים. אפשר לכתוב לבד פיתרון:

let timeout = null;
input.addEventListener('input', (e) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
      count.textContent = e.target.value.length;
  }, 500);
})

ויש לנו פיתרון מקומי שקצת מלכלך את הקוד אבל אני יכול לחיות איתו.

אבל אז יגיע מישהו שיחשוב שאולי יש פה בעיה כללית יותר וש timeout הוא משתנה גלובאלי ויהיה עדיף בהרבה לסגור את כל הלכלוך הזה בפונקציה:

input.addEventListener('input', debounce((e) => {
  count.textContent = e.target.value.length;
}, 500));

function debounce(f, ms) {
  let timeout = null;

  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
        f(...args);
    }, ms);
  }
}

והנה החוב הטכני שלנו.

מקוד קצר שאפשר להבין אנחנו צריכים לתחזק אבסטרקציה. ואבסטרקציה תמיד תהיה יותר מסובכת מפיתרון נקודתי כי צריך לחשוב מי עוד יכול להפעיל את הפונקציה debounce, מה הציפיות שלהם, איזה באגים יש או אין באבסטרקציה זו. ברור גם שלבחור שם טעון כמו debounce זו בעיה כי אם ההתנהגות שכתבתי לא זהה בדיוק ל debounce שכולם מכירים התפר יגרום לבאגים שקשה למצוא.

והאתגר הוא שוב AI. כי לפני AI היה ברור שאם אני צריך debounce כדאי לי להתקין את lodash והיה ברור איפה אני שומר כל אבסטרקציה בפרויקט וזה ידע שדאגנו להעביר בין מפתחים שונים בפרויקט. היום AI שמח לכתוב מחדש את debounce או פונקציות דומות כל פעם שהוא כותב פיצ'ר, ואנחנו צריכים להיות ערניים ולנקות אחריו. חוב טכני הוא אף פעם לא רעיון טוב, והעלות של חוב טכני אף פעם לא היתה בזמן הכתיבה שלו אלא בזמן התחזוקה.

נ.ב. היום בעשר וובינר מודלים חופשיים ומקומיים. אם יש לכם שעה פנויה שווה לקפוץ ולגלות על יתרונות הפרטיות והמחיר של מודלים כאלה. אם אין לכם עדיין את הלינק לזום אפשר להירשם כאן: https://www.tocode.co.il/talking_ai

אני לא מאמין ש PHP קיבלו את האופרטור הזה לפנינו

06/08/2025

כבר הרבה זמן שיש הצעה על השולחן לאופרטור Pipe ב JavaScript. זה יראה כך:

value |> foo(%)

או בשירשור ארוך יותר:

[1, 2, 3]
|> %.filter(x => x % 2 === 0)
|> %.map(x => x * 4)
|> %.join(' ')
|> console.log(%)

או אפילו:

return links
  |> Object.keys(%).map(function (rel) {
    return '<' + links[rel] + '>; rel="' + rel + '"';
  })
  |> link + %.join(', ')
  |> this.set('Link', %);

ובזמן שאנחנו כבר שנים מסתכלים על ההצעה ל Pipe Operator, ומתווכחים באיזה תחביר בדיוק להשתמש החברים ב PHP רצו קדימה עם מימוש פשוט ועובד:

$result = "Hello World"
    |> strtoupper(...)
    |> str_shuffle(...)
    |> trim(...);

בניגוד להצעה של JavaScript, ב PHP אופרטור הצינור מקבל רק פונקציות שיכולות לקבל פרמטר יחיד אבל בתמורה הם ויתרו על סימן האחוז.

ניסוי OpenRouter מ Python

05/08/2025

אופןראוטר מספק גישה להמון מודלים מסחריים ומודלי קוד פתוח דרך API אחד, מסך תשלום אחד ומסך אחד בו אפשר לראות את הפעילות והעלויות. זה מאוד נוח כשרוצים לשים לב כמה עולה כל ה AI הזה וגם לחסוך כסף באמצעות גיוון במודלים.

אפשר להירשם כאן: https://openrouter.ai/

וגם לדבר עם המון מודלים מתוך ממשק ה Web שלהם, אבל אני רוצה להריץ ניסוי מתוך פייתון כדי לתת לכמה מודלים לענות לאותה שאילתה. התחלתי עם הקוד הבא:

import asyncio
from agents import Agent, Runner
from agents.extensions.models.litellm_model import LitellmModel
import os

async def main():
    models = [
        "openrouter/moonshotai/kimi-k2",
        "openrouter/z-ai/glm-4.5",
        "openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
        "openrouter/inception/mercury"
    ]

    for model in models:
        agent = Agent(
            name="Assistant",
            model=LitellmModel(model=model),
            instructions="You only respond in haikus.",
        )

        print(f"Model: {model}")
        result = await Runner.run(agent, "Tell me about recursion in programming.")
        print(result.final_output)
        print("---")

if __name__ == "__main__":
    asyncio.run(main())

הקוד משתמש בספריית OpenAI Agents SDK אותה אני מאוד מחבב ומבקש מ-4 מודלים להסביר בשיר Haiku מהי רקורסיה. אלה התשובות שקיבלתי:

Model: openrouter/moonshotai/kimi-k2
Function calls itself
Each step unpeels a layer
Base case stops the fall
---

Model: openrouter/z-ai/glm-4.5
Function calls itself
To solve a smaller problem
Base case stops the loop

Code within the code
Stack grows deeper with each call
Until base is found

Divide and conquer
Breaking problems into parts
Solve the simplest
---

Model: openrouter/cognitivecomputations/dolphin-mistral-24b-venice-edition:free
A function calls itself,
Looping back in endless dance,
Chasing its own tail.

Base case breaks the spell,
Else infinite descent,
Stack overflow lovers.

Elegant solutions,
Complex problems simplified,
Beauty in repetition.

But beware the trap,
Unchecked, it can drive you mad,
Like rabbits in a field.

Fibonacci, trees,
Recursion's grace is seen,
In patterns endless woven.

So with care and caution,
Wield this tool, and watch it grow,
A tapestry of code.
---

Model: openrouter/inception/mercury
Function calls itself,
In layers it goes UNCer hands depthHappyLe Finding to end.

סך הכל חוץ ממרקורי כולם היו בסדר. מבחינת עלויות דולפין-מיסטרל הלא מצונזר הוא בחינם אז ברור שהוא הכי משתלם אבל בכל מקרה נצטרך לבדוק את זה על בקשות יותר משמעותיות.

איזה מודלי קוד פתוח אתם הכי אוהבים? ואיך אתם ניגשים אליהם?

חיבור Rails ל Langfuse

04/08/2025

אומנם AI לא יודע לסרב אבל הוא בהחלט יכול לסבך את עצמו לתוך לולאות אינסופיות או להדפיס קוד שלא עובד או עובד רק בחלק קטן מהמקרים. כבר הייתי מעדיף שהיה פשוט אומר "אני לא יודע".

האתגר שלנו היה ונשאר למצוא את הדרך להגיע למבנים שאנחנו רוצים, גם (ובמיוחד) כשאין פיתרון מדף ברשת. בדוגמה היום ביקשתי לחבר אפליקציית Rails ל langfuse כדי לעקוב אחרי קריאות API למנועי AI. קלוד ענה שאין חיבור מובנה והציע לכתוב משהו בעצמו וזה לא עבד בכלל. אז עברתי לתת לו רמזים, תחילה המצאתי את הקוד שמשתמש באינטגרציה:

def ask_ai
  Langfuse.trace("extract_lyrics", attributes: {
     "gen_ai.request.model" => "gemini-2.5-pro-preview-06-05",
     "gen_ai.system" => "Gemini"
  }) do |tracer|
    chat = RubyLLM.chat(model: 'gemini-2.5-pro-preview-06-05')
    response = chat.ask("hello")
    tracer.trace(response)
  end
end

ביקשתי גם להשתמש בממשק ה OpenTelemetry של Langfuse עבור המימוש וסיפקתי קישור לדף התיעוד שלהם על מאפייני ה OpenTelemetry הדרושים כדי לתעד שיחה. התוצאה היתה קוד לא נורא שגם עבר בכל המקרים שבדקתי:

require 'opentelemetry-api'

module Langfuse
  class TracerWrapper
    def initialize(span)
      @span = span
    end

    def trace(llm_response)
      # Customize based on RubyLLM / OpenAI / Gemini format
      if llm_response.respond_to?(:model)
        @span.set_attribute("gen_ai.response.model", llm_response.model)
      end

      if llm_response.respond_to?(:content)
        content = llm_response.content

        # Set the completion content according to GenAI semantic conventions
        @span.set_attribute("gen_ai.completion.0.role", "assistant")

        if content.is_a?(Hash)
          # For structured responses, store as JSON string
          @span.set_attribute("gen_ai.completion.0.content", content.to_json)
        elsif content.is_a?(String)
          @span.set_attribute("gen_ai.completion.0.content", content)
        end
      end

      if llm_response.respond_to?(:usage)
        usage = llm_response.usage
        @span.set_attribute("gen_ai.usage.prompt_tokens", usage.prompt_tokens)
        @span.set_attribute("gen_ai.usage.completion_tokens", usage.completion_tokens)
        @span.set_attribute("gen_ai.usage.total_tokens", usage.total_tokens)
      end
    end
  end

  def self.trace(name, attributes: {}, &block)
    tracer = OpenTelemetry.tracer_provider.tracer('langfuse)
    tracer.in_span(name, attributes: default_attributes.merge(attributes)) do |span|
      yield TracerWrapper.new(span)
    end
  end

  def self.default_attributes
    {
    }
  end
end

וקובץ האיתחול config/initializers/opentelemetry.rb:

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'base64'

public_key = Rails.application.credentials.langfuse[:pk]
secret_key = Rails.application.credentials.langfuse[:secret]

auth_token = Base64.strict_encode64("#{public_key}:#{secret_key}")

exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
  endpoint: "https://us.cloud.langfuse.com/api/public/otel/v1/traces",
  headers: { "Authorization" => "Basic #{auth_token}" },
)

span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter)

OpenTelemetry::SDK.configure do |c|
  c.add_span_processor(span_processor)
end

לא משתמש בפריימוורק

03/08/2025

יש מפתחים שלא משתמשים בפריימוורק כי אין להם כח ללמוד משהו חדש, כי מי הזיז את הגבינה שלי, כי הכי פשוט זה הכי טוב ואני מעדיף להסתדר עם הבעיות שאני מכיר.

אחרים כבר השתמשו בכל הפריימוורקים ואחרי לימוד מעמיק של הבעיה וכל הפיתרונות הבינו מה דרך העבודה הטובה ביותר עבורם. במקום לבחור פריימוורק קיים שייתן פיתרון חלקי הם בנו פנימית במערכת שלהם את הסטנדרטים והאחידות שהם צריכים.

המשפט "אם אתה לא משתמש ב ORM אתה מוצא את עצמך בונה אחד" עדיין נכון, גם אם הטכנולוגיה והפריימוורק השתנו. והלקח מהמשפט הזה הוא לא להשתמש בפריימוורק בכל מחיר, אלא שכדאי ללמוד לעומק מספר פריימוורקים כדי להבין את הבעיה לפני שמחליטים לא להשתמש בפריימוורק ולבנות פיתרון בעצמנו.

שלושה דברים שאהבתי בספריית RubyLLM

02/08/2025

רובי היא תמיד השפה האחרונה במסיבה. כש OpenAI הוציאו Agents SDK הם שחררו גירסה לפייתון ואחרת לטייפסקריפט. גם לנגגרף זמין לפייתון וטייפסקריפט וכך רוב מוחלט של הספריות בתחום. ספריות רובי לעבודה עם AI לא היו מספיק מתוחזקות, אמינות או גמישות. לפחות עד לאחרונה.

ספריית RubyLLM בולטת מיד עם תיעוד מצוין ומפורט, ריפו מתוחזק ותמיכה רחבה באקוסיסטם. אלה שלושה דברים שאהבתי במיוחד בספרייה:

המשך קריאה

התסכול של AI ותחושת מסוגלות

01/08/2025

כלי AI היום מנסים לענות על שני צרכים בו-זמנית: הם מנסים לחסוך לנו זמן במשימות אותן אנחנו מסוגלים לעשות לבד, וגם לעשות בשבילנו דברים שאנחנו עדיין לא יודעים או לא רוצים לדעת איך עושים. וככה אני יכול בתור מתכנת להיעזר ב AI כדי לעשות Boostrap לכל ה Boilerplate של פרויקט חדש שאני יודע לבד איך לכתוב אבל בפרומפט אחד חסכתי שעה-שעתיים של הקלדה, ובאותו כלי אני יכול לתרגם דף תיעוד מסינית ולקבל גירסה שלא היתה מביישת הרבה מתרגמים אנושיים.

לאורך זמן ושימוש ב AI מתכנתים מדווחים על תחושת שחיקה וחוסר הנאה מהקוד. חלק מהגורמים לתחושה הוא בלבול בתחושת המסוגלות. כשאני משתמש ב AI גם לדברים שאני יודע לעשות כדי לחסוך זמן וגם לדברים שאני לא יודע לעשות כדי לחסוך זמן לימוד, לאורך זמן אני עלול לשכוח מה זה מה. בתכנות זו אפילו בעיה יותר רצינית כי כתיבת קוד היא לא כמו רכיבה על אופניים. זו מיומנות שנעלמת מהר ואנשים שמפסיקים לכתוב קוד באמת מגלים שעם הזמן גם כניסה קטנה לקוד יכולה להיות מאתגרת.

כשמתכנת או מתכנתת פרונט אנד מסתכלים על AI מנסה ליצור דף HTML ותמיד יוצר דף מכוער, ההתעקשות של חלקם לא להיכנס לקוד, לא לכתוב את השלד או להכניס תיקונים רק כדי "להתאמן על עבודה עם AI" לדעתי לא הגיונית. להסתכל על AI מג'נרט קוד גרוע שוב ושוב לא הופך אותך ליותר מיומן עם AI, אלא רק לפחות מיומן עם קוד.

הדרך קדימה היא לא לזרוק את כל מה שלמדנו אלא להשתמש במה שלמדנו כדי לבנות מוצרים טובים יותר בעזרת AI. יודעת לכתוב את ה HTML שאת צריכה בצורה נכונה סמנטית? מצוין, כתבי אותו, ואם את יותר מהירה מ AI תכתבי אותו בלי AI. המשחק, והאתגר, הוא למצוא את הדברים ש AI יכול לכתוב יותר מהר ממך ולשלב כוחות כדי לייצר מערכות טובות יותר ומהר יותר.

דוגמאות:

  1. אחרי מימוש פונקציה או קלאס אפשר לתת ל AI לכתוב על זה Code Review כדי לקבל רעיונות לשיפור.

  2. אחרי שינוי ארכיטקטורה ה AI יכול לרוץ על הקוד ולעדכן מסמך ארכיטקטורה.

  3. לפני ריפקטורינג ה AI יכול לרוץ על הקוד בצורה יותר מדויקת מחיפוש טקסטואלי כדי להראות לי מה הולך להיות מושפע.

  4. שינוי API שמשפיע על רכיבי צד שרת וצד לקוח הכתובים במספר קבצים יכול להיות מטופל מהר יותר על ידי AI.

  5. מימוש פונקציה לפי Spec במיוחד אם היא ניגשת ל APIs חיצוניים או עושה שינויים מבניים ב Data.

אז כן תנו ל AI לעבוד ותעזרו לו כשצריך. תסכול והרצה חוזרת של שאילתות לא יעזרו ל AI "ללמוד" אלא רק יגרמו לכם לשכוח.

פריימוורקים אג'נטיים ושמירת הודעות בבסיס הנתונים

31/07/2025

אחד הפערים בין ספריות לכתיבת מערכות משולבות AI הוא האופן בו מערכות אלה שומרות הודעות בבסיס הנתונים. בואו נראה שלוש תבניות מרכזיות ומהן נלמד על מה חשוב להסתכל.

נתחיל עם OpenAI Agents SDK, ספריה יחסית חדשה של OpenAI. זאת הדוגמה מתוך התיעוד שלהם:

import asyncio

from agents import Agent, Runner, SQLiteSession

async def main():
    agent = Agent(name="Assistant", instructions="Reply very concisely.")

    # Create session instance
    session = SQLiteSession("conversation_123", "chats.db")

    # First turn
    result = await Runner.run(agent, "What city is the Golden Gate Bridge in?", session=session)
    print(result.final_output)
    # San Francisco

    # Second turn - agent automatically remembers previous context
    result = await Runner.run(agent, "What state is it in?", session=session)
    print(result.final_output)
    # California

if __name__ == '__main__':
    asyncio.run(main())

הספריה שומרת את כל ההודעות הישנות בבסיס נתונים SQLite או בזיכרון. בדף התיעוד הם מציעים לי לכתוב Custom Session Backend אם אני רוצה להשתמש בספריה בפרודקשן כדי לשמור את השיחות ב Postgresql או רדיס, אבל הם לא מספקים אחד. בכניסה לבסיס הנתונים שנוצר אני יכול לזהות שם טבלה של שיחות וטבלה של הודעות:

sqlite> .tables
agent_messages  agent_sessions
sqlite> select * from agent_messages ;
1|conversation_123|{"content": "What city is the Golden Gate Bridge in?", "role": "user"}|2025-07-30 13:35:36
2|conversation_123|{"id": "msg_688a1fa7fbe0819b9f4096e23741045f0d855ca99eba94a7", "content": [{"annotations": [], "text": "San Francisco.", "type": "output_text", "logprobs": []}], "role": "assistant", "status": "completed", "type": "message"}|2025-07-30 13:35:36
3|conversation_123|{"content": "What state is it in?", "role": "user"}|2025-07-30 13:35:37
4|conversation_123|{"id": "msg_688a1fa8fde0819b958bac6b45c84f900d855ca99eba94a7", "content": [{"annotations": [], "text": "California.", "type": "output_text", "logprobs": []}], "role": "assistant", "status": "completed", "type": "message"}|2025-07-30 13:35:37
sqlite> select * from agent_sessions ;
conversation_123|2025-07-30 13:35:36|2025-07-30 13:35:37

כל הודעה נשמרת בתור JSON בלי סכימה מסודרת. יש פה יתרון שהמימוש פשוט ואם יהיו לנו חלקים נוספים להודעה יהיה קל להוסיף הודעות חדשות עם אותם חלקים, אבל גם חיסרון שקשה להריץ שאילתות על JSON או לוודא נכונות של התוכן.

ספריה שניה היא RubyLLM בשפת רובי שמתחברת עם ריילס ומציעה את המבנה הבא:

rails g model Chat model_id:string user:references
rails g model Message chat:references role:string content:text model_id:string input_tokens:integer output_tokens:integer tool_call:references
rails g model ToolCall message:references tool_call_id:string:index name:string arguments:jsonb

השינוי הראשון שאנחנו רואים הוא הבחירה להשתמש בסכימה מסודרת, לכל הודעה יש תוכן, role, טוקנים ואיזה מודל יצר אותה, ובנוסף יש לנו טבלה של הפעלת כלים ששומרת את הפרמטרים ושמות כל הכלים שנקראו. כל ToolCall מחובר לשתי הודעות, ההודעה שהפעילה את הכלי וההודעה עם התוצאה. יש פה צעד קדימה מבחינת בטיחות המידע וחיבור לבסיסי נתונים אמיתיים. חלקים אחרים בהודעה כמו קבצים או תמונות יישמרו בטבלה נפרדת של קבצים מצורפים במנגנון Active Storage. משתמשים שרוצים יכולים להוסיף עוד עמודות לטבלת ההודעות כמו metadata.

ספריה שלישית לסקירה היא Vercel AI SDK והיא מציעה את הפיתרון המורכב מכולן. כך נראית סכימה מהדוגמה שלהם:

import {
  check,
  index,
  integer,
  jsonb,
  pgTable,
  real,
  text,
  timestamp,
  varchar,
} from "drizzle-orm/pg-core";
import { MyDataPart, MyUIMessage, MyProviderMetadata } from "../message-type";
import { generateId, ToolUIPart } from "ai";
import { sql } from "drizzle-orm";
import {
  getLocationInput,
  getLocationOutput,
  getWeatherInformationInput,
  getWeatherInformationOutput,
} from "@/ai/tools";

export const chats = pgTable("chats", {
  id: varchar()
    .primaryKey()
    .$defaultFn(() => generateId()),
});

export const messages = pgTable(
  "messages",
  {
    id: varchar()
      .primaryKey()
      .$defaultFn(() => generateId()),
    chatId: varchar()
      .references(() => chats.id, { onDelete: "cascade" })
      .notNull(),
    createdAt: timestamp().defaultNow().notNull(),
    role: varchar().$type<MyUIMessage["role"]>().notNull(),
  },
  (table) => [
    index("messages_chat_id_idx").on(table.chatId),
    index("messages_chat_id_created_at_idx").on(table.chatId, table.createdAt),
  ],
);

export const parts = pgTable(
  "parts",
  {
    id: varchar()
      .primaryKey()
      .$defaultFn(() => generateId()),
    messageId: varchar()
      .references(() => messages.id, { onDelete: "cascade" })
      .notNull(),
    type: varchar().$type<MyUIMessage["parts"][0]["type"]>().notNull(),
    createdAt: timestamp().defaultNow().notNull(),
    order: integer().notNull().default(0),

    // Text fields
    text_text: text(),

    // Reasoning fields
    reasoning_text: text(),

    // File fields
    file_mediaType: varchar(),
    file_filename: varchar(), // optional
    file_url: varchar(),

    // Source url fields
    source_url_sourceId: varchar(),
    source_url_url: varchar(),
    source_url_title: varchar(), // optional

    // Source document fields
    source_document_sourceId: varchar(),
    source_document_mediaType: varchar(),
    source_document_title: varchar(),
    source_document_filename: varchar(), // optional

    // shared tool call columns
    tool_toolCallId: varchar(),
    tool_state: varchar().$type<ToolUIPart["state"]>(),
    tool_errorText: varchar().$type<ToolUIPart["state"]>(),

    // tools inputs and outputss are stored in separate cols
    tool_getWeatherInformation_input:
      jsonb().$type<getWeatherInformationInput>(),
    tool_getWeatherInformation_output:
      jsonb().$type<getWeatherInformationOutput>(),

    tool_getLocation_input: jsonb().$type<getLocationInput>(),
    tool_getLocation_output: jsonb().$type<getLocationOutput>(),

    // Data parts
    data_weather_id: varchar().$defaultFn(() => generateId()),
    data_weather_location: varchar().$type<MyDataPart["weather"]["location"]>(),
    data_weather_weather: varchar().$type<MyDataPart["weather"]["weather"]>(),
    data_weather_temperature:
      real().$type<MyDataPart["weather"]["temperature"]>(),

    providerMetadata: jsonb().$type<MyProviderMetadata>(),
  },
  (t) => [
    // Indexes for performance optimisation
    index("parts_message_id_idx").on(t.messageId),
    index("parts_message_id_order_idx").on(t.messageId, t.order),

    // Check constraints
    check(
      "text_text_required_if_type_is_text",
      // This SQL expression enforces: if type = 'text' then text_text IS NOT NULL
      sql`CASE WHEN ${t.type} = 'text' THEN ${t.text_text} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "reasoning_text_required_if_type_is_reasoning",
      sql`CASE WHEN ${t.type} = 'reasoning' THEN ${t.reasoning_text} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "file_fields_required_if_type_is_file",
      sql`CASE WHEN ${t.type} = 'file' THEN ${t.file_mediaType} IS NOT NULL AND ${t.file_url} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "source_url_fields_required_if_type_is_source_url",
      sql`CASE WHEN ${t.type} = 'source_url' THEN ${t.source_url_sourceId} IS NOT NULL AND ${t.source_url_url} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "source_document_fields_required_if_type_is_source_document",
      sql`CASE WHEN ${t.type} = 'source_document' THEN ${t.source_document_sourceId} IS NOT NULL AND ${t.source_document_mediaType} IS NOT NULL AND ${t.source_document_title} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "tool_getWeatherInformation_fields_required",
      sql`CASE WHEN ${t.type} = 'tool-getWeatherInformation' THEN ${t.tool_toolCallId} IS NOT NULL AND ${t.tool_state} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "tool_getLocation_fields_required",
      sql`CASE WHEN ${t.type} = 'tool-getLocation' THEN ${t.tool_toolCallId} IS NOT NULL AND ${t.tool_state} IS NOT NULL ELSE TRUE END`,
    ),
    check(
      "data_weather_fields_required",
      sql`CASE WHEN ${t.type} = 'data-weather' THEN ${t.data_weather_location} IS NOT NULL AND ${t.data_weather_weather} IS NOT NULL AND ${t.data_weather_temperature} IS NOT NULL ELSE TRUE END`,
    ),
  ],
);

export type MyDBUIMessagePart = typeof parts.$inferInsert;
export type MyDBUIMessagePartSelect = typeof parts.$inferSelect;

התוכן נשמר בטבלה שנקראת parts כאשר לכל הודעה יש הרבה חלקים. טבלה זו מכילה עמודות לכל "סוג" חלק ויותר מזה לכל כלי פוטנציאלי יש שתי עמודות עבור הקלט והפלט של אותו כלי. כששאלתי את קלוד על הסכימה הוא כתב בכתב מודגש "This is over-engineered for most AI conversation needs".

מכירים גישות נוספות? ניסיתם חלק מהגישות פה ורוצים לשתף איך היה? מוזמנים לשתף בתגובות בטלגרם או ברשתות החברתיות.

הזמנה לוובינר: בואו נכתוב שרת MCP

30/07/2025

בעבודה עם AI כח על שמאוד עוזר ל AI ליצור קוד טוב יותר הוא היכולת להפעיל "כלים" - כלומר לבקש מסביבת הפיתוח לעשות משהו, לקבל את התוצאה ולהמשיך ליצור טקסט לפי התוצאה שהתקבלה. סביבות פיתוח משולבות AI יודעות להתחבר לכלים חיצוניים דרך ארכיטקטורה שנקראת MCP, קיצור של Model Context Protocol.

אם אתם עובדים בסביבת פיתוח משולבת AI והתנסיתם במצב סוכן של הסביבה כבר ראיתם עבודה עם כלים - זה הסוכן שמבקש לקבל תוכן של קובץ, להריץ פקודה בשורת הפקודה או אפילו להתחבר לבסיס הנתונים, לגיטהאב או לאינטרנט. חלק מהכלים הגיעו מובנים בסביבה, את חלקם אולי התקנתם בעצמכם דרך האינטרנט. והאמת שמאוד קל גם לכתוב כלים כאלה בעצמכם.

מחר (ה 31.7) בעשר בבוקר אראה לכם בשיחת זום פתוחה איך לכתוב שרת MCP ולהעלות אותו לאינטרנט לתשתית של Cloudflare Worker. אנחנו נראה:

  1. איך לפתוח פרויקט MCP Server חדש בטייפסקריפט.

  2. נכיר את המבנה של שרת MCP שיודע לעבוד על קלאודפלייר דרך ספריית הסוכנים שלהם.

  3. נחבר את שרת ה MCP לאחסון KV בקלאודפלייר ונראה מה ההבדל בין סטייט של השרת לבין מידע קבוע שנשמר.

  4. נדבר על אותנטיקציה ונבין איך עובד ניהול גישה לשרת עם OAuth.

כרגיל במפגשים אלה תהיה שיחה פתוחה עם המון מקום לשאלות והארות שלכם. לא תהיה הקלטה אבל אם אתם לא מספיקים להגיע תכתבו לי ונארגן שידור חוזר.

אם אתם בקבוצה אז יש לכם כבר את הלינק לזום מהמפגשים הקודמים, ואם עדיין לא נכנסתם אז צריך רק להזין את המייל בתיבה בעמוד הזה:

https://tocode.ravpage.co.il/tocodeai

ואתם מקבלים את הלינק לזום ישר למייל כמו גם הזמנות למפגשים הבאים בנושאי AI.

נתראה בזום.

פערי שפה

29/07/2025

כשמנהל מוצר או מנהל בדיקות כותב במסמך בדיקה "נכנסים לתיבת דואר נכנס ומוודאים שאפשר לראות את כל ההודעות" זה אולי מתאר מה שבן אדם עושה אבל זה לא עוזר לכתוב בדיקה אוטומטית.

וכשבודק אוטומציה כותב במסמך בדיקה את השלבים המפורטים יותר של הבדיקה זה עוזר לממש אבל הרבה יותר קשה לקרוא מהר רצף של בדיקות ולהבין מה בדקנו ומה לא.

לפני שניגשים לכתוב מסמך טכני כדאי לוודא שאנחנו מבינים - מה אנחנו כותבים, למי אנחנו כותבים ולאיזו מטרה. וכן AI יכול מאוד לעזור לעבור בין השפות אבל כל מעבר כזה דורש הרבה השלמות של קונטקסט וכדאי להיות ערניים לאי-דיוקים.