feat: solution for 'Экзамен: Структурированный вывод (Pydantic)'

This commit is contained in:
2026-06-28 13:18:58 +03:00
commit e81b43d559
7 changed files with 241 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.env
dist/
build/
*.log
+86
View File
@@ -0,0 +1,86 @@
# Structured Output Extraction with LangChain and Pydantic
This project demonstrates how to extract structured data from raw text using LangChain and Pydantic.
It supports two schemas:
- **PersonInfo** name, age, profession, skills
- **MeetingNotes** date, participants, topics, decisions, next steps
The CLI automatically selects the appropriate schema based on the input text and prints the parsed object and a short summary.
## Prerequisites
- Python 3.10+
- An OpenAI API key (or compatible LLM provider)
## Installation
```bash
# Create a virtual environment (optional but recommended)
python -m venv .venv
source .venv/bin/activate # On Windows use `.venv\Scripts\activate`
# Install dependencies
pip install -r requirements.txt
```
## Configuration
Create a `.env` file in the project root with your OpenAI key:
```
OPENAI_API_KEY=sk-...
```
## Usage
Run the CLI:
```bash
python src/cli.py run
```
You will be prompted to provide text or a file path.
If no input is given, example texts for both schemas are displayed.
### Example
```bash
python src/cli.py run --file example.txt
```
The output will look like:
```
Detected schema: person
Parsed object:
{
"name": "Анна",
"age": 28,
"profession": "Python-разработчик",
"skills": [
"FastAPI",
"Docker"
]
}
Summary:
Person: Анна, age=28, profession=Python-разработчик, skills=FastAPI, Docker
```
## Project Structure
```
src/
├── __init__.py
├── cli.py
├── main.py
└── models.py
requirements.txt
README.md
```
## License
MIT License
+5
View File
@@ -0,0 +1,5 @@
langchain-core>=1.0.0
langchain-openai>=0.2.0
pydantic>=2.0
python-dotenv>=1.0
click>=8.0
+1
View File
@@ -0,0 +1 @@
# This file makes src a Python package.
+61
View File
@@ -0,0 +1,61 @@
import click
from src.main import extract
@click.group()
def cli():
"""Structured Output Extraction CLI."""
pass
@cli.command()
@click.argument("text", required=False)
@click.option("--file", "-f", type=click.Path(exists=True), help="Path to a text file containing the input.")
def run(text, file):
"""
Extract structured data from TEXT or from a file.
If no input is provided, example texts are shown.
"""
if file:
with open(file, "r", encoding="utf-8") as f:
text = f.read()
if not text:
click.echo("No input provided. Showing example texts.\n")
example_person = "Анна, 28 лет, Python-разработчик. Навыки: FastAPI, Docker."
example_meeting = (
"Meeting on 2023-09-15.\n"
"Participants: Alice, Bob, Charlie.\n"
"Topics: Project roadmap, budget.\n"
"Decisions: Approve Q3 budget, assign tasks.\n"
"Next steps: Alice to draft timeline, Bob to update budget spreadsheet."
)
click.echo("Example Person Text:")
click.echo(example_person)
click.echo("\nExample Meeting Text:")
click.echo(example_meeting)
return
result, schema = extract(text)
click.echo(f"\nDetected schema: {schema}")
click.echo("\nParsed object:")
click.echo(result.model_dump_json(indent=2))
# Short summary
if schema == "person":
summary = (
f"Person: {result.name}, "
f"age={result.age if result.age is not None else 'N/A'}, "
f"profession={result.profession}, "
f"skills={', '.join(result.skills)}"
)
else:
summary = (
f"Meeting on {result.date} with participants {', '.join(result.participants)}. "
f"Topics: {', '.join(result.topics)}. "
f"Decisions: {', '.join(result.decisions)}. "
f"Next steps: {', '.join(result.next_steps)}."
)
click.echo("\nSummary:")
click.echo(summary)
if __name__ == "__main__":
cli()
+68
View File
@@ -0,0 +1,68 @@
import os
from typing import Tuple
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from src.models import PersonInfo, MeetingNotes
load_dotenv()
# Configure the LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def build_chain(pydantic_model) -> PromptTemplate:
"""
Build a LangChain chain that parses the given Pydantic model from raw text.
"""
parser = PydanticOutputParser(pydantic_object=pydantic_model)
prompt = PromptTemplate(
input_variables=["text"],
partial_variables={"format_instructions": parser.get_format_instructions()},
template=(
"You are an assistant that extracts structured data from the following text.\n"
"The output must be in JSON format following the schema:\n"
"{format_instructions}\n\n"
"Text:\n{text}\n"
"JSON output:"
),
)
chain = prompt | llm | parser
return chain
# Build chains for each schema
person_chain = build_chain(PersonInfo)
meeting_chain = build_chain(MeetingNotes)
def select_schema(text: str) -> str:
"""
Heuristically determine whether the input text describes a person or a meeting.
"""
meeting_keywords = [
"meeting",
"date",
"participants",
"topics",
"decisions",
"next steps",
"agenda",
"action items",
]
if any(keyword in text.lower() for keyword in meeting_keywords):
return "meeting"
return "person"
def extract(text: str) -> Tuple[object, str]:
"""
Extract structured data from the input text using the appropriate schema.
Returns a tuple of (parsed_object, schema_name).
"""
schema = select_schema(text)
if schema == "meeting":
result = meeting_chain.invoke({"text": text})
return result, "meeting"
else:
result = person_chain.invoke({"text": text})
return result, "person"
+15
View File
@@ -0,0 +1,15 @@
from pydantic import BaseModel, Field
from typing import List, Optional
class PersonInfo(BaseModel):
name: str = Field(..., description="Full name of the person")
age: Optional[int] = Field(None, description="Age of the person, if known")
profession: str = Field(..., description="Profession or job title")
skills: List[str] = Field(..., description="List of skills or technologies")
class MeetingNotes(BaseModel):
date: str = Field(..., description="Date of the meeting in ISO format or natural language")
participants: List[str] = Field(..., description="Names of participants")
topics: List[str] = Field(..., description="Main topics discussed")
decisions: List[str] = Field(..., description="Decisions made during the meeting")
next_steps: List[str] = Field(..., description="Next steps or action items")