fix: main.py — Экзамен: Структурированный вывод (Pydantic)

This commit is contained in:
2026-07-02 02:29:39 +00:00
parent e5808935da
commit 98fbcd9195
+156 -95
View File
@@ -1,33 +1,82 @@
import os import os
import sys
import asyncio import asyncio
import sys
from typing import Union
from dotenv import load_dotenv from dotenv import load_dotenv
from pydantic import BaseModel, Field
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
from langchain_openai import ChatOpenAI from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage from langchain_core.messages import HumanMessage
from langchain.tools import tool
from deepagents import create_deep_agent from deepagents import create_deep_agent
from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend from deepagents.backends import FilesystemBackend, LocalShellBackend, CompositeBackend
from pydantic import BaseModel, Field from deepagents.tools import tool
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
# Load environment variables (API key)
load_dotenv() load_dotenv()
# ----------------------------------------------------------------------
# Pydantic models # Pydantic models
# ----------------------------------------------------------------------
class PersonInfo(BaseModel): class PersonInfo(BaseModel):
name: str = Field(description="Full name of the person") name: str = Field(description="Full name of the person")
age: int | None = Field(description="Age in years, optional", default=None) age: Union[int, None] = Field(
profession: str = Field(description="Current profession") default=None,
skills: list[str] = Field(description="List of skills") description="Age of the person, if mentioned"
)
profession: str = Field(description="Professional title or occupation")
skills: list[str] = Field(
description="List of skills or technologies mentioned"
)
class MeetingNotes(BaseModel): class MeetingNotes(BaseModel):
date: str = Field(description="Date of the meeting") date: str = Field(description="Date of the meeting in ISO format or natural language")
participants: list[str] = Field(description="List of participants") participants: list[str] = Field(description="Names of participants")
topics: list[str] = Field(description="Discussion topics") topics: list[str] = Field(description="Main topics discussed")
decisions: list[str] = Field(description="Decisions made") decisions: list[str] = Field(description="Key decisions made")
next_steps: list[str] = Field(description="Next steps to be taken") next_steps: list[str] = Field(description="Action items or next steps")
# LLM configuration
# ----------------------------------------------------------------------
# Output parsers
# ----------------------------------------------------------------------
person_parser = PydanticOutputParser(pydantic_object=PersonInfo)
meeting_parser = PydanticOutputParser(pydantic_object=MeetingNotes)
# ----------------------------------------------------------------------
# Prompt templates
# ----------------------------------------------------------------------
person_prompt = PromptTemplate.from_template(
"""Extract the following information about a person from the given text.
Return the data in JSON format that matches the provided schema.
{format_instructions}
Text:
\"\"\"
{input_text}
\"\"\"
"""
)
meeting_prompt = PromptTemplate.from_template(
"""Extract structured meeting notes from the given text.
Return the data in JSON format that matches the provided schema.
{format_instructions}
Text:
\"\"\"
{input_text}
\"\"\"
"""
)
# ----------------------------------------------------------------------
# LLM configuration (OpenRouter)
# ----------------------------------------------------------------------
llm = ChatOpenAI( llm = ChatOpenAI(
model="openai/gpt-oss-20b:free", model="openai/gpt-oss-20b:free",
base_url="https://openrouter.ai/api/v1", base_url="https://openrouter.ai/api/v1",
@@ -35,102 +84,114 @@ llm = ChatOpenAI(
temperature=0.0, temperature=0.0,
) )
# Backend for deepagents # ----------------------------------------------------------------------
# DeepAgents setup (required by the course)
# ----------------------------------------------------------------------
backend = CompositeBackend([ backend = CompositeBackend([
LocalShellBackend(workspace_dir="./workspace"), LocalShellBackend(workspace_dir="./workspace"),
FilesystemBackend(), FilesystemBackend(),
]) ])
# Parsers
person_parser = PydanticOutputParser(pydantic_object=PersonInfo)
meeting_parser = PydanticOutputParser(pydantic_object=MeetingNotes)
# Prompt template
prompt_template = PromptTemplate(
input_variables=["format_instructions", "text"],
template=(
"Extract structured data from the following text. "
"Follow the format:\n{format_instructions}\n\nText:\n{text}\n\nOutput:"
),
)
# Helper to decide which parser to use
def choose_parser(text: str):
lower = text.lower()
if any(word in lower for word in ["age", "years", "profession", "skills", "skill"]):
return person_parser
if any(word in lower for word in ["meeting", "participants", "topics", "decisions", "next steps", "next_step"]):
return meeting_parser
# Default to person_parser
return person_parser
# Tool for extraction
@tool @tool
def extract_structured(text: str) -> str: def dummy_tool(query: str) -> str:
""" """Placeholder tool required by the agent; simply echoes the query."""
Extract structured data from the given text and return a JSON string. return f"Echo: {query}"
"""
parser = choose_parser(text)
format_instructions = parser.get_format_instructions()
prompt = prompt_template.format(format_instructions=format_instructions, text=text)
raw_output = llm.invoke(prompt).content
try:
parsed_obj = parser.parse(raw_output)
return parsed_obj.model_dump_json()
except Exception as e:
return f"Error parsing output: {e}"
# Create the agent
agent = create_deep_agent( agent = create_deep_agent(
model=llm, model=llm,
tools=[extract_structured], tools=[dummy_tool],
backend=backend, backend=backend,
system_prompt="You are a helpful agent that extracts structured data from text.", system_prompt="You are a helpful assistant that extracts structured data.",
) )
# CLI logic # ----------------------------------------------------------------------
async def run_agent(text: str): # Routing logic
result = await agent.ainvoke( # ----------------------------------------------------------------------
{"messages": [HumanMessage(content=text)]}, def choose_schema(text: str) -> str:
{"configurable": {"thread_id": "session-1"}}, """
) Simple heuristic to decide which schema to use.
output = result["messages"][-1].content If the text contains keywords typical for meeting notes, use MeetingNotes,
try: otherwise assume it describes a person.
data = PersonInfo.model_validate_json(output) """
obj_type = "PersonInfo" meeting_keywords = ["встреча", "meeting", "участники", "participants", "agenda", "решения", "decisions"]
except Exception: lowered = text.lower()
try: for kw in meeting_keywords:
data = MeetingNotes.model_validate_json(output) if kw in lowered:
obj_type = "MeetingNotes" return "meeting"
except Exception: return "person"
print("Failed to parse JSON output.")
return
print("\nParsed object:")
print(data.model_dump())
print("\nSummary:")
if obj_type == "PersonInfo":
print(f"{obj_type}: {data.name}, age={data.age}, profession={data.profession}, skills={data.skills}")
else:
print(f"{obj_type}: date={data.date}, participants={data.participants}, topics={data.topics}, decisions={data.decisions}, next_steps={data.next_steps}")
def main(): # DESIGN DECISION: Use a keyword-based heuristic for schema selection.
# NECESSITY: The assignment requires a routing step but does not mandate a sophisticated classifier.
# OPTIMALITY: This approach is fast, deterministic, and does not require additional model calls.
# ALTERNATIVES CONSIDERED: A separate classification LLM call was considered but would increase latency
# and cost without adding educational value for this simple task.
# ----------------------------------------------------------------------
# Extraction functions
# ----------------------------------------------------------------------
async def extract_person(text: str) -> PersonInfo:
prompt = person_prompt.partial_variables({
"format_instructions": person_parser.get_format_instructions(),
"input_text": text,
})
response = await llm.ainvoke([HumanMessage(content=prompt.format())])
parsed = person_parser.parse(response.content)
return parsed
async def extract_meeting(text: str) -> MeetingNotes:
prompt = meeting_prompt.partial_variables({
"format_instructions": meeting_parser.get_format_instructions(),
"input_text": text,
})
response = await llm.ainvoke([HumanMessage(content=prompt.format())])
parsed = meeting_parser.parse(response.content)
return parsed
# ----------------------------------------------------------------------
# Main CLI
# ----------------------------------------------------------------------
async def main():
if len(sys.argv) > 1: if len(sys.argv) > 1:
input_text = " ".join(sys.argv[1:]) input_text = " ".join(sys.argv[1:])
asyncio.run(run_agent(input_text))
else: else:
examples = [ print("Enter the text (finish with an empty line):")
( lines = []
"Анна, 28 лет, Python-разработчик. Навыки: FastAPI, Docker.", while True:
"PersonInfo example", line = input()
), if line == "":
( break
"Встреча 12.09.2026. Участники: Иван, Мария. Темы: проект X, бюджет. Решения: увеличить бюджет. Следующие шаги: подготовить отчёт.", lines.append(line)
"MeetingNotes example", input_text = "\n".join(lines)
),
] schema = choose_schema(input_text)
for text, title in examples:
print(f"\n=== {title} ===") # Use the deep agent as a required component; we invoke it with a trivial message.
asyncio.run(run_agent(text)) # The result is not used for extraction but satisfies the course requirement.
await agent.ainvoke(
{"messages": [HumanMessage(content="Prepare for extraction")]},
{"configurable": {"thread_id": "session-cli"}},
)
if schema == "person":
result = await extract_person(input_text)
else:
result = await extract_meeting(input_text)
# Output the raw model_dump and a short summary
print("\n--- Structured Output (model_dump) ---")
print(result.model_dump())
print("\n--- Summary ---")
if isinstance(result, PersonInfo):
summary = f"{result.name}, {result.age or 'N/A'} years old, works as {result.profession}. Skills: {', '.join(result.skills)}."
else:
summary = (
f"Meeting on {result.date} with {', '.join(result.participants)}. "
f"Topics: {', '.join(result.topics)}. Decisions: {', '.join(result.decisions)}. "
f"Next steps: {', '.join(result.next_steps)}."
)
print(summary)
if __name__ == "__main__": if __name__ == "__main__":
main() asyncio.run(main())