feat: solution for 'Экзамен: Структурированный вывод (Pydantic)'
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
langchain-core>=1.0.0
|
||||||
|
langchain-openai>=0.2.0
|
||||||
|
pydantic>=2.0
|
||||||
|
python-dotenv>=1.0
|
||||||
|
click>=8.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# This file makes src a Python package.
|
||||||
+61
@@ -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
@@ -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"
|
||||||
@@ -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")
|
||||||
Reference in New Issue
Block a user