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