Files
2026-06-02 14:56:37 +00:00

174 lines
4.9 KiB
Python
Raw Permalink 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.
"""CLI tool that extracts structured data from text using LangChain and Pydantic.
The tool supports two schemas:
- PersonInfo
- MeetingNotes
It automatically detects which schema to use based on the input text.
"""
import sys
import argparse
from typing import List
from dotenv import load_dotenv
import os
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
# Load environment variables
load_dotenv()
# LLM configuration user can override via env vars
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "")
# ---------- Pydantic models ----------
class PersonInfo(BaseModel):
"""Information about a person."""
name: str = Field(..., description="Person's name")
age: int = Field(..., description="Age of the person")
profession: str = Field(..., description="Profession of the person")
skills: List[str] = Field(..., description="List of skills")
class MeetingNotes(BaseModel):
"""Notes from a meeting."""
title: str = Field(..., description="Title of the meeting")
date: str = Field(..., description="Date of the meeting")
participants: List[str] = Field(..., description="List of participants")
agenda: List[str] = Field(..., description="Agenda items")
# ---------- LLM instance ----------
llm = ChatOpenAI(
model=OPENAI_MODEL,
api_key=OPENAI_API_KEY,
base_url=OPENAI_BASE_URL if OPENAI_BASE_URL else None,
temperature=0,
)
# ---------- Prompts and parsers ----------
# 1. Schema detection prompt
schema_detection_prompt = PromptTemplate(
input_variables=["text"],
template="""
Determine whether the following text is about a person or a meeting. Respond with one of the words:
- PERSON
- MEETING
Text: {text}
Answer:
"""
)
# 2. PersonInfo extraction prompt
person_prompt = PromptTemplate(
input_variables=["text"],
template="""
Extract the following information about a person from the text:
- name
- age
- profession
- skills (comma separated list)
Return a JSON object with fields name, age, profession, skills.
Text: {text}
JSON:
"""
)
person_parser = PydanticOutputParser(pydantic_object=PersonInfo)
person_chain = person_prompt | llm | person_parser
# 3. MeetingNotes extraction prompt
meeting_prompt = PromptTemplate(
input_variables=["text"],
template="""
Extract the following information about a meeting from the text:
- title
- date
- participants (comma separated list)
- agenda (comma separated list)
Return a JSON object with fields title, date, participants, agenda.
Text: {text}
JSON:
"""
)
meeting_parser = PydanticOutputParser(pydantic_object=MeetingNotes)
meeting_chain = meeting_prompt | llm | meeting_parser
# ---------- Helper functions ----------
def detect_schema(text: str) -> str:
"""Return "PERSON" or "MEETING" based on LLM classification."""
try:
result = schema_detection_prompt.invoke({"text": text}, llm=llm)
# The result is a string; strip and uppercase
return result.strip().upper()
except Exception as e:
raise RuntimeError(f"Schema detection failed: {e}")
def extract_person(text: str) -> PersonInfo:
"""Run the person extraction chain."""
try:
return person_chain.invoke({"text": text})
except Exception as e:
raise RuntimeError(f"Person extraction failed: {e}")
def extract_meeting(text: str) -> MeetingNotes:
"""Run the meeting extraction chain."""
try:
return meeting_chain.invoke({"text": text})
except Exception as e:
raise RuntimeError(f"Meeting extraction failed: {e}")
# ---------- CLI ----------
def main() -> None:
parser = argparse.ArgumentParser(description="Extract structured data from text.")
parser.add_argument(
"text",
nargs="?",
help="Input text. If omitted, read from stdin.",
)
args = parser.parse_args()
if args.text:
input_text = args.text
else:
input_text = sys.stdin.read().strip()
if not input_text:
print("No input provided.", file=sys.stderr)
sys.exit(1)
# Detect schema
schema_type = detect_schema(input_text)
if schema_type == "PERSON":
obj = extract_person(input_text)
elif schema_type == "MEETING":
obj = extract_meeting(input_text)
else:
print(f"Unable to determine schema type: {schema_type}", file=sys.stderr)
sys.exit(1)
# Output
print(obj.model_dump(indent=2))
# Simple summary
if isinstance(obj, PersonInfo):
summary = f"{obj.name}, {obj.age} years old, works as {obj.profession}.\n" + f"Skills: {', '.join(obj.skills)}."
else:
summary = f"Meeting '{obj.title}' on {obj.date} with participants {', '.join(obj.participants)}. Agenda: {', '.join(obj.agenda)}."
print("\nSummary:\n" + summary)
if __name__ == "__main__":
main()