From e81b43d5597127ef052ed70d7921d974fcd48a70 Mon Sep 17 00:00:00 2001 From: kuzakhmetovartur Date: Sun, 28 Jun 2026 13:18:58 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20solution=20for=20'=D0=AD=D0=BA=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D0=BD:=20=D0=A1=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20(Pydantic)'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +++ README.md | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 +++ src/__init__.py | 1 + src/cli.py | 61 ++++++++++++++++++++++++++++++++++ src/main.py | 68 ++++++++++++++++++++++++++++++++++++++ src/models.py | 15 +++++++++ 7 files changed, 241 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/__init__.py create mode 100644 src/cli.py create mode 100644 src/main.py create mode 100644 src/models.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..fac4208 --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..cd9c50a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +langchain-core>=1.0.0 +langchain-openai>=0.2.0 +pydantic>=2.0 +python-dotenv>=1.0 +click>=8.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..46f0bca --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# This file makes src a Python package. \ No newline at end of file diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..8b2af62 --- /dev/null +++ b/src/cli.py @@ -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() \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..f5af19f --- /dev/null +++ b/src/main.py @@ -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" \ No newline at end of file diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..c8e0a92 --- /dev/null +++ b/src/models.py @@ -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") \ No newline at end of file