יום 2 - פלט מובנה מסוכן
הצד החזק של ספריית Pydantic AI הוא המילה הראשונה בשם הספריה - pydantic. פידנטיק היא ספריית הגדרת טיפוסים והסוכן שלנו היום ישתמש בפידנטיק כדי לוודא שהוא מקבל מהמודל מידע בדיוק בצורה לה הוא מצפה.
1. מה אנחנו בונים
היום נבנה סוכן תרגום שניתן להפעלה בשתי דרכים: יהיה לו ממשק שורת פקודה וממשק ווב. סוכן התרגום מקבל טקסט ורשימה של שפות ומתרגם את הטקסט לכל השפות שנבחרו בעזרת AI.
בגלל שאותו סוכן צריך לתקשר גם עם ממשק ווב וגם עם ממשק שורת פקודה אני כבר לא יכול להשתמש בפלט של ה AI כמו שהוא: הרי אם אבקש ממנו את הפלט בפורמט טקסטואלי יהיה לי קשה לשתול את זה בממשק ווב, ואם אבקש את הפלט בפורמט HTML יהיה לי קשה להציג את זה על המסך. הפתרון ילמד אותנו על מנגנון פלט מובנה מסוכן.
אפשר למצוא את קוד התוכנית המלא בגיטהאב בקישור: https://github.com/ynonp/pydanticai-demos/tree/main/02-translator-agent
2. הסוכן - הקובץ translator.py
לב המערכת הוא הסוכן ולכן אני מתחיל איתו. אחרי רשימה של import-ים תוכלו למצוא בקובץ את שתי הגדרות הקלאסים הבאות:
class Translation(BaseModel):
"""A single translation of the source text into one language."""
language: str
translation: str
class TranslationResult(BaseModel):
"""Structured output: one entry per requested target language."""
translations: list[Translation]
אלה קלאסים בתחביר של פידנטיק שמתארים איזה שדות מידע יש להם. אוביקט Translation מכיל שפה ואת התרגום אליה, ואוביקט TranslationResult מכיל רשימה של תרגומים.
נמשיך להגדרת הסוכן:
def build_agent() -> Agent[None, TranslationResult]:
load_dotenv()
provider = GoogleProvider(api_key=os.environ["GEMINI_API_KEY"])
model = GoogleModel("gemini-3-flash-preview", provider=provider)
return Agent(
model,
output_type=TranslationResult, # Structured Output
instructions=(
"You are a translator. Translate the given text into each of the "
"requested target languages. Return one entry per language, using "
"the language code you were given as the 'language' field and the "
"translated text as the 'translation' field. Translate only; do not "
"add comments or explanations."
),
)
את המפתחות model ו instructions אתם כבר מכירים מהמדריך של אתמול. הפרמטר החדש היום הוא output_type. פרמטר זה מבקש מהסוכן להחזיר רק פלט שמתאים למבנה שבחרנו, במקרה שלנו TranslationResult.
מנגנון הפלט המובנה של פידנטיק בנוי בצורה מעניינת: ברירת המחדל בה השתמשתי בדוגמה בכלל לא מבקשת מהמודל פלט במבנה שביקשנו אלא חושפת למודל Tool מיוחד, כלומר פונקציה מיוחדת שהמודל צריך להפעיל ולהעביר לה את הפלט. אותה פונקציה תבדוק שהפלט של המודל מתאים לתבנית שהגדרנו ואם זה לא מתאים תחזיר למודל שגיאה ותבקש שינסה שוב. בצורה כזאת אנחנו מגדילים את הסיכוי שנקבל פלט נכון.
רוב ספקי המודלים תומכים גם במנגנון פלט מובנה ברמת ה API. במנגנון זה אנחנו מעבירים ל API את הסכימה לפלט הרצוי והספק מבצע את הנסיונות החוזרים מול המודל בצד שלו עד שמתקבל מהמודל פלט שמתאים לסכימה. אפשר לראות בדוגמה מהתיעוד של פידנטיק את התמיכה במצב זה באמצעות עטיפת מודל הנתונים באוביקט NativeOutput של פידנטיק:
from pydantic_ai import Agent, NativeOutput
from tool_output import Fruit, Vehicle
agent = Agent(
'openai:gpt-5.2',
output_type=NativeOutput(
[Fruit, Vehicle],
name='Fruit_or_vehicle',
description='Return a fruit or vehicle.'
),
)
result = agent.run_sync('What is a Ford Explorer?')
print(repr(result.output))
#> Vehicle(name='Ford Explorer', wheels=4)
בשני המקרים התוצאה זהה - פקודת run_sync של הסוכן תחזיר אוביקט שיתאים למודל הפידנטיק שהעברנו.
הפונקציה האחרונה בקובץ היא פונקציית התרגום שקוראת לסוכן ושמתי אותה כאן בתור פונקציית נוחות:
def translate(text: str, languages: list[str]) -> TranslationResult:
"""Translate ``text`` into each language code in ``languages``."""
agent = build_agent()
prompt = (
f"Text to translate:\n{text}\n\n"
f"Target languages: {', '.join(languages)}"
)
return agent.run_sync(prompt).output
הפונקציה לוקחת טקסט ורשימה של שפות ומחזירה תרגום לכל אחת משפות היעד בעזרת הסוכן. קוד חיצוני יכול להשתמש בה בלי לדעת שמדובר ב AI.
3. ממשק שורת פקודה
הדרך הכי קלה להשתמש בסוכן שיצרנו היא דרך ממשק שורת פקודה. זה הקוד מתוך קובץ main.py:
from cli_args import parse_args, parse_languages
from translator import translate
def main():
args = parse_args()
languages = parse_languages(args.to)
result = translate(args.text, languages)
for item in result.translations:
print(f"[{item.language}] {item.translation}")
if __name__ == "__main__":
main()
הפונקציות parse_args ו parse_languages הן פונקציות עזר שמוגדרות בקובץ cli_args.py זה תוכנו:
import argparse
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse command line arguments for the translator CLI."""
parser = argparse.ArgumentParser(
description="Translate text into one or more languages.",
)
parser.add_argument(
"--text",
required=True,
help="The text to translate.",
)
parser.add_argument(
"--to",
required=True,
help="Comma-separated target language codes, e.g. 'es,fr'.",
)
return parser.parse_args(argv)
def parse_languages(value: str) -> list[str]:
"""Split a comma-separated language string into a clean list of codes."""
return [lang.strip() for lang in value.split(",") if lang.strip()]
אפשר להפעיל את הקוד משורת הפקודה עם פקודה כמו:
uv run main.py --text 'hello world' --to es,fr
ולקבל תרגום לשתי השפות.
4. ממשק ווב
הממשק השני לאותו סוכן הוא ממשק ה Web. גם הוא ישתמש באותה פונקציית עזר translate ויעביר את התוצאה לפורמט HTML. זה הקוד הרלוונטי מהקובץ app.py:
@app.post("/translate", response_class=HTMLResponse)
def translate_endpoint(
request: Request,
text: str = Form(...),
languages: list[str] = Form(...),
):
"""Translate the submitted text and render the results template."""
result = translate(text, languages)
return templates.TemplateResponse(
request,
"result.html",
{
"text": text,
"translations": result.translations,
},
)
פלט מובנה אפשר לנו לקחת את אותו פלט של סוכן ולהשתמש בו בצורה תכנותית - גם להדפסה למסוף וגם להדפסה לדפדפן.
5. עכשיו אתם
עדכנו את הקוד כך שישתמש ב NativeOutput וכך במנגנון הפלט המובנה של ספק המודלים. האם יש הבדל בתוצאות? בזמני התגובה?
פידנטיק מאפשר להגדיר תיעוד על שדות מידע במודלים שלו, לדוגמה:
class UserProfile(BaseModel):
username: str = Field(
...,
title="Account Username",
description="The unique handle used to identify the user across the platform."
)
age: int = Field(
None,
description="The age of the user in years. Optional."
)
הוסיפו שדה בשם comments ל Translation והגדירו בתיעוד שזה המקום להכניס טיפים על התרגום, מתי משתמשים במילה, מה משלב השפה, למה נבחר דווקא תרגום זה, האם היו אפשרויות נוספות. שימו לב איך תשובת המודל משתנה רק לפי הפלט השונה שביקשתם.