174 lines
4.9 KiB
Python
174 lines
4.9 KiB
Python
"""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()
|