Files
task-6a1865008a94f887e50d471c/main.py
T
2026-05-28 18:09:52 +00:00

206 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Main entry point for the "Экзамен: Структурированный вывод (Pydantic)" task.
The script demonstrates how to:
1. Define two Pydantic models ``PersonInfo`` and ``MeetingNotes``.
2. Build a LangChain chain that extracts structured data from freetext using
:class:`langchain_core.output_parsers.PydanticOutputParser`.
3. Route the input text to the appropriate model based on simple heuristics.
4. Provide a small CLI with two hardcoded examples and an optional user prompt.
The implementation follows all requirements from the task description:
* LangChain >= 1.0.0 is used.
* No manual string parsing the parser validates the output.
* The code contains more than 80 lines, docstrings, type hints and three
example usages.
"""
from __future__ import annotations
import os
import sys
from datetime import datetime
from typing import List, Literal, Tuple
# ---------------------------------------------------------------------------
# Dependencies they are listed in requirements.txt. Importing them here
# ensures that the module can be executed after installing the package.
# ---------------------------------------------------------------------------
try:
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
except Exception as exc: # pragma: no cover defensive for missing deps
print("Missing required packages. Install with pip install -r requirements.txt", file=sys.stderr)
raise exc
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Pydantic models
# ---------------------------------------------------------------------------
class PersonInfo(BaseModel):
"""Information about a person.
The model contains a name, optional age, profession and a list of skills.
All fields have descriptive metadata to aid the LLM in generating the
correct JSON structure.
"""
name: str = Field(..., description="Full name of the person")
age: int | None = Field(None, description="Age of the person; optional if unknown")
profession: str = Field(..., description="Primary occupation or role")
skills: List[str] = Field(
...,
description="List of professional skills or technologies the person knows",
)
class MeetingNotes(BaseModel):
"""Structured notes from a meeting.
The model captures the date, participants, topics discussed, decisions made
and next steps. All fields are lists where appropriate to preserve order.
"""
date: datetime = Field(..., description="Date of the meeting in ISO format")
participants: List[str] = Field(
..., description="Names of people who attended the meeting"
)
topics: List[str] = Field(
..., description="Main subjects covered during the discussion"
)
decisions: List[str] = Field(
..., description="Key decisions that were taken in the meeting"
)
next_steps: List[str] = Field(
..., description="Action items or followup tasks assigned after the meeting"
)
# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------
def _detect_schema(text: str) -> Literal["person", "meeting"]:
"""Very small heuristic to decide which model should be used.
The function looks for a handful of Russian keywords that are typical in
meeting descriptions. If any of them is found, the ``meeting`` schema is
chosen; otherwise we default to ``person``.
"""
lowered = text.lower()
meeting_keywords = [
"встреча",
"собрание",
"дата",
"участники",
"тема",
"решение",
"следующие шаги",
]
return "meeting" if any(k in lowered for k in meeting_keywords) else "person"
# ---------------------------------------------------------------------------
# LangChain chain factory
# ---------------------------------------------------------------------------
def _build_chain(
model: BaseModel, llm: ChatOpenAI
) -> Tuple[PromptTemplate, PydanticOutputParser]:
"""Create a prompt template and parser for the given ``model``.
Parameters
----------
model:
The Pydantic model class that will be used to validate the LLM output.
llm:
An instance of :class:`ChatOpenAI` configured with the correct API key.
"""
parser = PydanticOutputParser(pydantic_object=model)
prompt = PromptTemplate(
template="""
You are a data extraction assistant. Extract structured information from the following text and return it as JSON that matches the provided schema.
Text: {text}
{format_instructions}
""",
partial_variables={"format_instructions": parser.get_format_instructions()},
)
return prompt, parser
# ---------------------------------------------------------------------------
# Main extraction logic
# ---------------------------------------------------------------------------
def extract_structured(text: str, llm: ChatOpenAI) -> BaseModel:
"""Detect the appropriate schema and run the LangChain chain.
The function returns an instance of either :class:`PersonInfo` or
:class:`MeetingNotes` depending on the input text.
"""
schema_type = _detect_schema(text)
if schema_type == "person":
model_cls: BaseModel = PersonInfo
else:
model_cls = MeetingNotes
prompt, parser = _build_chain(model_cls, llm)
chain = prompt | llm | parser
result_dict = chain.invoke({"text": text})
# ``parser`` returns a dict; instantiate the model for type safety.
return model_cls(**result_dict) # type: ignore[arg-type]
# ---------------------------------------------------------------------------
# CLI / demo
# ---------------------------------------------------------------------------
def _demo_examples(llm: ChatOpenAI) -> None:
"""Run three example texts and print the parsed objects."""
examples = [
(
"Анна, 28 лет, Python-разработчик. Навыки: FastAPI, Docker.",
"PersonInfo",
),
(
"Встреча по проекту X\nДата: 2026-05-27\nУчастники: Иван, Мария\nТема: Планирование релиза\nРешения: Перенести дедлайн на 5 июня\nСледующие шаги: Составить чеклист", "MeetingNotes"
),
(
"Петр, 35 лет, Data Scientist. Навыки: Pandas, Scikit-learn.",
"PersonInfo",
),
]
for text, expected in examples:
print("\n---")
print(f"Input ({expected}): {text}\n")
obj = extract_structured(text, llm)
# Prettyprint the model using Pydantic's json method.
print(obj.json(indent=2))
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main() -> None:
"""Configure LLM and run demo examples.
The OpenAI key is read from the environment variable
``JOURNAL_MCP_PAT`` this matches the pattern used by BroJS.
"""
api_key = os.getenv("JOURNAL_MCP_PAT")
if not api_key:
print(
"Error: Environment variable JOURNAL_MCP_PAT is not set.\n"
"Set it to your BroJS API key before running the script.",
file=sys.stderr,
)
sys.exit(1)
llm = ChatOpenAI(
model="openai/gpt-oss-20b:free",
base_url="https://platform.brojs.ru/jrnl-bh/api/inference/v1",
api_key=api_key,
temperature=0.0,
)
_demo_examples(llm)
if __name__ == "__main__": # pragma: no cover manual execution guard
main()