ontology
@@ -1,42 +0,0 @@
|
||||
import instructor
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Noticed thhat we use JSON not TOOLS mode
|
||||
client = instructor.from_provider(
|
||||
"anthropic/claude-3-7-sonnet-latest",
|
||||
mode=instructor.Mode.JSON,
|
||||
async_client=False,
|
||||
)
|
||||
|
||||
|
||||
class Citation(BaseModel):
|
||||
id: int
|
||||
url: str
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
citations: list[Citation]
|
||||
response: str
|
||||
|
||||
|
||||
response_data, completion_details = client.messages.create_with_completion(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant that summarizes news articles. Your final response should be only contain a single JSON object returned in your final message to the user. Make sure to provide the exact ids for the citations that support the information you provide in the form of inline citations as [1] [2] [3] which correspond to a unique id you generate for a url that you find in the web search tool which is relevant to your final response.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What are the latest results for the UFC and who won? Answer this in a concise response that's under 3 sentences.",
|
||||
},
|
||||
],
|
||||
tools=[{"type": "web_search_20250305", "name": "web_search", "max_uses": 3}],
|
||||
response_model=Response,
|
||||
)
|
||||
|
||||
print("Response:")
|
||||
print(response_data.response)
|
||||
print("\nCitations:")
|
||||
for citation in response_data.citations:
|
||||
print(f"{citation.id}: {citation.url}")
|
||||
@@ -1,33 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
import anthropic
|
||||
import instructor
|
||||
|
||||
# Patching the Anthropics client with the instructor for enhanced capabilities
|
||||
client = instructor.from_anthropic(anthropic.Anthropic())
|
||||
|
||||
|
||||
class Properties(BaseModel):
|
||||
key: str
|
||||
value: str
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
properties: list[Properties]
|
||||
|
||||
|
||||
user = client.messages.create(
|
||||
model="claude-3-haiku-20240307",
|
||||
max_tokens=1024,
|
||||
max_retries=0,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Create a user for a model with a name, age, and properties.",
|
||||
}
|
||||
],
|
||||
response_model=User,
|
||||
)
|
||||
|
||||
print(user.model_dump_json(indent=2))
|
||||
@@ -1,403 +0,0 @@
|
||||
"""
|
||||
Asyncio Benchmarks with Instructor
|
||||
|
||||
This script demonstrates and benchmarks different asyncio patterns for LLM processing:
|
||||
- Sequential processing (baseline)
|
||||
- asyncio.gather (concurrent, ordered results)
|
||||
- asyncio.as_completed (concurrent, streaming results)
|
||||
- Rate-limited processing with semaphores
|
||||
- Error handling patterns
|
||||
- Progress tracking
|
||||
- Batch processing with chunking
|
||||
|
||||
Run this script to see performance comparisons and verify all code examples work.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
import instructor
|
||||
from pydantic import BaseModel, field_validator
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
import os
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Set up the async client with Instructor
|
||||
client = instructor.from_openai(AsyncOpenAI())
|
||||
sync_client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
occupation: str
|
||||
|
||||
@field_validator("age")
|
||||
@classmethod
|
||||
def validate_age(cls, v):
|
||||
if v < 0 or v > 150:
|
||||
raise ValueError(f"Age {v} is invalid")
|
||||
return v
|
||||
|
||||
|
||||
# Sample dataset
|
||||
dataset = [
|
||||
"John Smith is a 30-year-old software engineer",
|
||||
"Sarah Johnson is a 25-year-old data scientist",
|
||||
"Mike Davis is a 35-year-old product manager",
|
||||
"Lisa Wilson is a 28-year-old UX designer",
|
||||
"Tom Brown is a 32-year-old DevOps engineer",
|
||||
"Emma Garcia is a 27-year-old frontend developer",
|
||||
"David Lee is a 33-year-old backend developer",
|
||||
]
|
||||
|
||||
|
||||
async def extract_person(text: str) -> Person:
|
||||
"""Extract person information from text using LLM."""
|
||||
return await client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=Person,
|
||||
messages=[{"role": "user", "content": f"Extract person info: {text}"}],
|
||||
)
|
||||
|
||||
|
||||
# Method 1: Sequential Processing (Baseline)
|
||||
async def sequential_processing() -> tuple[list[Person], float]:
|
||||
"""Process items one by one - slowest method."""
|
||||
start_time = time.time()
|
||||
persons = []
|
||||
|
||||
for text in dataset:
|
||||
person = await extract_person(text)
|
||||
persons.append(person)
|
||||
print(f"Processed: {person.name}")
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Sequential processing took: {duration:.2f} seconds")
|
||||
return persons, duration
|
||||
|
||||
|
||||
# Method 2: asyncio.gather - Concurrent Processing
|
||||
async def gather_processing() -> tuple[list[Person], float]:
|
||||
"""Process all items concurrently and return in order."""
|
||||
start_time = time.time()
|
||||
|
||||
# Create tasks for all items
|
||||
tasks = [extract_person(text) for text in dataset]
|
||||
|
||||
# Execute all tasks concurrently
|
||||
persons = await asyncio.gather(*tasks)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"asyncio.gather took: {duration:.2f} seconds")
|
||||
|
||||
# Results maintain original order
|
||||
for person in persons:
|
||||
print(f"Processed: {person.name}")
|
||||
|
||||
return persons, duration
|
||||
|
||||
|
||||
# Method 3: asyncio.as_completed - Streaming Results
|
||||
async def as_completed_processing() -> tuple[list[Person], float]:
|
||||
"""Process items concurrently and handle results as they complete."""
|
||||
start_time = time.time()
|
||||
persons = []
|
||||
|
||||
# Create tasks for all items
|
||||
tasks = [extract_person(text) for text in dataset]
|
||||
|
||||
# Process results as they complete
|
||||
for task in asyncio.as_completed(tasks):
|
||||
person = await task
|
||||
persons.append(person)
|
||||
print(f"Completed: {person.name}")
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"asyncio.as_completed took: {duration:.2f} seconds")
|
||||
return persons, duration
|
||||
|
||||
|
||||
# Method 4: Rate-Limited Processing with Semaphores
|
||||
async def rate_limited_extract_person(
|
||||
text: str, semaphore: asyncio.Semaphore
|
||||
) -> Person:
|
||||
"""Extract person info with rate limiting."""
|
||||
async with semaphore:
|
||||
return await extract_person(text)
|
||||
|
||||
|
||||
async def rate_limited_gather(concurrency_limit: int = 3) -> tuple[list[Person], float]:
|
||||
"""Process items with controlled concurrency using asyncio.gather."""
|
||||
start_time = time.time()
|
||||
|
||||
# Create semaphore to limit concurrent requests
|
||||
semaphore = asyncio.Semaphore(concurrency_limit)
|
||||
|
||||
# Create rate-limited tasks
|
||||
tasks = [rate_limited_extract_person(text, semaphore) for text in dataset]
|
||||
|
||||
# Execute with rate limiting
|
||||
persons = await asyncio.gather(*tasks)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(
|
||||
f"Rate-limited gather (limit={concurrency_limit}) took: {duration:.2f} seconds"
|
||||
)
|
||||
return persons, duration
|
||||
|
||||
|
||||
async def rate_limited_as_completed(
|
||||
concurrency_limit: int = 3,
|
||||
) -> tuple[list[Person], float]:
|
||||
"""Process items with controlled concurrency using asyncio.as_completed."""
|
||||
start_time = time.time()
|
||||
persons = []
|
||||
|
||||
# Create semaphore to limit concurrent requests
|
||||
semaphore = asyncio.Semaphore(concurrency_limit)
|
||||
|
||||
# Create rate-limited tasks
|
||||
tasks = [rate_limited_extract_person(text, semaphore) for text in dataset]
|
||||
|
||||
# Process results as they complete
|
||||
for task in asyncio.as_completed(tasks):
|
||||
person = await task
|
||||
persons.append(person)
|
||||
print(f"Rate-limited completed: {person.name}")
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(
|
||||
f"Rate-limited as_completed (limit={concurrency_limit}) took: {duration:.2f} seconds"
|
||||
)
|
||||
return persons, duration
|
||||
|
||||
|
||||
# Advanced Patterns
|
||||
async def robust_gather_processing() -> tuple[list[Person], float]:
|
||||
"""Process items with error handling."""
|
||||
start_time = time.time()
|
||||
tasks = [extract_person(text) for text in dataset]
|
||||
|
||||
# Execute with error handling
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
persons = []
|
||||
for i, result in enumerate(results):
|
||||
if isinstance(result, Exception):
|
||||
print(f"Error processing item {i}: {result}")
|
||||
else:
|
||||
persons.append(result)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Robust gather processing took: {duration:.2f} seconds")
|
||||
return persons, duration
|
||||
|
||||
|
||||
async def timeout_gather_processing(
|
||||
timeout_seconds: float = 30.0,
|
||||
) -> tuple[list[Person], float]:
|
||||
"""Process items with timeout."""
|
||||
start_time = time.time()
|
||||
tasks = [extract_person(text) for text in dataset]
|
||||
|
||||
try:
|
||||
persons = await asyncio.wait_for(
|
||||
asyncio.gather(*tasks), timeout=timeout_seconds
|
||||
)
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Timeout gather processing took: {duration:.2f} seconds")
|
||||
return persons, duration
|
||||
except asyncio.TimeoutError:
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(
|
||||
f"Processing timed out after {timeout_seconds} seconds (took {duration:.2f}s)"
|
||||
)
|
||||
return [], duration
|
||||
|
||||
|
||||
async def progress_tracking_processing() -> tuple[list[Person], float]:
|
||||
"""Process items with progress tracking."""
|
||||
start_time = time.time()
|
||||
persons = []
|
||||
total_items = len(dataset)
|
||||
completed = 0
|
||||
|
||||
tasks = [extract_person(text) for text in dataset]
|
||||
|
||||
for task in asyncio.as_completed(tasks):
|
||||
person = await task
|
||||
persons.append(person)
|
||||
completed += 1
|
||||
print(
|
||||
f"Progress: {completed}/{total_items} ({completed / total_items * 100:.1f}%)"
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Progress tracking processing took: {duration:.2f} seconds")
|
||||
return persons, duration
|
||||
|
||||
|
||||
async def chunked_processing(chunk_size: int = 3) -> tuple[list[Person], float]:
|
||||
"""Process items in chunks to manage memory and rate limits."""
|
||||
start_time = time.time()
|
||||
all_persons = []
|
||||
|
||||
# Process in chunks
|
||||
for i in range(0, len(dataset), chunk_size):
|
||||
chunk = dataset[i : i + chunk_size]
|
||||
print(f"Processing chunk {i // chunk_size + 1}")
|
||||
|
||||
tasks = [extract_person(text) for text in chunk]
|
||||
chunk_results = await asyncio.gather(*tasks)
|
||||
all_persons.extend(chunk_results)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Chunked processing took: {duration:.2f} seconds")
|
||||
return all_persons, duration
|
||||
|
||||
|
||||
async def benchmark_all_methods():
|
||||
"""Run all processing methods and compare performance."""
|
||||
print("=== Python asyncio.gather and asyncio.as_completed Performance Test ===\n")
|
||||
|
||||
# Check if OpenAI API key is set
|
||||
if not os.getenv("OPENAI_API_KEY"):
|
||||
print("⚠️ OPENAI_API_KEY not set. Using mock responses for demonstration.")
|
||||
return
|
||||
|
||||
# Test different methods
|
||||
methods = [
|
||||
("Sequential", sequential_processing),
|
||||
("asyncio.gather", gather_processing),
|
||||
("asyncio.as_completed", as_completed_processing),
|
||||
("Rate-limited gather (3)", lambda: rate_limited_gather(3)),
|
||||
("Rate-limited as_completed (3)", lambda: rate_limited_as_completed(3)),
|
||||
("Robust gather", robust_gather_processing),
|
||||
("Timeout gather", timeout_gather_processing),
|
||||
("Progress tracking", progress_tracking_processing),
|
||||
("Chunked processing", chunked_processing),
|
||||
]
|
||||
|
||||
results = {}
|
||||
|
||||
for name, method in methods:
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"Testing: {name}")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
persons, duration = await method()
|
||||
results[name] = {
|
||||
"count": len(persons),
|
||||
"duration": duration,
|
||||
"success": True,
|
||||
}
|
||||
print(f"✓ Success: {len(persons)} items processed in {duration:.2f}s")
|
||||
|
||||
# Show first few results
|
||||
for person in persons[:3]:
|
||||
print(f" - {person.name}, {person.age}, {person.occupation}")
|
||||
if len(persons) > 3:
|
||||
print(f" ... and {len(persons) - 3} more")
|
||||
|
||||
except Exception as e:
|
||||
results[name] = {
|
||||
"count": 0,
|
||||
"duration": 0,
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
}
|
||||
print(f"✗ Failed: {e}")
|
||||
|
||||
# Print summary table
|
||||
print(f"\n{'=' * 80}")
|
||||
print("PERFORMANCE SUMMARY")
|
||||
print("=" * 80)
|
||||
print(f"{'Method':<25} {'Items':<6} {'Time (s)':<10} {'Speed':<15} {'Status'}")
|
||||
print("-" * 80)
|
||||
|
||||
for name, result in results.items():
|
||||
if result["success"]:
|
||||
speed = (
|
||||
f"{result['count'] / result['duration']:.1f} items/s"
|
||||
if result["duration"] > 0
|
||||
else "N/A"
|
||||
)
|
||||
status = "✓ Success"
|
||||
else:
|
||||
speed = "N/A"
|
||||
status = "✗ Failed"
|
||||
|
||||
print(
|
||||
f"{name:<25} {result['count']:<6} {result['duration']:<10.2f} {speed:<15} {status}"
|
||||
)
|
||||
|
||||
# Calculate speedup compared to sequential
|
||||
if "Sequential" in results and results["Sequential"]["success"]:
|
||||
baseline = results["Sequential"]["duration"]
|
||||
print(f"\nSpeedup compared to sequential processing:")
|
||||
for name, result in results.items():
|
||||
if name != "Sequential" and result["success"] and result["duration"] > 0:
|
||||
speedup = baseline / result["duration"]
|
||||
print(f" {name}: {speedup:.1f}x faster")
|
||||
|
||||
|
||||
def sync_example():
|
||||
"""Show sync version for comparison."""
|
||||
print("\n" + "=" * 50)
|
||||
print("Sync Example (for comparison)")
|
||||
print("=" * 50)
|
||||
|
||||
start_time = time.time()
|
||||
persons = []
|
||||
|
||||
for text in dataset[:3]: # Just first 3 for demo
|
||||
person = sync_client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=Person,
|
||||
messages=[{"role": "user", "content": f"Extract person info: {text}"}],
|
||||
)
|
||||
persons.append(person)
|
||||
print(f"Sync processed: {person.name}")
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
print(f"Sync processing (3 items) took: {duration:.2f} seconds")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function to run all examples."""
|
||||
try:
|
||||
await benchmark_all_methods()
|
||||
|
||||
# Run sync example if API key is available
|
||||
if os.getenv("OPENAI_API_KEY"):
|
||||
sync_example()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n⚠️ Interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
logger.exception("Unexpected error occurred")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🚀 Starting asyncio benchmarks with Instructor...")
|
||||
print("💡 Make sure to set OPENAI_API_KEY environment variable")
|
||||
print("⏱️ This will take a few minutes to complete all benchmarks\n")
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,149 +0,0 @@
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class PriorityEnum(str, Enum):
|
||||
high = "High"
|
||||
medium = "Medium"
|
||||
low = "Low"
|
||||
|
||||
|
||||
class Subtask(BaseModel):
|
||||
"""
|
||||
Correctly resolved subtask from the given transcript
|
||||
"""
|
||||
|
||||
id: int = Field(..., description="Unique identifier for the subtask")
|
||||
name: str = Field(..., description="Informative title of the subtask")
|
||||
|
||||
|
||||
class Ticket(BaseModel):
|
||||
"""
|
||||
Correctly resolved ticket from the given transcript
|
||||
"""
|
||||
|
||||
id: int = Field(..., description="Unique identifier for the ticket")
|
||||
name: str = Field(..., description="Title of the task")
|
||||
description: str = Field(..., description="Detailed description of the task")
|
||||
priority: PriorityEnum = Field(..., description="Priority level")
|
||||
assignees: list[str] = Field(..., description="List of users assigned to the task")
|
||||
subtasks: Optional[list[Subtask]] = Field(
|
||||
None, description="List of subtasks associated with the main task"
|
||||
)
|
||||
dependencies: Optional[list[int]] = Field(
|
||||
None, description="List of ticket IDs that this ticket depends on"
|
||||
)
|
||||
|
||||
|
||||
class ActionItems(BaseModel):
|
||||
"""
|
||||
Correctly resolved set of action items from the given transcript
|
||||
"""
|
||||
|
||||
items: list[Ticket]
|
||||
|
||||
|
||||
def generate(data: str):
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=ActionItems,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "The following is a transcript of a meeting between a manager and their team. The manager is assigning tasks to their team members and creating action items for them to complete.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Create the action items for the following transcript: {data}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
prediction = generate(
|
||||
"""
|
||||
Alice: Hey team, we have several critical tasks we need to tackle for the upcoming release. First, we need to work on improving the authentication system. It's a top priority.
|
||||
|
||||
Bob: Got it, Alice. I can take the lead on the authentication improvements. Are there any specific areas you want me to focus on?
|
||||
|
||||
Alice: Good question, Bob. We need both a front-end revamp and back-end optimization. So basically, two sub-tasks.
|
||||
|
||||
Carol: I can help with the front-end part of the authentication system.
|
||||
|
||||
Bob: Great, Carol. I'll handle the back-end optimization then.
|
||||
|
||||
Alice: Perfect. Now, after the authentication system is improved, we have to integrate it with our new billing system. That's a medium priority task.
|
||||
|
||||
Carol: Is the new billing system already in place?
|
||||
|
||||
Alice: No, it's actually another task. So it's a dependency for the integration task. Bob, can you also handle the billing system?
|
||||
|
||||
Bob: Sure, but I'll need to complete the back-end optimization of the authentication system first, so it's dependent on that.
|
||||
|
||||
Alice: Understood. Lastly, we also need to update our user documentation to reflect all these changes. It's a low-priority task but still important.
|
||||
|
||||
Carol: I can take that on once the front-end changes for the authentication system are done. So, it would be dependent on that.
|
||||
|
||||
Alice: Sounds like a plan. Let's get these tasks modeled out and get started."""
|
||||
)
|
||||
|
||||
print(prediction.model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Improve Authentication System",
|
||||
"description": "Revamp the front-end and optimize the back-end of the authentication system",
|
||||
"priority": "High",
|
||||
"assignees": [
|
||||
"Bob",
|
||||
"Carol"
|
||||
],
|
||||
"subtasks": [
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Front-end Revamp"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Back-end Optimization"
|
||||
}
|
||||
],
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Integrate Authentication System with Billing System",
|
||||
"description": "Integrate the improved authentication system with the new billing system",
|
||||
"priority": "Medium",
|
||||
"assignees": [
|
||||
"Bob"
|
||||
],
|
||||
"subtasks": [],
|
||||
"dependencies": [
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Update User Documentation",
|
||||
"description": "Update the user documentation to reflect the changes in the authentication system",
|
||||
"priority": "Low",
|
||||
"assignees": [
|
||||
"Carol"
|
||||
],
|
||||
"subtasks": [],
|
||||
"dependencies": [
|
||||
2
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
Before Width: | Height: | Size: 243 KiB |
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Example demonstrating the unified provider interface with string-based initialization.
|
||||
Creates clients for multiple providers with both sync and async interfaces.
|
||||
"""
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
from typing import Any
|
||||
import instructor
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""Simple model to extract user information from text."""
|
||||
|
||||
name: str = Field(description="The user's full name")
|
||||
age: int = Field(description="The user's age in years")
|
||||
occupation: str = Field(description="The user's job or profession")
|
||||
|
||||
|
||||
async def test_async_client(
|
||||
client_name: str, client: instructor.AsyncInstructor
|
||||
) -> dict[str, Any]:
|
||||
"""Test an async client and return the results."""
|
||||
print(f"Testing async client: {client_name}")
|
||||
try:
|
||||
result = await client.chat.completions.create(
|
||||
response_model=UserInfo,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "John Smith is a 35-year-old software engineer.",
|
||||
}
|
||||
],
|
||||
)
|
||||
print(f"✅ Async {client_name} result: {result.model_dump()}")
|
||||
return {"provider": client_name, "success": True, "result": result.model_dump()}
|
||||
except Exception as e:
|
||||
print(f"❌ Async {client_name} error: {str(e)}")
|
||||
return {"provider": client_name, "success": False, "error": str(e)}
|
||||
|
||||
|
||||
def test_sync_client(client_name: str, client: instructor.Instructor) -> dict[str, Any]:
|
||||
"""Test a sync client and return the results."""
|
||||
print(f"Testing sync client: {client_name}")
|
||||
try:
|
||||
result = client.chat.completions.create(
|
||||
response_model=UserInfo,
|
||||
messages=[
|
||||
{"role": "user", "content": "Jane Doe is a 28-year-old data scientist."}
|
||||
],
|
||||
)
|
||||
print(f"✅ Sync {client_name} result: {result.model_dump()}")
|
||||
return {"provider": client_name, "success": True, "result": result.model_dump()}
|
||||
except Exception as e:
|
||||
print(f"❌ Sync {client_name} error: {str(e)}")
|
||||
return {"provider": client_name, "success": False, "error": str(e)}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Create and test multiple clients using the unified provider interface."""
|
||||
# Collect the test results
|
||||
sync_results = []
|
||||
async_results = []
|
||||
|
||||
# Test OpenAI clients
|
||||
if os.environ.get("OPENAI_API_KEY"):
|
||||
# Sync client
|
||||
openai_client = instructor.from_provider("openai/gpt-3.5-turbo")
|
||||
sync_results.append(test_sync_client("OpenAI", openai_client))
|
||||
|
||||
# Async client
|
||||
openai_async = instructor.from_provider(
|
||||
"openai/gpt-3.5-turbo", async_client=True
|
||||
)
|
||||
async_results.append(
|
||||
asyncio.create_task(test_async_client("OpenAI", openai_async))
|
||||
)
|
||||
else:
|
||||
print("⚠️ OPENAI_API_KEY not set, skipping OpenAI tests")
|
||||
|
||||
# Test Anthropic clients
|
||||
if os.environ.get("ANTHROPIC_API_KEY"):
|
||||
# Sync client
|
||||
anthropic_client = instructor.from_provider(
|
||||
model="anthropic/claude-3-haiku-20240307", max_tokens=400
|
||||
)
|
||||
sync_results.append(test_sync_client("Anthropic", anthropic_client))
|
||||
|
||||
# Async client
|
||||
anthropic_async = instructor.from_provider(
|
||||
model="anthropic/claude-3-haiku-20240307", async_client=True, max_tokens=400
|
||||
)
|
||||
async_results.append(
|
||||
asyncio.create_task(test_async_client("Anthropic", anthropic_async))
|
||||
)
|
||||
else:
|
||||
print("⚠️ ANTHROPIC_API_KEY not set, skipping Anthropic tests")
|
||||
|
||||
# Test Cohere clients
|
||||
if os.environ.get("COHERE_API_KEY"):
|
||||
# Sync client
|
||||
cohere_client = instructor.from_provider("cohere/command")
|
||||
sync_results.append(test_sync_client("Cohere", cohere_client))
|
||||
|
||||
# Async client
|
||||
cohere_async = instructor.from_provider("cohere/command", async_client=True)
|
||||
async_results.append(
|
||||
asyncio.create_task(test_async_client("Cohere", cohere_async))
|
||||
)
|
||||
else:
|
||||
print("⚠️ COHERE_API_KEY not set, skipping Cohere tests")
|
||||
|
||||
# Test Mistral clients
|
||||
if os.environ.get("MISTRAL_API_KEY"):
|
||||
# Sync client
|
||||
mistral_client = instructor.from_provider("mistral/mistral-small")
|
||||
sync_results.append(test_sync_client("Mistral", mistral_client))
|
||||
|
||||
# Async client
|
||||
mistral_async = instructor.from_provider(
|
||||
"mistral/mistral-small", async_client=True
|
||||
)
|
||||
async_results.append(
|
||||
asyncio.create_task(test_async_client("Mistral", mistral_async))
|
||||
)
|
||||
else:
|
||||
print("⚠️ MISTRAL_API_KEY not set, skipping Mistral tests")
|
||||
|
||||
# Process async results
|
||||
if async_results:
|
||||
completed_tasks = await asyncio.gather(*async_results)
|
||||
async_results = completed_tasks
|
||||
|
||||
# Print summary
|
||||
print("\n----- Test Results Summary -----")
|
||||
|
||||
print("\nSync Clients:")
|
||||
for result in sync_results:
|
||||
if result.get("success", False):
|
||||
print(f"✅ {result['provider']} - Success")
|
||||
else:
|
||||
print(
|
||||
f"❌ {result['provider']} - Failed: {result.get('error', 'Unknown error')}"
|
||||
)
|
||||
|
||||
print("\nAsync Clients:")
|
||||
for result in async_results:
|
||||
if result.get("success", False):
|
||||
print(f"✅ {result['provider']} - Success")
|
||||
else:
|
||||
print(
|
||||
f"❌ {result['provider']} - Failed: {result.get('error', 'Unknown error')}"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,133 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class DateRange(BaseModel):
|
||||
explain: str = Field(
|
||||
...,
|
||||
description="Explain the date range in the context of the text before generating the date range and the repeat pattern.",
|
||||
)
|
||||
repeats: Literal["daily", "weekly", "monthly", None] = Field(
|
||||
default=None,
|
||||
description="If the date range repeats, and how often, this way we can generalize the date range to the future., if its special, then we can assume it is a one time event.",
|
||||
)
|
||||
days_of_week: list[
|
||||
Literal[
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
None,
|
||||
]
|
||||
] = Field(
|
||||
...,
|
||||
description="If the date range repeats, which days of the week does it repeat on.",
|
||||
)
|
||||
time_start: datetime = Field(
|
||||
description="The start of the first time range in the day."
|
||||
)
|
||||
time_end: datetime = Field(
|
||||
description="The end of the first time range in the day."
|
||||
)
|
||||
|
||||
|
||||
class AvailabilityResponse(BaseModel):
|
||||
availability: list[DateRange]
|
||||
|
||||
|
||||
def prepare_dates(n=7) -> str:
|
||||
# Current date and time
|
||||
now = datetime.now()
|
||||
|
||||
acc = ""
|
||||
# Loop for the next 7 days
|
||||
for i in range(n):
|
||||
# Calculate the date for each day
|
||||
day = now + timedelta(days=i)
|
||||
# Print the day of the week, date, and time
|
||||
acc += "\n" + day.strftime("%A, %Y-%m-%d %H:%M:%S")
|
||||
|
||||
return acc.strip()
|
||||
|
||||
|
||||
def parse_availability(text: str) -> Iterable[AvailabilityResponse]:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4-1106-preview",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a state of the art date range parse designed to correctly extract availabilities.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": text,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"To help you understand the dates, here are the next 7 days: {prepare_dates()}",
|
||||
},
|
||||
],
|
||||
response_model=Iterable[AvailabilityResponse],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = """
|
||||
#1
|
||||
|
||||
12/8-12/24
|
||||
9am - 5pm Monday - Saturday
|
||||
10am - 5pm Sunday
|
||||
|
||||
#2
|
||||
We are open Friday, after Thanksgiving, and then Saturdays and Sundays 9 a.m. till dusk.``
|
||||
"""
|
||||
schedules = parse_availability(text)
|
||||
for schedule in schedules:
|
||||
print(schedule.model_dump_json(indent=2))
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"explain": "For the first date range, the availability is from December 8 to December 24, from 9 am to 5 pm on Mondays through Saturdays",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
],
|
||||
"time_start": "2023-12-08T09:00:00",
|
||||
"time_end": "2023-12-08T17:00:00",
|
||||
},
|
||||
{
|
||||
"explain": "For the same date range, the availability on Sundays is from 10 am to 5 pm",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": ["sunday"],
|
||||
"time_start": "2023-12-10T10:00:00",
|
||||
"time_end": "2023-12-10T17:00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"explain": "The second date range starting from the Friday after Thanksgiving, which is November 24, 2023, and then on Saturdays and Sundays from 9 am until dusk. Assuming 'dusk' means approximately 5 pm, similar to the previous timings.",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": ["friday", "saturday", "sunday"],
|
||||
"time_start": "2023-11-24T09:00:00",
|
||||
"time_end": "2023-11-24T17:00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import os
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
client = instructor.from_openai(
|
||||
OpenAI(
|
||||
base_url="https://api.endpoints.anyscale.com/v1",
|
||||
api_key=os.environ["ANYSCALE_API_KEY"],
|
||||
),
|
||||
mode=instructor.Mode.JSON_SCHEMA,
|
||||
model="mistralai/Mixtral-8x7B-Instruct-v0.1",
|
||||
)
|
||||
|
||||
|
||||
class DateRange(BaseModel):
|
||||
explain: str = Field(
|
||||
...,
|
||||
description="Explain the date range in the context of the text before generating the date range and the repeat pattern.",
|
||||
)
|
||||
repeats: Literal["daily", "weekly", "monthly", None] = Field(
|
||||
default=None,
|
||||
description="If the date range repeats, and how often, this way we can generalize the date range to the future., if its special, then we can assume it is a one time event.",
|
||||
)
|
||||
days_of_week: list[
|
||||
Literal[
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
"sunday",
|
||||
None,
|
||||
]
|
||||
] = Field(
|
||||
...,
|
||||
description="If the date range repeats, which days of the week does it repeat on.",
|
||||
)
|
||||
time_start: datetime = Field(
|
||||
description="The start of the first time range in the day."
|
||||
)
|
||||
time_end: datetime = Field(
|
||||
description="The end of the first time range in the day."
|
||||
)
|
||||
|
||||
|
||||
class AvailabilityResponse(BaseModel):
|
||||
availability: list[DateRange]
|
||||
|
||||
|
||||
def prepare_dates(n=7) -> str:
|
||||
# Current date and time
|
||||
now = datetime.now()
|
||||
|
||||
acc = ""
|
||||
# Loop for the next 7 days
|
||||
for i in range(n):
|
||||
# Calculate the date for each day
|
||||
day = now + timedelta(days=i)
|
||||
# Print the day of the week, date, and time
|
||||
acc += "\n" + day.strftime("%A, %Y-%m-%d %H:%M:%S")
|
||||
|
||||
return acc.strip()
|
||||
|
||||
|
||||
def parse_availability(text: str):
|
||||
return client.chat.completions.create_iterable(
|
||||
max_tokens=10000,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a state of the art date range parse designed to correctly extract availabilities.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": text,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"To help you understand the dates, here are the next 7 days: {prepare_dates()}",
|
||||
},
|
||||
],
|
||||
response_model=AvailabilityResponse,
|
||||
max_retries=3,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = """
|
||||
#1
|
||||
|
||||
12/8-12/24
|
||||
9am - 5pm Monday - Saturday
|
||||
10am - 5pm Sunday
|
||||
|
||||
#2
|
||||
We are open Friday, after Thanksgiving, and then Saturdays and Sundays 9 a.m. till dusk.``
|
||||
"""
|
||||
schedules = parse_availability(text)
|
||||
for schedule in schedules:
|
||||
print(schedule.model_dump_json(indent=2))
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"explain": "For the first date range, the availability is from December 8 to December 24, from 9 am to 5 pm on Mondays through Saturdays",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": [
|
||||
"monday",
|
||||
"tuesday",
|
||||
"wednesday",
|
||||
"thursday",
|
||||
"friday",
|
||||
"saturday",
|
||||
],
|
||||
"time_start": "2023-12-08T09:00:00",
|
||||
"time_end": "2023-12-08T17:00:00",
|
||||
},
|
||||
{
|
||||
"explain": "For the same date range, the availability on Sundays is from 10 am to 5 pm",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": ["sunday"],
|
||||
"time_start": "2023-12-10T10:00:00",
|
||||
"time_end": "2023-12-10T17:00:00",
|
||||
},
|
||||
]
|
||||
}
|
||||
{
|
||||
"availability": [
|
||||
{
|
||||
"explain": "The second date range starting from the Friday after Thanksgiving, which is November 24, 2023, and then on Saturdays and Sundays from 9 am until dusk. Assuming 'dusk' means approximately 5 pm, similar to the previous timings.",
|
||||
"repeats": "weekly",
|
||||
"days_of_week": ["friday", "saturday", "sunday"],
|
||||
"time_start": "2023-11-24T09:00:00",
|
||||
"time_end": "2023-11-24T17:00:00",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import instructor
|
||||
import asyncio
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from enum import Enum
|
||||
|
||||
client = instructor.from_openai(AsyncOpenAI(), mode=instructor.Mode.TOOLS)
|
||||
sem = asyncio.Semaphore(5)
|
||||
|
||||
|
||||
class QuestionType(Enum):
|
||||
CONTACT = "CONTACT"
|
||||
TIMELINE_QUERY = "TIMELINE_QUERY"
|
||||
DOCUMENT_SEARCH = "DOCUMENT_SEARCH"
|
||||
COMPARE_CONTRAST = "COMPARE_CONTRAST"
|
||||
EMAIL = "EMAIL"
|
||||
PHOTOS = "PHOTOS"
|
||||
SUMMARY = "SUMMARY"
|
||||
|
||||
|
||||
# You can add more instructions and examples in the description
|
||||
# or you can put it in the prompt in `messages=[...]`
|
||||
class QuestionClassification(BaseModel):
|
||||
"""
|
||||
Predict the type of question that is being asked.
|
||||
Here are some tips on how to predict the question type:
|
||||
CONTACT: Searches for some contact information.
|
||||
TIMELINE_QUERY: "When did something happen?
|
||||
DOCUMENT_SEARCH: "Find me a document"
|
||||
COMPARE_CONTRAST: "Compare and contrast two things"
|
||||
EMAIL: "Find me an email, search for an email"
|
||||
PHOTOS: "Find me a photo, search for a photo"
|
||||
SUMMARY: "Summarize a large amount of data"
|
||||
"""
|
||||
|
||||
# If you want only one classification, just change it to
|
||||
# `classification: QuestionType` rather than `classifications: List[QuestionType]``
|
||||
chain_of_thought: str = Field(
|
||||
..., description="The chain of thought that led to the classification"
|
||||
)
|
||||
classification: list[QuestionType] = Field(
|
||||
description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used",
|
||||
)
|
||||
|
||||
@field_validator("classification", mode="before")
|
||||
def validate_classification(cls, v):
|
||||
# sometimes the API returns a single value, just make sure it's a list
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
return v
|
||||
|
||||
|
||||
# Modify the classify function
|
||||
async def classify(data: str):
|
||||
async with sem: # some simple rate limiting
|
||||
return data, await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
response_model=QuestionClassification,
|
||||
max_retries=2,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following question: {data}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def main(questions: list[str]):
|
||||
tasks = [classify(question) for question in questions]
|
||||
resps = []
|
||||
for task in asyncio.as_completed(tasks):
|
||||
question, label = await task
|
||||
resp = {
|
||||
"question": question,
|
||||
"classification": [c.value for c in label.classification],
|
||||
"chain_of_thought": label.chain_of_thought,
|
||||
}
|
||||
resps.append(resp)
|
||||
return resps
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
questions = [
|
||||
"What was that ai app that i saw on the news the other day?",
|
||||
"Can you find the trainline booking email?",
|
||||
"What was the book I saw on amazon yesturday?",
|
||||
"Can you speak german?",
|
||||
"Do you have access to the meeting transcripts?",
|
||||
"what are the recent sites I visited?",
|
||||
"what did I do on Monday?",
|
||||
"Tell me about todays meeting and how it relates to the email on Monday",
|
||||
]
|
||||
|
||||
asyncio.run(main(questions))
|
||||
@@ -1,100 +0,0 @@
|
||||
import json
|
||||
import instructor
|
||||
import asyncio
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from enum import Enum
|
||||
|
||||
client = AsyncOpenAI()
|
||||
client = instructor.from_openai(client, mode=instructor.Mode.TOOLS)
|
||||
sem = asyncio.Semaphore(5)
|
||||
|
||||
|
||||
class QuestionType(Enum):
|
||||
CONTACT = "CONTACT"
|
||||
TIMELINE_QUERY = "TIMELINE_QUERY"
|
||||
DOCUMENT_SEARCH = "DOCUMENT_SEARCH"
|
||||
COMPARE_CONTRAST = "COMPARE_CONTRAST"
|
||||
EMAIL = "EMAIL"
|
||||
PHOTOS = "PHOTOS"
|
||||
SUMMARY = "SUMMARY"
|
||||
|
||||
|
||||
# You can add more instructions and examples in the description
|
||||
# or you can put it in the prompt in `messages=[...]`
|
||||
class QuestionClassification(BaseModel):
|
||||
"""
|
||||
Predict the type of question that is being asked.
|
||||
Here are some tips on how to predict the question type:
|
||||
CONTACT: Searches for some contact information.
|
||||
TIMELINE_QUERY: "When did something happen?
|
||||
DOCUMENT_SEARCH: "Find me a document"
|
||||
COMPARE_CONTRAST: "Compare and contrast two things"
|
||||
EMAIL: "Find me an email, search for an email"
|
||||
PHOTOS: "Find me a photo, search for a photo"
|
||||
SUMMARY: "Summarize a large amount of data"
|
||||
"""
|
||||
|
||||
# If you want only one classification, just change it to
|
||||
# `classification: QuestionType` rather than `classifications: List[QuestionType]``
|
||||
chain_of_thought: str = Field(
|
||||
..., description="The chain of thought that led to the classification"
|
||||
)
|
||||
classification: list[QuestionType] = Field(
|
||||
description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used",
|
||||
)
|
||||
|
||||
@field_validator("classification", mode="before")
|
||||
def validate_classification(cls, v):
|
||||
# sometimes the API returns a single value, just make sure it's a list
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
return v
|
||||
|
||||
|
||||
async def classify(data: str):
|
||||
async with sem: # some simple rate limiting
|
||||
return data, await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
response_model=QuestionClassification,
|
||||
max_retries=2,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following question: {data}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def main(questions: list[str], *, path_to_jsonl: str = None):
|
||||
tasks = [classify(question) for question in questions]
|
||||
for task in asyncio.as_completed(tasks):
|
||||
question, label = await task
|
||||
resp = {
|
||||
"question": question,
|
||||
"classification": [c.value for c in label.classification],
|
||||
}
|
||||
print(resp)
|
||||
if path_to_jsonl:
|
||||
with open(path_to_jsonl, "a") as f:
|
||||
json_dump = json.dumps(resp)
|
||||
f.write(json_dump + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
questions = [
|
||||
"What was that ai app that i saw on the news the other day?",
|
||||
"Can you find the trainline booking email?",
|
||||
"What was the book I saw on amazon yesturday?",
|
||||
"Can you speak german?",
|
||||
"Do you have access to the meeting transcripts?",
|
||||
"what are the recent sites I visited?",
|
||||
"what did I do on Monday?",
|
||||
"Tell me about todays meeting and how it relates to the email on Monday",
|
||||
]
|
||||
|
||||
asyncio.run(main(questions))
|
||||
@@ -1,103 +0,0 @@
|
||||
import instructor
|
||||
import asyncio
|
||||
|
||||
from langsmith import traceable
|
||||
from langsmith.wrappers import wrap_openai
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from enum import Enum
|
||||
|
||||
client = wrap_openai(AsyncOpenAI())
|
||||
client = instructor.from_openai(client, mode=instructor.Mode.TOOLS)
|
||||
sem = asyncio.Semaphore(5)
|
||||
|
||||
|
||||
class QuestionType(Enum):
|
||||
CONTACT = "CONTACT"
|
||||
TIMELINE_QUERY = "TIMELINE_QUERY"
|
||||
DOCUMENT_SEARCH = "DOCUMENT_SEARCH"
|
||||
COMPARE_CONTRAST = "COMPARE_CONTRAST"
|
||||
EMAIL = "EMAIL"
|
||||
PHOTOS = "PHOTOS"
|
||||
SUMMARY = "SUMMARY"
|
||||
|
||||
|
||||
# You can add more instructions and examples in the description
|
||||
# or you can put it in the prompt in `messages=[...]`
|
||||
class QuestionClassification(BaseModel):
|
||||
"""
|
||||
Predict the type of question that is being asked.
|
||||
Here are some tips on how to predict the question type:
|
||||
CONTACT: Searches for some contact information.
|
||||
TIMELINE_QUERY: "When did something happen?
|
||||
DOCUMENT_SEARCH: "Find me a document"
|
||||
COMPARE_CONTRAST: "Compare and contrast two things"
|
||||
EMAIL: "Find me an email, search for an email"
|
||||
PHOTOS: "Find me a photo, search for a photo"
|
||||
SUMMARY: "Summarize a large amount of data"
|
||||
"""
|
||||
|
||||
# If you want only one classification, just change it to
|
||||
# `classification: QuestionType` rather than `classifications: List[QuestionType]``
|
||||
chain_of_thought: str = Field(
|
||||
..., description="The chain of thought that led to the classification"
|
||||
)
|
||||
classification: list[QuestionType] = Field(
|
||||
description=f"An accuracy and correct prediction predicted class of question. Only allowed types: {[t.value for t in QuestionType]}, should be used",
|
||||
)
|
||||
|
||||
@field_validator("classification", mode="before")
|
||||
def validate_classification(cls, v):
|
||||
# sometimes the API returns a single value, just make sure it's a list
|
||||
if not isinstance(v, list):
|
||||
v = [v]
|
||||
return v
|
||||
|
||||
|
||||
# Modify the classify function
|
||||
@traceable(name="classify-question")
|
||||
async def classify(data: str):
|
||||
async with sem: # some simple rate limiting
|
||||
return data, await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
response_model=QuestionClassification,
|
||||
max_retries=2,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following question: {data}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
async def main(questions: list[str]):
|
||||
tasks = [classify(question) for question in questions]
|
||||
resps = []
|
||||
for task in asyncio.as_completed(tasks):
|
||||
question, label = await task
|
||||
resp = {
|
||||
"question": question,
|
||||
"classification": [c.value for c in label.classification],
|
||||
"chain_of_thought": label.chain_of_thought,
|
||||
}
|
||||
resps.append(resp)
|
||||
return resps
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
questions = [
|
||||
"What was that ai app that i saw on the news the other day?",
|
||||
"Can you find the trainline booking email?",
|
||||
"What was the book I saw on amazon yesturday?",
|
||||
"Can you speak german?",
|
||||
"Do you have access to the meeting transcripts?",
|
||||
"what are the recent sites I visited?",
|
||||
"what did I do on Monday?",
|
||||
"Tell me about todays meeting and how it relates to the email on Monday",
|
||||
]
|
||||
|
||||
asyncio.run(main(questions))
|
||||
@@ -1,223 +0,0 @@
|
||||
# Batch API Examples
|
||||
|
||||
This directory contains examples and test scripts for Instructor's batch processing capabilities, including both traditional file-based and new in-memory processing.
|
||||
|
||||
## Examples
|
||||
|
||||
### 1. In-Memory Batch Processing (`in_memory_batch_example.py`)
|
||||
|
||||
Demonstrates the new in-memory batch processing feature, perfect for serverless deployments:
|
||||
|
||||
```bash
|
||||
python in_memory_batch_example.py
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- No disk I/O required - ideal for serverless environments
|
||||
- BytesIO buffers instead of temporary files
|
||||
- Automatic cleanup - no file management needed
|
||||
- Security benefits - no temporary files on disk
|
||||
|
||||
### 2. Unified Test Script (`run_batch_test.py`)
|
||||
|
||||
Tests the unified BatchProcessor with all supported providers: OpenAI, Anthropic, and Google Gemini.
|
||||
|
||||
The script creates a batch job to extract structured `User(name: str, age: int)` data from 10 text examples and saves the batch ID for later checking. Since batch jobs can take time to complete, the script returns immediately after creation.
|
||||
|
||||
## Unified Test Script (`run_batch_test.py`)
|
||||
|
||||
Tests the unified BatchProcessor with any supported provider/model combination.
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Test OpenAI
|
||||
export OPENAI_API_KEY="your-openai-api-key"
|
||||
python run_batch_test.py create --model "openai/gpt-4o-mini"
|
||||
|
||||
# Test Anthropic
|
||||
export ANTHROPIC_API_KEY="your-anthropic-api-key"
|
||||
python run_batch_test.py create --model "anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
# Test Google (simulation mode)
|
||||
python run_batch_test.py create --model "google/gemini-2.0-flash-001"
|
||||
```
|
||||
|
||||
### Supported Models
|
||||
|
||||
Use the `list-models` command to see all supported models:
|
||||
|
||||
```bash
|
||||
python run_batch_test.py list-models
|
||||
```
|
||||
|
||||
**OpenAI Models:**
|
||||
- `openai/gpt-4o-mini`
|
||||
- `openai/gpt-4o`
|
||||
- `openai/gpt-4-turbo`
|
||||
|
||||
**Anthropic Models:**
|
||||
- `anthropic/claude-3-5-sonnet-20241022`
|
||||
- `anthropic/claude-3-opus-20240229`
|
||||
- `anthropic/claude-3-haiku-20240307`
|
||||
|
||||
**Google Models:**
|
||||
- `google/gemini-2.0-flash-001`
|
||||
- `google/gemini-pro`
|
||||
- `google/gemini-pro-vision`
|
||||
|
||||
### What the Script Does
|
||||
|
||||
1. **Creates test messages**: 10 prompts containing user information
|
||||
2. **Uses BatchProcessor**: Leverages the unified API with provider detection
|
||||
3. **Generates batch file**: Provider-specific format with JSON schema
|
||||
4. **Submits batch job**: Actual API call to create the batch
|
||||
5. **Saves batch ID**: Stores ID in `{provider}_batch_id.txt`
|
||||
6. **Returns immediately**: No waiting for completion
|
||||
|
||||
### API Keys Required
|
||||
|
||||
| Provider | Environment Variable | Required |
|
||||
|----------|---------------------|----------|
|
||||
| OpenAI | `OPENAI_API_KEY` | Yes |
|
||||
| Anthropic | `ANTHROPIC_API_KEY` | Yes |
|
||||
| Google | `GOOGLE_API_KEY` | No (simulation mode) |
|
||||
|
||||
### Output Files
|
||||
|
||||
Each run creates:
|
||||
- `{provider}_batch_id.txt` - Contains the batch ID for status checking
|
||||
- Temporary batch files (automatically cleaned up)
|
||||
|
||||
### Test Data
|
||||
|
||||
All providers use the same 10 test prompts:
|
||||
|
||||
1. "Hi there! My name is Alice and I'm 28 years old. I work as a software engineer."
|
||||
2. "Hello, I'm Bob, 35 years old, and I love hiking and photography."
|
||||
3. "This is Sarah speaking. I'm 42 and I'm a graphic designer."
|
||||
4. "Hey! John here, I'm 29 years old and I teach high school math."
|
||||
5. "I'm Emma, 33 years old, currently working as a marketing manager."
|
||||
6. "My name is Michael and I'm 45 years old. I'm a chef at a downtown restaurant."
|
||||
7. "I'm Lisa, 31 years old, working as a nurse at the local hospital."
|
||||
8. "This is David, 38 years old, I'm a freelance photographer."
|
||||
9. "Hello, I'm Jessica, 26 years old, and I'm a data scientist."
|
||||
10. "I'm Ryan, 41 years old, working in software development for a tech startup."
|
||||
|
||||
### Expected Results
|
||||
|
||||
Each batch job should extract `User` objects:
|
||||
|
||||
```python
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
```
|
||||
|
||||
Expected extractions:
|
||||
- Alice, 28 | Bob, 35 | Sarah, 42 | John, 29 | Emma, 33
|
||||
- Michael, 45 | Lisa, 31 | David, 38 | Jessica, 26 | Ryan, 41
|
||||
|
||||
## Checking Batch Status
|
||||
|
||||
After creating batch jobs, use the CLI to check their status:
|
||||
|
||||
```bash
|
||||
# List all batch jobs for a provider
|
||||
instructor batch list --model "openai/gpt-4o-mini"
|
||||
instructor batch list --model "anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
# Check specific batch status
|
||||
instructor batch status --batch-id "batch_123" --model "openai/gpt-4o-mini"
|
||||
|
||||
# Get results when completed
|
||||
instructor batch results \
|
||||
--batch-id "batch_123" \
|
||||
--output-file "results.jsonl" \
|
||||
--model "openai/gpt-4o-mini"
|
||||
```
|
||||
|
||||
## Processing Times
|
||||
|
||||
- **OpenAI**: Usually completes within a few hours, guaranteed within 24h
|
||||
- **Anthropic**: Most batches complete in under 1 hour
|
||||
- **Google**: Varies (simulation only in this test)
|
||||
|
||||
## Running Tests for All Providers
|
||||
|
||||
```bash
|
||||
# Test all providers (requires API keys)
|
||||
python run_batch_test.py create --model "openai/gpt-4o-mini"
|
||||
python run_batch_test.py create --model "anthropic/claude-3-5-sonnet-20241022"
|
||||
python run_batch_test.py create --model "google/gemini-2.0-flash-001"
|
||||
|
||||
# Check what was created
|
||||
ls *_batch_id.txt
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **API Key Not Set**
|
||||
```
|
||||
❌ Error: OPENAI_API_KEY environment variable is not set
|
||||
```
|
||||
Solution: Set the appropriate environment variable.
|
||||
|
||||
2. **Invalid Model Format**
|
||||
```
|
||||
❌ Error: Model must be in format 'provider/model-name'
|
||||
```
|
||||
Solution: Use the format `provider/model-name`, e.g., `openai/gpt-4o-mini`.
|
||||
|
||||
3. **Unsupported Provider**
|
||||
```
|
||||
❌ Unsupported provider: xyz
|
||||
```
|
||||
Solution: Use `openai`, `anthropic`, or `google` as the provider.
|
||||
|
||||
### Provider-Specific Notes
|
||||
|
||||
**OpenAI:**
|
||||
- Requires valid API key with sufficient credits
|
||||
- Supports both individual and organization accounts
|
||||
- Rate limits are separate for batch vs regular API
|
||||
|
||||
**Anthropic:**
|
||||
- Uses beta API endpoints (`client.beta.messages.batches`)
|
||||
- Requires Anthropic API access
|
||||
- May have different availability by region
|
||||
|
||||
**Google:**
|
||||
- Runs in simulation mode by default
|
||||
- Full implementation requires Google Cloud Storage setup
|
||||
- Would need proper GCS authentication for real batch jobs
|
||||
|
||||
## Integration with CLI
|
||||
|
||||
This test validates that the unified BatchProcessor works correctly, which powers the CLI commands:
|
||||
|
||||
```bash
|
||||
# Create batch using CLI directly
|
||||
instructor batch create \
|
||||
--messages-file messages.jsonl \
|
||||
--model "openai/gpt-4o-mini" \
|
||||
--response-model "examples.User" \
|
||||
--output-file batch_requests.jsonl
|
||||
|
||||
# Submit the batch
|
||||
instructor batch create-from-file \
|
||||
--file-path batch_requests.jsonl \
|
||||
--model "openai/gpt-4o-mini"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To modify the test:
|
||||
1. Update `create_test_messages()` to change test data
|
||||
2. Modify the `User` model if needed
|
||||
3. Add new providers in the provider detection logic
|
||||
4. Adjust batch creation functions for new provider-specific behavior
|
||||
|
||||
The test demonstrates that the same code works across all providers thanks to the unified BatchProcessor abstraction!
|
||||
@@ -1,244 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Example of using in-memory batching for serverless deployments.
|
||||
|
||||
This example shows how to create and submit batch requests without writing to disk
|
||||
"""
|
||||
|
||||
import time
|
||||
from pydantic import BaseModel
|
||||
from instructor.batch.processor import BatchProcessor
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
"""User model for extraction."""
|
||||
|
||||
name: str
|
||||
age: int
|
||||
email: str
|
||||
|
||||
|
||||
def main():
|
||||
"""Demonstrate in-memory batch processing."""
|
||||
print("In-Memory Batch Processing Example")
|
||||
print("===================================\n")
|
||||
|
||||
# Initialize batch processor
|
||||
# Note: Use gpt-4o-mini for JSON schema support in batch API
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
|
||||
# Sample messages for batch processing
|
||||
messages_list = [
|
||||
[
|
||||
{"role": "system", "content": "Extract user information from the text."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "John Doe is 25 years old and his email is john@example.com",
|
||||
},
|
||||
],
|
||||
[
|
||||
{"role": "system", "content": "Extract user information from the text."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Jane Smith, age 30, can be reached at jane.smith@company.com",
|
||||
},
|
||||
],
|
||||
[
|
||||
{"role": "system", "content": "Extract user information from the text."},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Bob Wilson (bob.wilson@email.com) is 28 years old",
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
print("Creating batch requests in memory...")
|
||||
|
||||
# Create batch in memory (no file_path specified)
|
||||
batch_buffer = processor.create_batch_from_messages(
|
||||
messages_list,
|
||||
file_path=None, # This triggers in-memory mode
|
||||
max_tokens=150,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
print(f"Created batch buffer: {type(batch_buffer)}")
|
||||
print(f"Buffer size: {len(batch_buffer.getvalue())} bytes\n")
|
||||
|
||||
# Show the content of the buffer (first 200 chars)
|
||||
batch_buffer.seek(0)
|
||||
content_preview = batch_buffer.read(200).decode("utf-8")
|
||||
print("Buffer content preview:")
|
||||
print(f"{content_preview}...\n")
|
||||
|
||||
# Reset buffer position for submission
|
||||
batch_buffer.seek(0)
|
||||
|
||||
print("Submitting batch job...")
|
||||
|
||||
try:
|
||||
# Submit the batch using the in-memory buffer
|
||||
batch_id = processor.submit_batch(
|
||||
batch_buffer, metadata={"description": "In-memory batch example"}
|
||||
)
|
||||
|
||||
print(f"Batch submitted successfully!")
|
||||
print(f"Batch ID: {batch_id}")
|
||||
|
||||
# Poll for completion
|
||||
print("\nWaiting for batch to complete...")
|
||||
max_wait_time = 300 # 5 minutes max
|
||||
start_time = time.time()
|
||||
status = {}
|
||||
|
||||
while time.time() - start_time < max_wait_time:
|
||||
status = processor.get_batch_status(batch_id)
|
||||
current_status = status.get("status", "unknown")
|
||||
|
||||
# Update status on the same line
|
||||
print(f"\rCurrent status: {current_status.ljust(20)}", end="")
|
||||
|
||||
if current_status in ["completed", "failed", "cancelled", "expired"]:
|
||||
break
|
||||
|
||||
time.sleep(10)
|
||||
|
||||
print() # Newline after polling is done
|
||||
|
||||
# Use the last fetched status
|
||||
final_status = status
|
||||
print(f"\nFinal status: {final_status.get('status', 'unknown')}")
|
||||
|
||||
if final_status.get("status") == "completed":
|
||||
print("\nBatch completed! Retrieving results...")
|
||||
|
||||
# Retrieve and process results
|
||||
results = processor.get_results(batch_id)
|
||||
|
||||
print(f"\nResults Summary:")
|
||||
print(f" Total results: {len(results)}")
|
||||
|
||||
successful_results = [r for r in results if hasattr(r, "result")]
|
||||
error_results = [r for r in results if hasattr(r, "error_message")]
|
||||
|
||||
print(f" Successful: {len(successful_results)}")
|
||||
print(f" Errors: {len(error_results)}")
|
||||
|
||||
# Show successful extractions
|
||||
if successful_results:
|
||||
print("\nExtracted Users:")
|
||||
for result in successful_results:
|
||||
user = result.result
|
||||
print(f" - {user.name}, {user.age} years old, {user.email}")
|
||||
|
||||
# Show any errors
|
||||
if error_results:
|
||||
print("\nErrors encountered:")
|
||||
for error in error_results:
|
||||
print(f" - {error.custom_id}: {error.error_message}")
|
||||
|
||||
elif final_status.get("status") == "failed":
|
||||
print("\nBatch failed to complete")
|
||||
print(" Check your API usage and batch format")
|
||||
|
||||
else:
|
||||
print(f"\nBatch did not complete within {max_wait_time} seconds")
|
||||
print(f" Current status: {final_status.get('status', 'unknown')}")
|
||||
print(
|
||||
" You can check status later with processor.get_batch_status(batch_id)"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during batch processing: {e}")
|
||||
print("\nThis is expected if you don't have OpenAI API credentials set up.")
|
||||
print(
|
||||
" The important part is that the in-memory buffer was created successfully!"
|
||||
)
|
||||
|
||||
print("\nIn-memory batch processing demo complete!")
|
||||
print("\nKey benefits of in-memory batching:")
|
||||
print(" - No disk I/O required - perfect for serverless")
|
||||
print(" - Faster processing - no file system overhead")
|
||||
print(" - Better security - no temporary files on disk")
|
||||
print(" - Cleaner code - no file cleanup required")
|
||||
|
||||
|
||||
def compare_file_vs_memory():
|
||||
"""Compare file-based vs in-memory batch creation."""
|
||||
print("\nComparing File-based vs In-Memory Batching")
|
||||
print("===========================================\n")
|
||||
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
|
||||
messages_list = [
|
||||
[{"role": "user", "content": "Extract: John, 25, john@example.com"}],
|
||||
[{"role": "user", "content": "Extract: Jane, 30, jane@example.com"}],
|
||||
]
|
||||
|
||||
# File-based approach (traditional)
|
||||
print("File-based approach:")
|
||||
file_path = processor.create_batch_from_messages(
|
||||
messages_list,
|
||||
file_path="temp_batch.jsonl", # Specify file path
|
||||
)
|
||||
print(f" Created file: {file_path}")
|
||||
|
||||
# Clean up the file
|
||||
import os
|
||||
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
print(" File cleaned up")
|
||||
|
||||
# In-memory approach (new)
|
||||
print("\nIn-memory approach:")
|
||||
buffer = processor.create_batch_from_messages(
|
||||
messages_list,
|
||||
file_path=None, # No file path = in-memory
|
||||
)
|
||||
print(f" Created buffer: {type(buffer).__name__}")
|
||||
print(f" Buffer size: {len(buffer.getvalue())} bytes")
|
||||
print(" No cleanup required!")
|
||||
|
||||
|
||||
def demo_polling_logic():
|
||||
"""Demonstrate how to properly poll for batch completion."""
|
||||
print("\nBatch Polling Best Practices")
|
||||
print("============================\n")
|
||||
|
||||
print("When working with real batches, follow this pattern:")
|
||||
print("")
|
||||
print("```python")
|
||||
print("import time")
|
||||
print("")
|
||||
print("# Submit your batch")
|
||||
print("batch_id = processor.submit_batch(buffer)")
|
||||
print("")
|
||||
print("# Poll for completion")
|
||||
print("while True:")
|
||||
print(" status = processor.get_batch_status(batch_id)")
|
||||
print(" current_status = status.get('status')")
|
||||
print(" ")
|
||||
print(" if current_status == 'completed':")
|
||||
print(" results = processor.get_results(batch_id)")
|
||||
print(" break")
|
||||
print(" elif current_status in ['failed', 'cancelled', 'expired']:")
|
||||
print(" print(f'Batch failed with status: {current_status}')")
|
||||
print(" break")
|
||||
print(" else:")
|
||||
print(" print(f'Status: {current_status}, waiting...')")
|
||||
print(" time.sleep(10) # Wait 10 seconds before checking again")
|
||||
print("```")
|
||||
print("")
|
||||
print("Typical batch statuses:")
|
||||
print(" - validating - Checking request format")
|
||||
print(" - in_progress - Processing requests")
|
||||
print(" - finalizing - Preparing results")
|
||||
print(" - completed - Ready for download")
|
||||
print(" - failed - Something went wrong")
|
||||
print(" - cancelled - Manually cancelled")
|
||||
print(" - expired - Took too long to process")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
compare_file_vs_memory()
|
||||
@@ -1,851 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Unified Batch API Test Script
|
||||
|
||||
Test script to verify the unified BatchProcessor works correctly with all supported providers.
|
||||
Creates a batch job to extract User(name: str, age: int) data from text examples.
|
||||
|
||||
Supports:
|
||||
- OpenAI: openai/gpt-4o-mini, openai/gpt-4o, etc.
|
||||
- Anthropic: anthropic/claude-3-5-sonnet-20241022, anthropic/claude-3-opus-20240229, etc.
|
||||
- Google: google/gemini-2.5-flash, google/gemini-pro, etc.
|
||||
|
||||
Usage:
|
||||
# Default (Google Gemini 2.5 Flash)
|
||||
export GOOGLE_API_KEY="your-key"
|
||||
python run_batch_test.py
|
||||
|
||||
# OpenAI
|
||||
export OPENAI_API_KEY="your-key"
|
||||
python run_batch_test.py --model "openai/gpt-4o-mini"
|
||||
|
||||
# Anthropic
|
||||
export ANTHROPIC_API_KEY="your-key"
|
||||
python run_batch_test.py --model "anthropic/claude-3-5-sonnet-20241022"
|
||||
|
||||
# Google with specific model
|
||||
export GOOGLE_API_KEY="your-key"
|
||||
python run_batch_test.py --model "google/gemini-2.5-flash"
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
import typer
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
from instructor.batch import (
|
||||
BatchProcessor,
|
||||
BatchStatus,
|
||||
filter_successful,
|
||||
filter_errors,
|
||||
extract_results,
|
||||
)
|
||||
|
||||
app = typer.Typer(help="Unified Batch API Test for all providers")
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
def create_test_messages() -> list[list[dict]]:
|
||||
"""Create test message conversations for user extraction"""
|
||||
test_prompts = [
|
||||
"Hi there! My name is Alice and I'm 28 years old. I work as a software engineer.",
|
||||
]
|
||||
|
||||
messages_list = []
|
||||
for prompt in test_prompts:
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert at extracting structured user information from text. Extract the person's name and age.",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
messages_list.append(messages)
|
||||
|
||||
return messages_list
|
||||
|
||||
|
||||
def get_expected_results() -> list[User]:
|
||||
"""Get the expected User objects for validation"""
|
||||
return [
|
||||
User(name="Alice", age=28),
|
||||
]
|
||||
|
||||
|
||||
def check_api_key(provider: str) -> bool:
|
||||
"""Check if the required API key is set for the provider"""
|
||||
key_map = {
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"google": "GOOGLE_API_KEY",
|
||||
}
|
||||
|
||||
required_key = key_map.get(provider)
|
||||
if not required_key:
|
||||
return True # Unknown provider, let it fail later
|
||||
|
||||
if provider == "google":
|
||||
# Google is optional since we simulate
|
||||
if not os.getenv(required_key):
|
||||
typer.echo(f"Warning: {required_key} not set - will run in simulation mode")
|
||||
return True
|
||||
|
||||
if not os.getenv(required_key):
|
||||
typer.echo(f"Error: {required_key} environment variable is not set", err=True)
|
||||
typer.echo(
|
||||
f"Please set your API key: export {required_key}='your-api-key-here'",
|
||||
err=True,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def create_openai_batch(model: str, messages_list: list[list[dict]]) -> Optional[str]:
|
||||
"""Create OpenAI batch job using BatchProcessor"""
|
||||
processor = BatchProcessor(model, User)
|
||||
|
||||
# Create batch file
|
||||
batch_filename = "test_batch.jsonl"
|
||||
processor.create_batch_from_messages(
|
||||
file_path=batch_filename,
|
||||
messages_list=messages_list,
|
||||
max_tokens=200,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
try:
|
||||
typer.echo("Submitting batch job...")
|
||||
batch_id = processor.submit_batch(
|
||||
file_path=batch_filename,
|
||||
metadata={"description": "Unified BatchProcessor test"},
|
||||
)
|
||||
return batch_id
|
||||
|
||||
finally:
|
||||
if os.path.exists(batch_filename):
|
||||
os.remove(batch_filename)
|
||||
|
||||
|
||||
def create_anthropic_batch(
|
||||
model: str, messages_list: list[list[dict]]
|
||||
) -> Optional[str]:
|
||||
"""Create Anthropic batch job using BatchProcessor"""
|
||||
processor = BatchProcessor(model, User)
|
||||
|
||||
# Create batch file
|
||||
batch_filename = "test_batch.jsonl"
|
||||
processor.create_batch_from_messages(
|
||||
file_path=batch_filename,
|
||||
messages_list=messages_list,
|
||||
max_tokens=200,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
try:
|
||||
typer.echo("Submitting batch job...")
|
||||
batch_id = processor.submit_batch(file_path=batch_filename)
|
||||
return batch_id
|
||||
|
||||
finally:
|
||||
if os.path.exists(batch_filename):
|
||||
os.remove(batch_filename)
|
||||
|
||||
|
||||
def create_google_batch(model: str, messages_list: list[list[dict]]) -> Optional[str]:
|
||||
"""Create Google batch job using BatchProcessor (inline only)"""
|
||||
processor = BatchProcessor(model, User)
|
||||
|
||||
typer.echo("Submitting Google inline batch...")
|
||||
batch_id = processor.submit_batch(
|
||||
messages_list=messages_list,
|
||||
metadata={"description": "Unified BatchProcessor test"},
|
||||
use_inline=True,
|
||||
max_tokens=200,
|
||||
temperature=0.1,
|
||||
)
|
||||
|
||||
typer.echo(f"Inline batch job created: {batch_id}")
|
||||
return batch_id
|
||||
|
||||
|
||||
@app.command()
|
||||
def create(
|
||||
model: str = typer.Option(
|
||||
"openai/gpt-4o-mini",
|
||||
help="Model in format 'provider/model-name' (e.g., 'google/gemini-2.5-flash', 'openai/gpt-4o-mini', 'anthropic/claude-3-5-sonnet-20241022')",
|
||||
),
|
||||
save_id: bool = typer.Option(True, help="Save batch ID to file"),
|
||||
):
|
||||
"""Create a batch job for the specified model"""
|
||||
|
||||
typer.echo(f"Creating Batch Job for {model}")
|
||||
typer.echo("=" * 50)
|
||||
|
||||
# Parse provider from model
|
||||
try:
|
||||
provider, model_name = model.split("/", 1)
|
||||
except ValueError:
|
||||
typer.echo("Error: Model must be in format 'provider/model-name'", err=True)
|
||||
typer.echo(
|
||||
"Examples: 'openai/gpt-4o-mini', 'anthropic/claude-3-5-sonnet-20241022'",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1) from None
|
||||
|
||||
# Check API key
|
||||
if not check_api_key(provider):
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Create test messages
|
||||
messages_list = create_test_messages()
|
||||
typer.echo(f"Created {len(messages_list)} test message conversations")
|
||||
|
||||
try:
|
||||
# Create batch job based on provider
|
||||
batch_id = None
|
||||
|
||||
if provider == "openai":
|
||||
batch_id = create_openai_batch(model, messages_list)
|
||||
elif provider == "anthropic":
|
||||
batch_id = create_anthropic_batch(model, messages_list)
|
||||
else:
|
||||
typer.echo(f"Unsupported provider: {provider}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
if batch_id:
|
||||
typer.echo(f"Batch job created with ID: {batch_id}")
|
||||
|
||||
if save_id:
|
||||
filename = f"{provider}_batch_id.txt"
|
||||
with open(filename, "w") as f:
|
||||
f.write(batch_id)
|
||||
typer.echo(f"Batch ID saved to {filename}")
|
||||
|
||||
# Validate expected results
|
||||
expected_results = get_expected_results()
|
||||
typer.echo(f"Expected results validated: {len(expected_results)} users")
|
||||
for i, user in enumerate(expected_results):
|
||||
typer.echo(f" {i + 1}. {user.name}, age {user.age}")
|
||||
|
||||
# Show how to check status
|
||||
typer.echo(f"Check status with:")
|
||||
typer.echo(f" instructor batch list --model {model}")
|
||||
|
||||
typer.echo(f"Cost savings: 50% vs regular API")
|
||||
typer.echo(f"\nSuccess! Batch ID: {batch_id}")
|
||||
|
||||
else:
|
||||
typer.echo("Failed to create batch job", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error creating batch: {e}", err=True)
|
||||
raise typer.Exit(1) from e
|
||||
|
||||
|
||||
@app.command()
|
||||
def list_batches():
|
||||
"""List saved batch IDs for all providers"""
|
||||
typer.echo("Saved Batch IDs:")
|
||||
typer.echo("=" * 30)
|
||||
|
||||
providers = ["openai", "anthropic"]
|
||||
found_any = False
|
||||
|
||||
for provider in providers:
|
||||
filename = f"{provider}_batch_id.txt"
|
||||
if os.path.exists(filename):
|
||||
with open(filename) as f:
|
||||
batch_id = f.read().strip()
|
||||
|
||||
typer.echo(f"{provider.upper()}: {batch_id}")
|
||||
found_any = True
|
||||
|
||||
if not found_any:
|
||||
typer.echo("No batch IDs found. Run 'create' command first.")
|
||||
typer.echo(
|
||||
"Usage: python run_batch_test.py create --model 'provider/model-name'"
|
||||
)
|
||||
else:
|
||||
typer.echo()
|
||||
typer.echo(
|
||||
"To fetch results: python run_batch_test.py fetch --provider <provider>"
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def fetch(
|
||||
provider: str = typer.Option(
|
||||
help="Provider to fetch results from (openai, anthropic, google)"
|
||||
),
|
||||
validate: bool = typer.Option(
|
||||
True, help="Validate extracted data against expected results"
|
||||
),
|
||||
poll: bool = typer.Option(
|
||||
False, help="Poll every 30 seconds until batch completes"
|
||||
),
|
||||
max_wait: int = typer.Option(
|
||||
600, help="Maximum time to wait in seconds (default: 10 minutes)"
|
||||
),
|
||||
):
|
||||
"""Fetch and validate batch results from a provider"""
|
||||
|
||||
if provider not in ["openai", "anthropic"]:
|
||||
typer.echo("Error: Provider must be one of: openai, anthropic", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Check if batch ID file exists
|
||||
filename = f"{provider}_batch_id.txt"
|
||||
if not os.path.exists(filename):
|
||||
typer.echo(
|
||||
f"Error: No batch ID found for {provider}. Run 'create' command first.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Read batch ID
|
||||
with open(filename) as f:
|
||||
batch_id = f.read().strip()
|
||||
|
||||
typer.echo(f"Fetching results for {provider.upper()} batch: {batch_id}")
|
||||
typer.echo("=" * 60)
|
||||
|
||||
# Check API key
|
||||
if not check_api_key(provider):
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
if poll:
|
||||
results = poll_for_results(provider, batch_id, validate, max_wait)
|
||||
else:
|
||||
if provider == "openai":
|
||||
results = fetch_openai_results(batch_id, validate)
|
||||
elif provider == "anthropic":
|
||||
results = fetch_anthropic_results(batch_id, validate)
|
||||
|
||||
if results:
|
||||
typer.echo(f"Successfully fetched and validated {len(results)} results!")
|
||||
if validate:
|
||||
# Assert that the results match the expected results
|
||||
assert validate_results(results, provider.capitalize()), (
|
||||
f"Test failed: {provider} results do not match expected results."
|
||||
)
|
||||
else:
|
||||
typer.echo("No results available yet or batch still processing")
|
||||
if not poll:
|
||||
typer.echo("Use --poll to automatically wait for completion")
|
||||
|
||||
except AssertionError as ae:
|
||||
typer.echo(f"AssertionError: {ae}", err=True)
|
||||
raise typer.Exit(1) from ae
|
||||
except Exception as e:
|
||||
typer.echo(f"Error fetching results: {e}", err=True)
|
||||
raise typer.Exit(1) from e
|
||||
|
||||
|
||||
@app.command()
|
||||
def show_results(
|
||||
provider: str = typer.Option(
|
||||
help="Provider to show detailed results from (openai, anthropic, google)"
|
||||
),
|
||||
):
|
||||
"""Show detailed parsed Pydantic objects from batch results"""
|
||||
|
||||
if provider not in ["openai", "anthropic"]:
|
||||
typer.echo("Error: Provider must be one of: openai, anthropic", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Check if batch ID file exists
|
||||
filename = f"{provider}_batch_id.txt"
|
||||
if not os.path.exists(filename):
|
||||
typer.echo(
|
||||
f"Error: No batch ID found for {provider}. Run 'create' command first.",
|
||||
err=True,
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Read batch ID
|
||||
with open(filename) as f:
|
||||
batch_id = f.read().strip()
|
||||
|
||||
typer.echo(f"{provider.upper()} BATCH RESULTS")
|
||||
typer.echo("=" * 50)
|
||||
typer.echo(f"Batch ID: {batch_id}")
|
||||
|
||||
# Check API key
|
||||
if not check_api_key(provider):
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
# Get results using BatchProcessor
|
||||
if provider == "openai":
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
elif provider == "anthropic":
|
||||
processor = BatchProcessor("anthropic/claude-3-5-sonnet-20241022", User)
|
||||
|
||||
# Get batch info using list_batches to find our batch
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_id:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
typer.echo(f"Batch {batch_id} not found")
|
||||
return
|
||||
|
||||
typer.echo(f"Status: {batch_info.status.value}")
|
||||
typer.echo(f"Raw Status: {batch_info.raw_status}")
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
typer.echo(f"Batch not completed yet: {batch_info.status.value}")
|
||||
return
|
||||
|
||||
# Get all results using the new get_results method
|
||||
all_results = processor.get_results(batch_id)
|
||||
typer.echo(f"Total results: {len(all_results)}")
|
||||
|
||||
# Show each result with detailed info
|
||||
for i, result in enumerate(all_results):
|
||||
typer.echo(f"\n--- Result {i + 1} ---")
|
||||
typer.echo(f"Custom ID: {result.custom_id}")
|
||||
typer.echo(f"Success: {result.success}")
|
||||
|
||||
if result.success:
|
||||
user = result.result
|
||||
typer.echo(f"PARSED USER OBJECT:")
|
||||
typer.echo(f" Type: {type(user)}")
|
||||
typer.echo(f" Name: {user.name}")
|
||||
typer.echo(f" Age: {user.age}")
|
||||
typer.echo(f" JSON: {user.model_dump_json()}")
|
||||
typer.echo(f" Dict: {user.model_dump()}")
|
||||
|
||||
# Test that it's a real Pydantic object
|
||||
typer.echo(f" Is BaseModel: {isinstance(user, BaseModel)}")
|
||||
typer.echo(f" Is User: {isinstance(user, User)}")
|
||||
|
||||
# Test Pydantic methods
|
||||
try:
|
||||
validated = User.model_validate(user.model_dump())
|
||||
typer.echo(f" Re-validation: Works")
|
||||
typer.echo(f" Re-validated: {validated}")
|
||||
except Exception as e:
|
||||
typer.echo(f" Re-validation: Failed - {e}")
|
||||
else:
|
||||
typer.echo(f"ERROR:")
|
||||
typer.echo(f" Type: {result.error_type}")
|
||||
typer.echo(f" Message: {result.error_message}")
|
||||
|
||||
# Test the utility functions
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_users = extract_results(all_results)
|
||||
|
||||
typer.echo(f"\nUTILITY FUNCTIONS:")
|
||||
typer.echo(f"Successful results: {len(successful_results)}")
|
||||
typer.echo(f"Error results: {len(error_results)}")
|
||||
typer.echo(f"Extracted users: {len(extracted_users)}")
|
||||
|
||||
if extracted_users:
|
||||
typer.echo(f"\nEXTRACTED USER OBJECTS:")
|
||||
for user in extracted_users:
|
||||
typer.echo(
|
||||
f" • {user.name}, age {user.age} (type: {type(user).__name__})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error showing results: {e}", err=True)
|
||||
raise typer.Exit(1) from e
|
||||
|
||||
|
||||
def poll_for_results(
|
||||
provider: str, batch_id: str, validate: bool, max_wait: int
|
||||
) -> list[User]:
|
||||
"""Poll for batch results until completion or timeout"""
|
||||
import time
|
||||
|
||||
typer.echo(f"Polling {provider.upper()} batch every 30 seconds...")
|
||||
typer.echo(f"Max wait time: {max_wait} seconds ({max_wait // 60} minutes)")
|
||||
typer.echo(f"Batch ID: {batch_id}")
|
||||
typer.echo()
|
||||
|
||||
start_time = time.time()
|
||||
attempt = 1
|
||||
|
||||
while time.time() - start_time < max_wait:
|
||||
typer.echo(f"Attempt {attempt} - Checking batch status...")
|
||||
|
||||
try:
|
||||
if provider == "openai":
|
||||
status, results = fetch_openai_results_with_status(batch_id, validate)
|
||||
elif provider == "anthropic":
|
||||
status, results = fetch_anthropic_results_with_status(
|
||||
batch_id, validate
|
||||
)
|
||||
|
||||
if status == "completed" or status == "ended":
|
||||
typer.echo(
|
||||
f"Batch completed after {int(time.time() - start_time)} seconds!"
|
||||
)
|
||||
return results
|
||||
elif status in ["failed", "expired", "cancelled"]:
|
||||
typer.echo(f"Batch {status}")
|
||||
return []
|
||||
else:
|
||||
elapsed = int(time.time() - start_time)
|
||||
remaining = max_wait - elapsed
|
||||
typer.echo(
|
||||
f"Status: {status} | Elapsed: {elapsed}s | Remaining: {remaining}s"
|
||||
)
|
||||
|
||||
if remaining > 30:
|
||||
typer.echo("Waiting 30 seconds before next check...")
|
||||
time.sleep(30)
|
||||
else:
|
||||
typer.echo(f"Waiting {remaining} seconds...")
|
||||
time.sleep(remaining)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error during polling: {e}")
|
||||
time.sleep(30)
|
||||
|
||||
attempt += 1
|
||||
|
||||
typer.echo(f"Timeout reached after {max_wait} seconds")
|
||||
return []
|
||||
|
||||
|
||||
def fetch_openai_results_with_status(
|
||||
batch_id: str, validate: bool
|
||||
) -> tuple[str, list[User]]:
|
||||
"""Fetch OpenAI batch results and return status"""
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
|
||||
# Get batch info
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_id:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
return "not_found", []
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
return batch_info.raw_status, []
|
||||
|
||||
# Get results using the new get_results method
|
||||
all_results = processor.get_results(batch_id)
|
||||
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_results = extract_results(all_results)
|
||||
|
||||
typer.echo(f"Successful extractions: {len(successful_results)}")
|
||||
if error_results:
|
||||
typer.echo(f"Failed extractions: {len(error_results)}")
|
||||
# Show first few errors for debugging
|
||||
for error in error_results[:3]:
|
||||
typer.echo(f" Error ({error.custom_id}): {error.error_message}")
|
||||
|
||||
if validate and extracted_results:
|
||||
validate_results(extracted_results, "OpenAI")
|
||||
|
||||
return "completed", extracted_results
|
||||
|
||||
|
||||
def fetch_anthropic_results_with_status(
|
||||
batch_id: str, validate: bool
|
||||
) -> tuple[str, list[User]]:
|
||||
"""Fetch Anthropic batch results and return status"""
|
||||
processor = BatchProcessor("anthropic/claude-3-5-sonnet-20241022", User)
|
||||
|
||||
# Get batch info
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_id:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
return "not_found", []
|
||||
|
||||
# Check for various terminal states
|
||||
if batch_info.status in [
|
||||
BatchStatus.FAILED,
|
||||
BatchStatus.CANCELLED,
|
||||
BatchStatus.EXPIRED,
|
||||
]:
|
||||
return batch_info.raw_status, []
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
return batch_info.raw_status, []
|
||||
|
||||
# Get results using the new get_results method
|
||||
all_results = processor.get_results(batch_id)
|
||||
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_results = extract_results(all_results)
|
||||
|
||||
typer.echo(f"Successful extractions: {len(successful_results)}")
|
||||
if error_results:
|
||||
typer.echo(f"Failed extractions: {len(error_results)}")
|
||||
# Show first few errors for debugging
|
||||
for error in error_results[:3]:
|
||||
typer.echo(f" Error ({error.custom_id}): {error.error_message}")
|
||||
|
||||
if validate and extracted_results:
|
||||
validate_results(extracted_results, "Anthropic")
|
||||
|
||||
return "ended", extracted_results
|
||||
|
||||
|
||||
def fetch_openai_results(batch_id: str, validate: bool) -> list[User]:
|
||||
"""Fetch OpenAI batch results using BatchProcessor"""
|
||||
processor = BatchProcessor("openai/gpt-4o-mini", User)
|
||||
|
||||
# Get batch info
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_id:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
typer.echo(f"Batch {batch_id} not found")
|
||||
return []
|
||||
|
||||
typer.echo(f"Batch Status: {batch_info.status.value}")
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
typer.echo(
|
||||
f"Batch is still {batch_info.status.value}. Please wait and try again."
|
||||
)
|
||||
return []
|
||||
|
||||
# Get results using the new get_results method
|
||||
all_results = processor.get_results(batch_id)
|
||||
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_results = extract_results(all_results)
|
||||
|
||||
typer.echo(f"Successful extractions: {len(successful_results)}")
|
||||
if error_results:
|
||||
typer.echo(f"Failed extractions: {len(error_results)}")
|
||||
# Show first few errors for debugging
|
||||
for error in error_results[:3]:
|
||||
typer.echo(f" Error ({error.custom_id}): {error.error_message}")
|
||||
|
||||
if validate and extracted_results:
|
||||
validate_results(extracted_results, "OpenAI")
|
||||
|
||||
return extracted_results
|
||||
|
||||
|
||||
def fetch_anthropic_results(batch_id: str, validate: bool) -> list[User]:
|
||||
"""Fetch Anthropic batch results using BatchProcessor"""
|
||||
processor = BatchProcessor("anthropic/claude-3-5-sonnet-20241022", User)
|
||||
|
||||
# Get batch info
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_id:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
typer.echo(f"Batch {batch_id} not found")
|
||||
return []
|
||||
|
||||
typer.echo(f"Batch Status: {batch_info.status.value}")
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
typer.echo(
|
||||
f"Batch is still {batch_info.status.value}. Please wait and try again."
|
||||
)
|
||||
return []
|
||||
|
||||
# Get results using the new get_results method
|
||||
all_results = processor.get_results(batch_id)
|
||||
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_results = extract_results(all_results)
|
||||
|
||||
typer.echo(f"Successful extractions: {len(successful_results)}")
|
||||
if error_results:
|
||||
typer.echo(f"Failed extractions: {len(error_results)}")
|
||||
# Show first few errors for debugging
|
||||
for error in error_results[:3]:
|
||||
typer.echo(f" Error ({error.custom_id}): {error.error_message}")
|
||||
|
||||
if validate and extracted_results:
|
||||
validate_results(extracted_results, "Anthropic")
|
||||
|
||||
return extracted_results
|
||||
|
||||
|
||||
def fetch_google_results(batch_job_name: str, validate: bool) -> list[User]:
|
||||
"""Fetch Google batch results using BatchProcessor"""
|
||||
try:
|
||||
processor = BatchProcessor("google/gemini-2.5-flash", User)
|
||||
|
||||
# Get batch info
|
||||
all_batches = processor.list_batches(limit=100)
|
||||
batch_info = None
|
||||
for batch in all_batches:
|
||||
if batch.id == batch_job_name:
|
||||
batch_info = batch
|
||||
break
|
||||
|
||||
if not batch_info:
|
||||
typer.echo(f"Batch {batch_job_name} not found")
|
||||
return []
|
||||
|
||||
typer.echo(f"Batch Status: {batch_info.status.value}")
|
||||
|
||||
if batch_info.status != BatchStatus.COMPLETED:
|
||||
typer.echo(
|
||||
f"Batch is still {batch_info.status.value}. Please wait and try again."
|
||||
)
|
||||
return []
|
||||
|
||||
# Get results using the new get_results method
|
||||
all_results = processor.get_results(batch_job_name)
|
||||
|
||||
successful_results = filter_successful(all_results)
|
||||
error_results = filter_errors(all_results)
|
||||
extracted_results = extract_results(all_results)
|
||||
|
||||
typer.echo(f"Successful extractions: {len(successful_results)}")
|
||||
if error_results:
|
||||
typer.echo(f"Failed extractions: {len(error_results)}")
|
||||
|
||||
if validate and extracted_results:
|
||||
validate_results(extracted_results, "Google GenAI")
|
||||
|
||||
return extracted_results
|
||||
|
||||
except Exception as e:
|
||||
typer.echo(f"Error fetching Google batch results: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def validate_results(results: list[User], provider_name: str) -> bool:
|
||||
"""Validate extracted results against expected results"""
|
||||
expected_results = get_expected_results()
|
||||
|
||||
typer.echo(f"\nValidating {provider_name} Results:")
|
||||
typer.echo("-" * 40)
|
||||
|
||||
if len(results) != len(expected_results):
|
||||
typer.echo(f"Expected {len(expected_results)} results, got {len(results)}")
|
||||
return False
|
||||
|
||||
# Sort both lists by name for comparison
|
||||
results_sorted = sorted(results, key=lambda x: x.name)
|
||||
expected_sorted = sorted(expected_results, key=lambda x: x.name)
|
||||
|
||||
all_correct = True
|
||||
for i, (actual, expected) in enumerate(zip(results_sorted, expected_sorted)):
|
||||
if actual.name == expected.name and actual.age == expected.age:
|
||||
typer.echo(f"{i + 1}. {actual.name}, age {actual.age} - CORRECT")
|
||||
else:
|
||||
typer.echo(f"{i + 1}. Expected: {expected.name}, age {expected.age}")
|
||||
typer.echo(f" Got: {actual.name}, age {actual.age}")
|
||||
all_correct = False
|
||||
|
||||
if all_correct:
|
||||
typer.echo(f"\nAll {provider_name} extractions are correct!")
|
||||
else:
|
||||
typer.echo(f"\nSome {provider_name} extractions have errors")
|
||||
|
||||
return all_correct
|
||||
|
||||
|
||||
@app.command()
|
||||
def help():
|
||||
"""Show all available commands and usage examples"""
|
||||
typer.echo("Unified Batch API Test Commands")
|
||||
typer.echo("=" * 40)
|
||||
typer.echo()
|
||||
|
||||
typer.echo("Available Commands:")
|
||||
typer.echo(" • create - Create a new batch job")
|
||||
typer.echo(" • list-batches - List all saved batch IDs")
|
||||
typer.echo(" • fetch - Fetch and validate batch results")
|
||||
typer.echo(" • show-results - Show detailed parsed Pydantic objects")
|
||||
typer.echo(" • list-models - Show supported models")
|
||||
typer.echo(" • help - Show this help message")
|
||||
typer.echo()
|
||||
|
||||
typer.echo("Usage Examples:")
|
||||
typer.echo(" # Create batch job (default: Google Gemini 2.5 Flash)")
|
||||
typer.echo(" python run_batch_test.py create")
|
||||
typer.echo()
|
||||
typer.echo(" # Create batch job with specific model")
|
||||
typer.echo(" python run_batch_test.py create --model 'openai/gpt-4o-mini'")
|
||||
typer.echo()
|
||||
typer.echo(" # List saved batch IDs")
|
||||
typer.echo(" python run_batch_test.py list-batches")
|
||||
typer.echo()
|
||||
typer.echo(" # Fetch results with validation")
|
||||
typer.echo(" python run_batch_test.py fetch --provider openai")
|
||||
typer.echo()
|
||||
typer.echo(" # Show detailed parsed objects")
|
||||
typer.echo(" python run_batch_test.py show-results --provider anthropic")
|
||||
typer.echo()
|
||||
typer.echo(" # Poll every 30 seconds until batch completes (max 10 minutes)")
|
||||
typer.echo(" python run_batch_test.py fetch --provider openai --poll")
|
||||
typer.echo()
|
||||
typer.echo(" # Poll with custom timeout (20 minutes)")
|
||||
typer.echo(
|
||||
" python run_batch_test.py fetch --provider openai --poll --max-wait 1200"
|
||||
)
|
||||
typer.echo()
|
||||
|
||||
|
||||
@app.command()
|
||||
def list_models():
|
||||
"""List example models for each provider"""
|
||||
typer.echo("Supported Models by Provider:")
|
||||
typer.echo()
|
||||
|
||||
typer.echo("OpenAI:")
|
||||
typer.echo(" • openai/gpt-4o-mini")
|
||||
typer.echo(" • openai/gpt-4o")
|
||||
typer.echo(" • openai/gpt-4-turbo")
|
||||
typer.echo()
|
||||
|
||||
typer.echo("Anthropic:")
|
||||
typer.echo(" • anthropic/claude-3-5-sonnet-20241022")
|
||||
typer.echo(" • anthropic/claude-3-opus-20240229")
|
||||
typer.echo(" • anthropic/claude-3-haiku-20240307")
|
||||
typer.echo()
|
||||
|
||||
typer.echo("Google:")
|
||||
typer.echo(" • google/gemini-2.5-flash")
|
||||
typer.echo(" • google/gemini-2.0-flash-001")
|
||||
typer.echo(" • google/gemini-pro")
|
||||
typer.echo()
|
||||
|
||||
typer.echo("Usage: python run_batch_test.py create --model 'provider/model-name'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
@@ -1,125 +0,0 @@
|
||||
import functools
|
||||
import inspect
|
||||
import instructor
|
||||
import diskcache
|
||||
|
||||
from openai import OpenAI, AsyncOpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
aclient = instructor.from_openai(AsyncOpenAI())
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
cache = diskcache.Cache("./my_cache_directory")
|
||||
|
||||
|
||||
def instructor_cache(func):
|
||||
"""Cache a function that returns a Pydantic model"""
|
||||
return_type = inspect.signature(func).return_annotation
|
||||
if not issubclass(return_type, BaseModel):
|
||||
raise ValueError("The return type must be a Pydantic model")
|
||||
|
||||
is_async = inspect.iscoroutinefunction(func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = f"{func.__name__}-{functools._make_key(args, kwargs, typed=False)}"
|
||||
# Check if the result is already cached
|
||||
if (cached := cache.get(key)) is not None:
|
||||
# Deserialize from JSON based on the return type
|
||||
if issubclass(return_type, BaseModel):
|
||||
return return_type.model_validate_json(cached)
|
||||
|
||||
# Call the function and cache its result
|
||||
result = func(*args, **kwargs)
|
||||
serialized_result = result.model_dump_json()
|
||||
cache.set(key, serialized_result)
|
||||
|
||||
return result
|
||||
|
||||
@functools.wraps(func)
|
||||
async def awrapper(*args, **kwargs):
|
||||
key = f"{func.__name__}-{functools._make_key(args, kwargs, typed=False)}"
|
||||
# Check if the result is already cached
|
||||
if (cached := cache.get(key)) is not None:
|
||||
# Deserialize from JSON based on the return type
|
||||
if issubclass(return_type, BaseModel):
|
||||
return return_type.model_validate_json(cached)
|
||||
|
||||
# Call the function and cache its result
|
||||
result = await func(*args, **kwargs)
|
||||
serialized_result = result.model_dump_json()
|
||||
cache.set(key, serialized_result)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper if not is_async else awrapper
|
||||
|
||||
|
||||
@instructor_cache
|
||||
def extract(data) -> UserDetail:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@instructor_cache
|
||||
async def aextract(data) -> UserDetail:
|
||||
return await aclient.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
def test_extract():
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
|
||||
async def atest_extract():
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
model = await aextract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
start = time.perf_counter()
|
||||
model = await aextract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_extract()
|
||||
# Time taken: 0.7285366660216823
|
||||
# Time taken: 9.841693099588156e-05
|
||||
|
||||
import asyncio
|
||||
|
||||
asyncio.run(atest_extract())
|
||||
@@ -1,74 +0,0 @@
|
||||
import redis
|
||||
import functools
|
||||
import inspect
|
||||
import instructor
|
||||
|
||||
from pydantic import BaseModel
|
||||
from openai import OpenAI
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
cache = redis.Redis("localhost")
|
||||
|
||||
|
||||
def instructor_cache(func):
|
||||
"""Cache a function that returns a Pydantic model"""
|
||||
return_type = inspect.signature(func).return_annotation
|
||||
if not issubclass(return_type, BaseModel):
|
||||
raise ValueError("The return type must be a Pydantic model")
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = f"{func.__name__}-{functools._make_key(args, kwargs, typed=False)}"
|
||||
# Check if the result is already cached
|
||||
if (cached := cache.get(key)) is not None:
|
||||
# Deserialize from JSON based on the return type
|
||||
if issubclass(return_type, BaseModel):
|
||||
return return_type.model_validate_json(cached)
|
||||
|
||||
# Call the function and cache its result
|
||||
result = func(*args, **kwargs)
|
||||
serialized_result = result.model_dump_json()
|
||||
cache.set(key, serialized_result)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
@instructor_cache
|
||||
def extract(data) -> UserDetail:
|
||||
# Assuming client.chat.completions.create returns a UserDetail instance
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_extract():
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_extract()
|
||||
# Time taken: 0.798335583996959
|
||||
# Time taken: 0.00017016706988215446
|
||||
@@ -1,44 +0,0 @@
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel
|
||||
import functools
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
@functools.lru_cache
|
||||
def extract(data):
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_extract():
|
||||
import time
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
start = time.perf_counter()
|
||||
model = extract("Extract jason is 25 years old")
|
||||
assert model.name.lower() == "jason"
|
||||
assert model.age == 25
|
||||
print(f"Time taken: {time.perf_counter() - start}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_extract()
|
||||
# Time taken: 0.9267581660533324
|
||||
# Time taken: 1.2080417945981026e-06
|
||||
@@ -1,589 +0,0 @@
|
||||
"""
|
||||
Comprehensive Caching Example for Instructor
|
||||
===========================================
|
||||
|
||||
This example demonstrates various caching strategies for LLM applications:
|
||||
1. functools.cache - Simple in-memory caching
|
||||
2. diskcache - Persistent disk-based caching
|
||||
3. Redis - Distributed caching
|
||||
4. Performance benchmarks and cost analysis
|
||||
5. Advanced patterns: hierarchical caching, monitoring, schema invalidation
|
||||
|
||||
Run this example to see real-world performance improvements and cost savings.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import functools
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Any, Callable, Optional, TypeVar
|
||||
|
||||
import instructor
|
||||
from openai import AsyncOpenAI, OpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize clients
|
||||
client = instructor.from_openai(OpenAI())
|
||||
aclient = instructor.from_openai(AsyncOpenAI())
|
||||
|
||||
# Test data
|
||||
TEST_QUERIES = [
|
||||
"Extract: Jason is 25 years old and works as a software engineer",
|
||||
"Extract: Sarah is 30 years old and is a data scientist",
|
||||
"Extract: Mike is 28 years old and works in marketing",
|
||||
"Extract: Lisa is 32 years old and is a product manager",
|
||||
"Extract: Jason is 25 years old and works as a software engineer", # Duplicate for cache hit
|
||||
]
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
"""Enhanced user model with more fields for testing"""
|
||||
|
||||
name: str = Field(description="User's full name")
|
||||
age: int = Field(description="User's age", ge=0, le=150)
|
||||
occupation: Optional[str] = Field(None, description="User's job title")
|
||||
|
||||
|
||||
class CacheMetrics:
|
||||
"""Production-ready cache monitoring"""
|
||||
|
||||
def __init__(self):
|
||||
self.hits = 0
|
||||
self.misses = 0
|
||||
self.total_time_saved = 0.0
|
||||
self.error_count = 0
|
||||
self.hit_rate_by_function: dict[str, dict[str, int]] = defaultdict(
|
||||
lambda: {"hits": 0, "misses": 0}
|
||||
)
|
||||
|
||||
def record_hit(self, func_name: str, time_saved: float):
|
||||
self.hits += 1
|
||||
self.total_time_saved += time_saved
|
||||
self.hit_rate_by_function[func_name]["hits"] += 1
|
||||
logger.debug(f"Cache HIT for {func_name}, saved {time_saved:.3f}s")
|
||||
|
||||
def record_miss(self, func_name: str):
|
||||
self.misses += 1
|
||||
self.hit_rate_by_function[func_name]["misses"] += 1
|
||||
logger.debug(f"Cache MISS for {func_name}")
|
||||
|
||||
def record_error(self, func_name: str, error: str):
|
||||
self.error_count += 1
|
||||
logger.warning(f"Cache ERROR in {func_name}: {error}")
|
||||
|
||||
@property
|
||||
def hit_rate(self) -> float:
|
||||
total = self.hits + self.misses
|
||||
return self.hits / total if total > 0 else 0.0
|
||||
|
||||
def get_stats(self) -> dict[str, Any]:
|
||||
return {
|
||||
"hit_rate": f"{self.hit_rate:.2%}",
|
||||
"total_hits": self.hits,
|
||||
"total_misses": self.misses,
|
||||
"error_count": self.error_count,
|
||||
"time_saved_seconds": f"{self.total_time_saved:.3f}",
|
||||
"function_stats": dict(self.hit_rate_by_function),
|
||||
}
|
||||
|
||||
def reset(self):
|
||||
"""Reset all metrics for new test runs"""
|
||||
self.hits = 0
|
||||
self.misses = 0
|
||||
self.total_time_saved = 0.0
|
||||
self.error_count = 0
|
||||
self.hit_rate_by_function.clear()
|
||||
|
||||
|
||||
# Global metrics instance
|
||||
metrics = CacheMetrics()
|
||||
|
||||
|
||||
def smart_cache_key(
|
||||
func_name: str, args: tuple, kwargs: dict, model_class: type
|
||||
) -> str:
|
||||
"""Generate cache key with schema versioning for automatic invalidation"""
|
||||
# Include model schema in cache key for automatic invalidation
|
||||
schema_hash = hashlib.md5(
|
||||
json.dumps(model_class.model_json_schema(), sort_keys=True).encode()
|
||||
).hexdigest()[:8]
|
||||
|
||||
args_hash = hashlib.md5(str((args, kwargs)).encode()).hexdigest()[:8]
|
||||
|
||||
return f"{func_name}:{schema_hash}:{args_hash}"
|
||||
|
||||
|
||||
# 1. Simple functools.cache implementation
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def extract_functools(data: str) -> UserDetail:
|
||||
"""Simple in-memory caching with functools.lru_cache"""
|
||||
start_time = time.perf_counter()
|
||||
|
||||
result = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
# This won't be called on cache hits, so we track metrics differently
|
||||
return result
|
||||
|
||||
|
||||
def monitored_functools_cache(func: F) -> F:
|
||||
"""functools.cache with monitoring"""
|
||||
cached_func = functools.lru_cache(maxsize=1000)(func)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Check if we'll get a cache hit by calling cache_info
|
||||
info_before = cached_func.cache_info()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
result = cached_func(*args, **kwargs)
|
||||
execution_time = time.perf_counter() - start_time
|
||||
|
||||
info_after = cached_func.cache_info()
|
||||
|
||||
if info_after.hits > info_before.hits:
|
||||
# We got a cache hit
|
||||
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
|
||||
else:
|
||||
# Cache miss
|
||||
metrics.record_miss(func.__name__)
|
||||
|
||||
return result
|
||||
|
||||
# Preserve cache_info method
|
||||
wrapper.cache_info = cached_func.cache_info
|
||||
wrapper.cache_clear = cached_func.cache_clear
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@monitored_functools_cache
|
||||
def extract_functools_monitored(data: str) -> UserDetail:
|
||||
"""functools.cache with monitoring"""
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# 2. Enhanced diskcache implementation
|
||||
def create_diskcache_decorator(
|
||||
cache_dir: str = "./cache_directory", ttl: Optional[int] = None
|
||||
):
|
||||
"""Factory for diskcache decorator with enhanced features"""
|
||||
try:
|
||||
import diskcache
|
||||
|
||||
cache = diskcache.Cache(cache_dir)
|
||||
except ImportError:
|
||||
logger.warning("diskcache not available, skipping disk cache example")
|
||||
return lambda func: func
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
return_type = inspect.signature(func).return_annotation
|
||||
if not (inspect.isclass(return_type) and issubclass(return_type, BaseModel)):
|
||||
raise ValueError("The return type must be a Pydantic model")
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Generate smart cache key with schema versioning
|
||||
key = smart_cache_key(func.__name__, args, kwargs, return_type)
|
||||
|
||||
try:
|
||||
# Check if the result is already cached
|
||||
if (cached := cache.get(key)) is not None:
|
||||
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
|
||||
return return_type.model_validate_json(cached)
|
||||
|
||||
metrics.record_miss(func.__name__)
|
||||
except Exception as e:
|
||||
metrics.record_error(func.__name__, str(e))
|
||||
logger.warning(f"Cache read error: {e}")
|
||||
|
||||
# Call the function and cache its result
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
try:
|
||||
serialized_result = result.model_dump_json()
|
||||
if ttl:
|
||||
cache.set(key, serialized_result, expire=ttl)
|
||||
else:
|
||||
cache.set(key, serialized_result)
|
||||
except Exception as e:
|
||||
metrics.record_error(func.__name__, str(e))
|
||||
logger.warning(f"Cache write error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@create_diskcache_decorator(ttl=3600) # 1 hour TTL
|
||||
def extract_diskcache(data: str) -> UserDetail:
|
||||
"""Persistent disk-based caching with TTL"""
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# 3. Enhanced Redis implementation (with fallback)
|
||||
def create_redis_decorator(
|
||||
redis_url: str = "redis://localhost:6379",
|
||||
ttl: int = 3600,
|
||||
prefix: str = "instructor",
|
||||
):
|
||||
"""Factory for Redis decorator with production features"""
|
||||
try:
|
||||
import redis
|
||||
|
||||
cache = redis.from_url(redis_url, decode_responses=True)
|
||||
# Test connection
|
||||
cache.ping()
|
||||
logger.info("Connected to Redis successfully")
|
||||
except ImportError as e:
|
||||
logger.warning(f"Redis not available (ImportError: {e}), using fallback")
|
||||
return lambda func: func
|
||||
except Exception as e: # Covers redis.RedisError and other connection issues
|
||||
logger.warning(f"Redis not available ({e}), using fallback")
|
||||
return lambda func: func
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
return_type = inspect.signature(func).return_annotation
|
||||
if not (inspect.isclass(return_type) and issubclass(return_type, BaseModel)):
|
||||
raise ValueError("The return type must be a Pydantic model")
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
# Generate cache key with schema versioning
|
||||
schema_hash = hashlib.md5(
|
||||
json.dumps(return_type.model_json_schema(), sort_keys=True).encode()
|
||||
).hexdigest()[:8]
|
||||
key = f"{prefix}:{func.__name__}:{schema_hash}:{functools._make_key(args, kwargs, typed=False)}"
|
||||
|
||||
try:
|
||||
# Check if the result is already cached
|
||||
if (cached := cache.get(key)) is not None:
|
||||
metrics.record_hit(func.__name__, 0.8) # Assume 800ms saved
|
||||
logger.debug(f"Cache hit for key: {key}")
|
||||
return return_type.model_validate_json(cached)
|
||||
|
||||
metrics.record_miss(func.__name__)
|
||||
logger.debug(f"Cache miss for key: {key}")
|
||||
except redis.RedisError as e:
|
||||
metrics.record_error(func.__name__, str(e))
|
||||
logger.warning(f"Redis read error: {e}")
|
||||
|
||||
# Call the function and cache its result
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
try:
|
||||
serialized_result = result.model_dump_json()
|
||||
cache.setex(key, ttl, serialized_result)
|
||||
logger.debug(f"Cached result for key: {key}")
|
||||
except redis.RedisError as e:
|
||||
metrics.record_error(func.__name__, str(e))
|
||||
logger.warning(f"Redis write error: {e}")
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@create_redis_decorator(ttl=3600)
|
||||
def extract_redis(data: str) -> UserDetail:
|
||||
"""Distributed Redis caching with error handling"""
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# 4. No cache baseline for comparison
|
||||
def extract_no_cache(data: str) -> UserDetail:
|
||||
"""Baseline function without caching"""
|
||||
metrics.record_miss("extract_no_cache")
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# 5. Hierarchical caching example
|
||||
@functools.lru_cache(maxsize=50) # L1: Fast in-memory
|
||||
def extract_l1(data: str) -> UserDetail:
|
||||
return extract_l2(data)
|
||||
|
||||
|
||||
@create_diskcache_decorator() # L2: Persistent disk
|
||||
def extract_l2(data: str) -> UserDetail:
|
||||
return extract_l3(data)
|
||||
|
||||
|
||||
@create_redis_decorator() # L3: Shared distributed
|
||||
def extract_l3(data: str) -> UserDetail:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def benchmark_caching_strategy(
|
||||
func: Callable, name: str, queries: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Benchmark a specific caching strategy"""
|
||||
logger.info(f"\n=== Benchmarking {name} ===")
|
||||
|
||||
# Reset metrics for this test
|
||||
metrics.reset()
|
||||
|
||||
times = []
|
||||
results = []
|
||||
|
||||
for i, query in enumerate(queries):
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
result = func(query)
|
||||
execution_time = time.perf_counter() - start_time
|
||||
times.append(execution_time)
|
||||
results.append(result)
|
||||
logger.info(
|
||||
f"Query {i + 1}: {execution_time:.3f}s - {result.name}, {result.age}, {result.occupation}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in {name}: {e}")
|
||||
times.append(float("inf"))
|
||||
results.append(None)
|
||||
|
||||
# Calculate statistics
|
||||
valid_times = [t for t in times if t != float("inf")]
|
||||
if valid_times:
|
||||
avg_time = sum(valid_times) / len(valid_times)
|
||||
total_time = sum(valid_times)
|
||||
fastest_time = min(valid_times)
|
||||
slowest_time = max(valid_times)
|
||||
else:
|
||||
avg_time = total_time = fastest_time = slowest_time = 0
|
||||
|
||||
stats = {
|
||||
"name": name,
|
||||
"total_time": total_time,
|
||||
"avg_time": avg_time,
|
||||
"fastest_time": fastest_time,
|
||||
"slowest_time": slowest_time,
|
||||
"cache_metrics": metrics.get_stats(),
|
||||
"success_rate": len(valid_times) / len(queries),
|
||||
}
|
||||
|
||||
logger.info(f"Total time: {total_time:.3f}s")
|
||||
logger.info(f"Average time: {avg_time:.3f}s")
|
||||
logger.info(f"Cache hit rate: {metrics.hit_rate:.2%}")
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def calculate_cost_savings(baseline_stats: dict, cached_stats: dict) -> dict[str, Any]:
|
||||
"""Calculate cost savings from caching"""
|
||||
baseline_time = baseline_stats["total_time"]
|
||||
cached_time = cached_stats["total_time"]
|
||||
|
||||
# Assume $0.002 per API call (rough average)
|
||||
cost_per_call = 0.002
|
||||
num_queries = len(TEST_QUERIES)
|
||||
|
||||
# Without caching: every call costs money
|
||||
cost_without_cache = num_queries * cost_per_call
|
||||
|
||||
# With caching: only cache misses cost money
|
||||
cache_misses = cached_stats["cache_metrics"]["total_misses"]
|
||||
cost_with_cache = cache_misses * cost_per_call
|
||||
|
||||
savings = cost_without_cache - cost_with_cache
|
||||
savings_percent = (
|
||||
(savings / cost_without_cache) * 100 if cost_without_cache > 0 else 0
|
||||
)
|
||||
|
||||
time_saved = baseline_time - cached_time
|
||||
time_savings_percent = (
|
||||
(time_saved / baseline_time) * 100 if baseline_time > 0 else 0
|
||||
)
|
||||
|
||||
return {
|
||||
"cost_without_cache": cost_without_cache,
|
||||
"cost_with_cache": cost_with_cache,
|
||||
"cost_savings": savings,
|
||||
"cost_savings_percent": savings_percent,
|
||||
"time_saved": time_saved,
|
||||
"time_savings_percent": time_savings_percent,
|
||||
"speed_improvement": (
|
||||
baseline_time / cached_time if cached_time > 0 else float("inf")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def run_async_example():
|
||||
"""Demonstrate async caching patterns"""
|
||||
logger.info("\n=== Async Caching Example ===")
|
||||
|
||||
# Simple async function with metrics
|
||||
async def extract_async(data: str) -> UserDetail:
|
||||
metrics.record_miss("extract_async")
|
||||
return await aclient.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
)
|
||||
|
||||
# Run concurrent requests
|
||||
start_time = time.perf_counter()
|
||||
tasks = [
|
||||
extract_async(query) for query in TEST_QUERIES[:3]
|
||||
] # First 3 to save costs
|
||||
results = await asyncio.gather(*tasks)
|
||||
total_time = time.perf_counter() - start_time
|
||||
|
||||
logger.info(f"Async processing time: {total_time:.3f}s")
|
||||
for i, result in enumerate(results):
|
||||
logger.info(f"Result {i + 1}: {result.name}, {result.age}, {result.occupation}")
|
||||
|
||||
|
||||
def demonstrate_schema_invalidation():
|
||||
"""Show how cache keys change when model schema changes"""
|
||||
logger.info("\n=== Schema-Based Cache Invalidation ===")
|
||||
|
||||
# Original model
|
||||
class OriginalUser(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
# Modified model (different schema)
|
||||
class ModifiedUser(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
email: Optional[str] = None # New field
|
||||
|
||||
# Generate cache keys for same function args but different models
|
||||
args = ("test data",)
|
||||
kwargs = {}
|
||||
|
||||
key1 = smart_cache_key("test_func", args, kwargs, OriginalUser)
|
||||
key2 = smart_cache_key("test_func", args, kwargs, ModifiedUser)
|
||||
|
||||
logger.info(f"Original model cache key: {key1}")
|
||||
logger.info(f"Modified model cache key: {key2}")
|
||||
logger.info(f"Keys are different: {key1 != key2}")
|
||||
logger.info("This ensures cache invalidation when model schemas change!")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run comprehensive caching demonstration"""
|
||||
logger.info("🚀 Starting Comprehensive Caching Demonstration")
|
||||
logger.info("=" * 60)
|
||||
|
||||
# Run benchmarks for each strategy
|
||||
strategies = [
|
||||
(extract_no_cache, "No Cache (Baseline)"),
|
||||
(extract_functools_monitored, "functools.lru_cache"),
|
||||
(extract_diskcache, "diskcache"),
|
||||
(extract_redis, "Redis"),
|
||||
(extract_l1, "Hierarchical (L1→L2→L3)"),
|
||||
]
|
||||
|
||||
all_stats = {}
|
||||
|
||||
for func, name in strategies:
|
||||
try:
|
||||
stats = benchmark_caching_strategy(func, name, TEST_QUERIES)
|
||||
all_stats[name] = stats
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to benchmark {name}: {e}")
|
||||
continue
|
||||
|
||||
# Print summary comparison
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("📊 PERFORMANCE COMPARISON SUMMARY")
|
||||
logger.info("=" * 60)
|
||||
|
||||
baseline_stats = all_stats.get("No Cache (Baseline)")
|
||||
|
||||
if baseline_stats:
|
||||
for name, stats in all_stats.items():
|
||||
if name == "No Cache (Baseline)":
|
||||
continue
|
||||
|
||||
logger.info(f"\n{name}:")
|
||||
logger.info(f" Total time: {stats['total_time']:.3f}s")
|
||||
logger.info(f" Cache hit rate: {stats['cache_metrics']['hit_rate']}")
|
||||
|
||||
# Calculate savings
|
||||
savings = calculate_cost_savings(baseline_stats, stats)
|
||||
logger.info(f" Speed improvement: {savings['speed_improvement']:.1f}x")
|
||||
logger.info(
|
||||
f" Time saved: {savings['time_saved']:.3f}s ({savings['time_savings_percent']:.1f}%)"
|
||||
)
|
||||
logger.info(
|
||||
f" Cost savings: ${savings['cost_savings']:.4f} ({savings['cost_savings_percent']:.1f}%)"
|
||||
)
|
||||
|
||||
# Additional demonstrations
|
||||
demonstrate_schema_invalidation()
|
||||
|
||||
# Run async example
|
||||
asyncio.run(run_async_example())
|
||||
|
||||
# Print cache info for functools
|
||||
logger.info(
|
||||
f"\nfunctools.lru_cache info: {extract_functools_monitored.cache_info()}"
|
||||
)
|
||||
|
||||
logger.info("\n" + "=" * 60)
|
||||
logger.info("✅ Caching demonstration completed!")
|
||||
logger.info("💡 Key takeaways:")
|
||||
logger.info(" - Caching can provide 10x-1000x speed improvements")
|
||||
logger.info(" - Choose the right strategy based on your needs:")
|
||||
logger.info(" • functools.cache: Development, single process")
|
||||
logger.info(" • diskcache: Persistence, moderate performance")
|
||||
logger.info(" • Redis: Distributed systems, high performance")
|
||||
logger.info(" • Hierarchical: Best of all worlds")
|
||||
logger.info(" - Smart cache keys prevent stale data")
|
||||
logger.info(" - Monitoring helps optimize cache performance")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,75 +0,0 @@
|
||||
# Instructor Caching Prototype
|
||||
|
||||
This example demonstrates the new built-in caching functionality in Instructor.
|
||||
|
||||
## Files
|
||||
|
||||
- `run.py` - Main example showing all caching features (with mock calls for quick testing)
|
||||
- `run_real.py` - Complete demo with real API calls
|
||||
- `test_simple.py` - Unit tests for cache components without API calls
|
||||
- `test_anthropic.py` - Tests with Anthropic provider to verify caching works across providers
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. AutoCache (In-Memory LRU)
|
||||
```python
|
||||
from instructor.cache import AutoCache
|
||||
|
||||
cache = AutoCache(maxsize=100)
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
```
|
||||
|
||||
### 2. DiskCache (Persistent)
|
||||
```python
|
||||
from instructor.cache import DiskCache
|
||||
|
||||
cache = DiskCache(directory=".instructor_cache")
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
```
|
||||
|
||||
### 3. Cache TTL (Time-to-Live)
|
||||
```python
|
||||
client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
cache_ttl=3600, # 1 hour
|
||||
)
|
||||
```
|
||||
|
||||
### 4. create_with_completion Support
|
||||
Both the parsed model and raw completion objects are cached and restored.
|
||||
|
||||
## Performance Results
|
||||
|
||||
From our tests:
|
||||
- **156x faster** cache hits vs API calls
|
||||
- **Identical results** from cache and API
|
||||
- **Persistent storage** across client instances
|
||||
- **Automatic cache invalidation** based on:
|
||||
- Different prompts
|
||||
- Different models
|
||||
- Different response schemas
|
||||
- TTL expiration
|
||||
|
||||
## Running the Examples
|
||||
|
||||
```bash
|
||||
# Run the complete demo (requires OpenAI API key)
|
||||
uv run python run_real.py
|
||||
|
||||
# Run unit tests (no API required)
|
||||
uv run python test_simple.py
|
||||
|
||||
# Run pytest tests
|
||||
uv run pytest tests/test_cache*.py
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
1. **Deterministic caching** - same inputs always produce same cache key
|
||||
2. **Schema-aware** - changing field descriptions invalidates cache
|
||||
3. **Multiple backends** - AutoCache (LRU), DiskCache (persistent)
|
||||
4. **TTL support** - automatic expiration (where supported)
|
||||
5. **Raw response preservation** - `create_with_completion` works seamlessly
|
||||
6. **Thread-safe** - all cache implementations are thread-safe
|
||||
@@ -1,270 +0,0 @@
|
||||
"""Demonstrate real caching functionality with actual API calls."""
|
||||
|
||||
import time
|
||||
import instructor
|
||||
from instructor.cache import AutoCache, DiskCache
|
||||
from pydantic import BaseModel, Field
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str = Field(description="The user's name")
|
||||
age: int = Field(description="The user's age")
|
||||
|
||||
|
||||
def test_autocache():
|
||||
"""Test basic in-memory caching."""
|
||||
print("\n=== Testing AutoCache (in-memory) ===")
|
||||
|
||||
cache = AutoCache(maxsize=100)
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Generate a user named Alice who is 25 years old"}
|
||||
]
|
||||
|
||||
# First call - hits API
|
||||
print("First call (hits API)...")
|
||||
start = time.time()
|
||||
user1 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
api_time = time.time() - start
|
||||
print(f"Result: {user1}")
|
||||
print(f"Time: {api_time:.2f}s")
|
||||
|
||||
# Second call - from cache
|
||||
print("\nSecond call (from cache)...")
|
||||
start = time.time()
|
||||
user2 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
cache_time = time.time() - start
|
||||
print(f"Result: {user2}")
|
||||
print(f"Time: {cache_time:.4f}s")
|
||||
print(f"Speedup: {api_time / cache_time:.0f}x faster")
|
||||
|
||||
assert user1.name == user2.name
|
||||
assert user1.age == user2.age
|
||||
print("✓ Cache working - identical results")
|
||||
|
||||
|
||||
def test_create_with_completion():
|
||||
"""Test create_with_completion caching."""
|
||||
print("\n=== Testing create_with_completion ===")
|
||||
|
||||
cache = AutoCache(maxsize=100)
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "What's the weather? Say it's 22C and sunny."}
|
||||
]
|
||||
|
||||
class Weather(BaseModel):
|
||||
temperature: float
|
||||
condition: str
|
||||
|
||||
# First call
|
||||
print("First call with completion...")
|
||||
weather1, completion1 = client.create_with_completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=Weather,
|
||||
)
|
||||
print(f"Weather: {weather1}")
|
||||
print(f"Completion ID: {completion1.id}")
|
||||
print(f"Tokens used: {completion1.usage.total_tokens}")
|
||||
|
||||
# Second call - cached
|
||||
print("\nSecond call (cached)...")
|
||||
start = time.time()
|
||||
weather2, completion2 = client.create_with_completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=Weather,
|
||||
)
|
||||
cache_time = time.time() - start
|
||||
print(f"Weather: {weather2}")
|
||||
print(f"Completion ID: {completion2.id}")
|
||||
print(f"Cache time: {cache_time:.4f}s")
|
||||
|
||||
assert weather1.temperature == weather2.temperature
|
||||
assert completion1.id == completion2.id
|
||||
print("✓ Completion object cached correctly")
|
||||
|
||||
|
||||
def test_diskcache():
|
||||
"""Test persistent disk caching."""
|
||||
print("\n=== Testing DiskCache (persistent) ===")
|
||||
|
||||
# First client
|
||||
cache1 = DiskCache(directory=".instructor_cache_demo")
|
||||
client1 = instructor.from_openai(OpenAI(), cache=cache1)
|
||||
|
||||
messages = [{"role": "user", "content": "Create a user named Bob who is 30"}]
|
||||
|
||||
print("First client creates user...")
|
||||
user1 = client1.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
print(f"Result: {user1}")
|
||||
|
||||
# New client, same cache directory
|
||||
print("\nNew client with same cache dir...")
|
||||
cache2 = DiskCache(directory=".instructor_cache_demo")
|
||||
client2 = instructor.from_openai(OpenAI(), cache=cache2)
|
||||
|
||||
start = time.time()
|
||||
user2 = client2.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
cache_time = time.time() - start
|
||||
print(f"Result: {user2}")
|
||||
print(f"Time: {cache_time:.4f}s (from disk cache)")
|
||||
|
||||
assert user1.name == user2.name
|
||||
print("✓ Cache persisted across clients")
|
||||
|
||||
# Test create_with_completion persistence
|
||||
print("\nTesting create_with_completion persistence...")
|
||||
weather_messages = [{"role": "user", "content": "Weather is 25C and cloudy"}]
|
||||
|
||||
class Weather(BaseModel):
|
||||
temperature: float
|
||||
condition: str
|
||||
|
||||
# First call with completion
|
||||
weather1, completion1 = client1.create_with_completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=weather_messages,
|
||||
response_model=Weather,
|
||||
)
|
||||
print(f"Weather: {weather1}, Completion ID: {completion1.id}")
|
||||
|
||||
# Second call from different client - should get cached completion
|
||||
weather2, completion2 = client2.create_with_completion(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=weather_messages,
|
||||
response_model=Weather,
|
||||
)
|
||||
print(f"Cached: {weather2}, Completion ID: {completion2.id}")
|
||||
|
||||
assert weather1.temperature == weather2.temperature
|
||||
assert completion1.id == completion2.id
|
||||
print("✓ Raw completion persisted to disk")
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(".instructor_cache_demo", ignore_errors=True)
|
||||
|
||||
|
||||
def test_cache_ttl():
|
||||
"""Test cache TTL with DiskCache."""
|
||||
print("\n=== Testing Cache TTL ===")
|
||||
|
||||
cache = DiskCache(directory=".instructor_cache_ttl")
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
|
||||
messages = [{"role": "user", "content": "Create user Charlie age 35"}]
|
||||
|
||||
# Set with 2 second TTL
|
||||
print("Setting cache with 2s TTL...")
|
||||
user1 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
cache_ttl=2,
|
||||
)
|
||||
print(f"Result: {user1}")
|
||||
|
||||
# Immediate call - cached
|
||||
print("\nImmediate call (cached)...")
|
||||
start = time.time()
|
||||
user2 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
print(f"Time: {time.time() - start:.4f}s")
|
||||
|
||||
# Wait for expiry
|
||||
print("\nWaiting 3s for TTL expiry...")
|
||||
time.sleep(3)
|
||||
|
||||
# Should hit API again
|
||||
print("After TTL (hits API)...")
|
||||
start = time.time()
|
||||
user3 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=messages,
|
||||
response_model=User,
|
||||
)
|
||||
api_time = time.time() - start
|
||||
print(f"Time: {api_time:.2f}s")
|
||||
print("✓ TTL working correctly")
|
||||
|
||||
# Cleanup
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(".instructor_cache_ttl", ignore_errors=True)
|
||||
|
||||
|
||||
def test_different_inputs():
|
||||
"""Show that different inputs use different cache keys."""
|
||||
print("\n=== Testing Different Cache Keys ===")
|
||||
|
||||
cache = AutoCache(maxsize=100)
|
||||
client = instructor.from_openai(OpenAI(), cache=cache)
|
||||
|
||||
# Different prompts
|
||||
user1 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Create user David age 40"}],
|
||||
response_model=User,
|
||||
)
|
||||
print(f"User 1: {user1}")
|
||||
|
||||
user2 = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Create user Eve age 45"}],
|
||||
response_model=User,
|
||||
)
|
||||
print(f"User 2: {user2}")
|
||||
|
||||
assert user1.name != user2.name or user1.age != user2.age
|
||||
print("✓ Different prompts = different results")
|
||||
|
||||
# Different models
|
||||
class SimpleUser(BaseModel):
|
||||
name: str
|
||||
|
||||
simple = client.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Create user David age 40"}],
|
||||
response_model=SimpleUser,
|
||||
)
|
||||
print(f"Simple user: {simple}")
|
||||
print("✓ Different models = different cache keys")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Instructor Caching Demo - Real API Calls")
|
||||
print("=" * 50)
|
||||
|
||||
test_autocache()
|
||||
test_create_with_completion()
|
||||
test_diskcache()
|
||||
test_cache_ttl()
|
||||
test_different_inputs()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("All tests completed! ✨")
|
||||
@@ -1,31 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
This is a simple example which shows how to perform Chain Of Density summarization using GPT-3.5 and utilise the generated output to fine-tune a 3.5 model for production usage. All of our data referenced in this file is located [here](https://huggingface.co/datasets/ivanleomk/gpt4-chain-of-density) on hugging face
|
||||
|
||||
Check out our blog post [here](https://jxnl.github.io/instructor/blog/2023/11/05/implementing-chain-of-density/) where we have a detailed explanation of the code and a [colab notebook](https://colab.research.google.com/drive/1iBkrEh2G5U8yh8RmI8EkWxjLq6zIIuVm?usp=sharing) walking you through how we perform our calculations.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. First, install all of the required dependencies by running the command below. We recommend using a virtual environment to install these so that it does not affect your system installation.
|
||||
|
||||
> We use NLTK to ensure that our summaries are of a certain token length. In order to do so, you'll need to download the `punkt` package to compute the token metrics. You can do so by running the command `nltk.download('punkt')`
|
||||
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
2. Download the `test.csv` file and the `summarization.jsonl` file that you want to use for finetuning. We provide one with `20` examples, `50` examples and `100` examples to be used for testing. Let's now run a simple finetuning job with the following command.
|
||||
|
||||
> Don't forget to set your `OPENAI_API_KEY` as an environment variable in your shell before running these commands
|
||||
|
||||
```
|
||||
instructor jobs create-from-file summarization.jsonl
|
||||
```
|
||||
|
||||
3. Once the job is complete, you'll end up with a new GPT 3.5 model that's capable of producing high quality summaries with a high entity density. You can run it by simply changing our `finetune.py` file's `instructions.distil` annotator as
|
||||
|
||||
```
|
||||
@instructions.distil(model=<your finetuned model >,mode="dispatch")
|
||||
def distil_summarization(text: str) -> GeneratedSummary:
|
||||
// rest of code goes here
|
||||
```
|
||||
@@ -1,150 +0,0 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
import instructor
|
||||
import nltk
|
||||
from openai import OpenAI
|
||||
import spacy
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
nlp = spacy.load("en_core_web_sm")
|
||||
|
||||
|
||||
class InitialSummary(BaseModel):
|
||||
"""
|
||||
This is an initial summary which should be long ( 4-5 sentences, ~80 words) yet highly non-specific, containing little information beyond the entities marked as missing. Use overly verbose languages and fillers (Eg. This article discusses) to reach ~80 words.
|
||||
"""
|
||||
|
||||
summary: str = Field(
|
||||
...,
|
||||
description="This is a summary of the article provided which is overly verbose and uses fillers. It should be roughly 80 words in length",
|
||||
)
|
||||
|
||||
|
||||
class RewrittenSummary(BaseModel):
|
||||
"""
|
||||
This is a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities.
|
||||
|
||||
Guidelines
|
||||
- Make every word count : Rewrite the previous summary to improve flow and make space for additional entities
|
||||
- Never drop entities from the previous summary. If space cannot be made, add fewer new entities.
|
||||
- The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article.
|
||||
- Make space with fusion, compression, and removal of uninformative phrases like "the article discusses"
|
||||
- Missing entities can appear anywhere in the new summary
|
||||
|
||||
An Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title.
|
||||
"""
|
||||
|
||||
summary: str = Field(
|
||||
...,
|
||||
description="This is a new, denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities. It should have the same length ( ~ 80 words ) as the previous summary and should be easily understood without the Article",
|
||||
)
|
||||
absent: list[str] = Field(
|
||||
...,
|
||||
default_factory=list,
|
||||
description="this is a list of Entities found absent from the new summary that were present in the previous summary",
|
||||
)
|
||||
missing: list[str] = Field(
|
||||
default_factory=list,
|
||||
description="This is a list of 1-3 informative Entities from the Article that are missing from the new summary which should be included in the next generated summary.",
|
||||
)
|
||||
|
||||
@field_validator("summary")
|
||||
def min_entity_density(cls, v: str):
|
||||
# We want to make sure we have a minimum density of 0.12 whenever we do a rewrite. This ensures that the summary quality is always going up
|
||||
tokens = nltk.word_tokenize(v)
|
||||
num_tokens = len(tokens)
|
||||
|
||||
# Extract Entities
|
||||
doc = nlp(v)
|
||||
num_entities = len(doc.ents)
|
||||
|
||||
density = num_entities / num_tokens
|
||||
if density < 0.08:
|
||||
raise ValueError(
|
||||
f"The summary of {v} has too few entities. Please regenerate a new summary with more new entities added to it. Remember that new entities can be added at any point of the summary."
|
||||
)
|
||||
|
||||
return v
|
||||
|
||||
@field_validator("summary")
|
||||
def min_length(cls, v: str):
|
||||
tokens = nltk.word_tokenize(v)
|
||||
num_tokens = len(tokens)
|
||||
if num_tokens < 60:
|
||||
raise ValueError(
|
||||
"The current summary is too short. Please make sure that you generate a new summary that is around 80 words long."
|
||||
)
|
||||
return v
|
||||
|
||||
@field_validator("missing")
|
||||
def has_missing_entities(cls, missing_entities: list[str]):
|
||||
if len(missing_entities) == 0:
|
||||
raise ValueError(
|
||||
"You must identify 1-3 informative Entities from the Article which are missing from the previously generated summary to be used in a new summary"
|
||||
)
|
||||
return missing_entities
|
||||
|
||||
@field_validator("absent")
|
||||
def has_no_absent_entities(cls, absent_entities: list[str]):
|
||||
absent_entity_string = ",".join(absent_entities)
|
||||
if len(absent_entities) > 0:
|
||||
print(f"Detected absent entities of {absent_entity_string}")
|
||||
raise ValueError(
|
||||
f"Do not omit the following Entities {absent_entity_string} from the new summary"
|
||||
)
|
||||
return absent_entities
|
||||
|
||||
|
||||
def summarize_article(article: str, summary_steps: int = 3):
|
||||
summary_chain = []
|
||||
# We first generate an initial summary
|
||||
summary: InitialSummary = client.chat.completions.create(
|
||||
model="gpt-4-0613",
|
||||
response_model=InitialSummary,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Write a summary about the article that is long (4-5 sentences) yet highly non-specific. Use overly, verbose language and fillers(eg.,'this article discusses') to reach ~80 words. ",
|
||||
},
|
||||
{"role": "user", "content": f"Here is the Article: {article}"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "The generated summary should be about 80 words.",
|
||||
},
|
||||
],
|
||||
max_retries=2,
|
||||
)
|
||||
summary_chain.append(summary.summary)
|
||||
for _i in range(summary_steps):
|
||||
new_summary: RewrittenSummary = client.chat.completions.create(
|
||||
model="gpt-4-0613",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"""
|
||||
Article: {article}
|
||||
You are going to generate an increasingly concise,entity-dense summary of the following article.
|
||||
|
||||
Perform the following two tasks
|
||||
- Identify 1-3 informative entities from the following article which is missing from the previous summary
|
||||
- Write a new denser summary of identical length which covers every entity and detail from the previous summary plus the Missing Entities
|
||||
|
||||
Guidelines
|
||||
- Make every word count: re-write the previous summary to improve flow and make space for additional entities
|
||||
- Make space with fusion, compression, and removal of uninformative phrases like "the article discusses".
|
||||
- The summaries should become highly dense and concise yet self-contained, e.g., easily understood without the Article.
|
||||
- Missing entities can appear anywhere in the new summary
|
||||
- Never drop entities from the previous summary. If space cannot be made, add fewer new entities.
|
||||
""",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Here is the previous summary: {summary_chain[-1]}",
|
||||
},
|
||||
],
|
||||
max_retries=5,
|
||||
max_tokens=1000,
|
||||
response_model=RewrittenSummary,
|
||||
)
|
||||
summary_chain.append(new_summary.summary)
|
||||
|
||||
return summary_chain
|
||||
@@ -1,51 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from chain_of_density import summarize_article
|
||||
import csv
|
||||
import logging
|
||||
import instructor
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
instructions = instructor.Instructions(
|
||||
name="Chain Of Density",
|
||||
finetune_format="messages",
|
||||
# log handler is used to save the data to a file
|
||||
# you can imagine saving it to a database or other storage
|
||||
# based on your needs!
|
||||
log_handlers=[logging.FileHandler("generated.jsonl")],
|
||||
openai_client=client,
|
||||
)
|
||||
|
||||
|
||||
class GeneratedSummary(BaseModel):
|
||||
"""
|
||||
This represents a highly concise summary that includes as many entities as possible from the original source article.
|
||||
|
||||
An Entity is a real-world object that's assigned a name - for example, a person, country a product or a book title.
|
||||
|
||||
Guidelines
|
||||
- Make every word count
|
||||
- The new summary should be highly dense and concise yet self-contained, eg., easily understood without the Article.
|
||||
- Make space with fusion, compression, and removal of uninformative phrases like "the article discusses"
|
||||
"""
|
||||
|
||||
summary: str = Field(
|
||||
...,
|
||||
description="This represents the final summary generated that captures the meaning of the original article which is as concise as possible. ",
|
||||
)
|
||||
|
||||
|
||||
@instructions.distil
|
||||
def distil_summarization(text: str) -> GeneratedSummary:
|
||||
summary_chain: list[str] = summarize_article(text)
|
||||
return GeneratedSummary(summary=summary_chain[-1])
|
||||
|
||||
|
||||
with open("test.csv") as file:
|
||||
reader = csv.reader(file)
|
||||
next(reader) # Skip the header
|
||||
for article, _summary in reader:
|
||||
distil_summarization(article)
|
||||
@@ -1,5 +0,0 @@
|
||||
openai
|
||||
pydantic
|
||||
instructor
|
||||
nltk
|
||||
rich
|
||||
@@ -1,14 +0,0 @@
|
||||
# https://hub.docker.com/_/python
|
||||
FROM python:3.10-slim-bullseye
|
||||
|
||||
ENV PYTHONUNBUFFERED True
|
||||
ENV APP_HOME /app
|
||||
WORKDIR $APP_HOME
|
||||
COPY requirements.txt ./
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
|
||||
COPY . ./
|
||||
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
@@ -1,73 +0,0 @@
|
||||
# Citation with Extraction
|
||||
|
||||
This repository contains a FastAPI application that uses GPT-4 to answer questions based on a given context and extract relevant facts with correct and exact citations. The extracted facts are returned as JSON events using Server-Sent Events (SSE).
|
||||
|
||||
## How it Works
|
||||
|
||||
The FastAPI app defines an endpoint `/extract` that accepts a POST request with JSON data containing a `context` and a `query`. The `context` represents the text from which the question is being asked, and the `query` is the question itself.
|
||||
|
||||
The app leverages GPT-4, an advanced language model, to generate answers to the questions and extract relevant facts. It ensures that the extracted facts include direct quotes from the given context.
|
||||
|
||||
## Example Usage
|
||||
|
||||
To use the `/extract` endpoint, send a POST request with `curl` or any HTTP client with the following format:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{
|
||||
"context": "My name is Jason Liu, and I grew up in Toronto Canada but I was born in China.I went to an arts highschool but in university I studied Computational Mathematics and physics. As part of coop I worked at many companies including Stitchfix, Facebook. I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.",
|
||||
"query": "What did the author do in school?"
|
||||
}' -N http://localhost:8000/extract
|
||||
```
|
||||
|
||||
```sh
|
||||
data: {'body': 'In school, the author went to an arts high school.', 'spans': [(91, 106)], 'citation': ['arts highschool']}
|
||||
data: {'body': 'In university, the author studied Computational Mathematics and physics.', 'spans': [(135, 172)], 'citation': ['Computational Mathematics and physics']}
|
||||
```
|
||||
|
||||
Replace `http://localhost:8000` with the actual URL of your FastAPI app if it's running on a different host and port. The API will respond with Server-Sent Events (SSE) containing the extracted facts in real-time.
|
||||
|
||||
## Bring your own API key
|
||||
|
||||
If you have your own api key but dont want to try deploying it yourself you're welcome to use my
|
||||
modal isntance here, this code is public and I do not store your key.
|
||||
|
||||
```bash
|
||||
curl -X 'POST' \
|
||||
'https://jxnl--rag-citation-fastapi-app.modal.run/extract' \
|
||||
-H 'accept: */*' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <OPENAI_API_KEY>' \
|
||||
-d '{
|
||||
"context": "My name is Jason Liu, and I grew up in Toronto Canada but I was born in China.I went to an arts highschool but in university I studied Computational Mathematics and physics. As part of coop I worked at many companies including Stitchfix, Facebook. I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.",
|
||||
"query": "What did the author do in school?"
|
||||
}'
|
||||
```
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
To run this application, ensure you have the following Python packages installed:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Running the App
|
||||
|
||||
To run the FastAPI app, execute the following command:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
This will start the server, and the `/extract` endpoint will be available at `http://localhost:8000/extract`.
|
||||
|
||||
## Note
|
||||
|
||||
Ensure that you have a valid API key for GPT-4 from OpenAI. If you don't have one, you can obtain it from the OpenAI website.
|
||||
|
||||
Please use this application responsibly and be mindful of any usage limits or restrictions from OpenAI's API usage policy.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE). Feel free to use, modify, and distribute it as you see fit.
|
||||
@@ -1,129 +0,0 @@
|
||||
import instructor
|
||||
|
||||
from loguru import logger
|
||||
from openai import OpenAI
|
||||
from pydantic import Field, BaseModel, FieldValidationInfo, model_validator
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Fact(BaseModel):
|
||||
statement: str = Field(
|
||||
..., description="Body of the sentence, as part of a response"
|
||||
)
|
||||
substring_phrase: list[str] = Field(
|
||||
...,
|
||||
description="String quote long enough to evaluate the truthfulness of the fact",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_sources(self, info: FieldValidationInfo) -> "Fact":
|
||||
"""
|
||||
For each substring_phrase, find the span of the substring_phrase in the context.
|
||||
If the span is not found, remove the substring_phrase from the list.
|
||||
"""
|
||||
if info.context is None:
|
||||
logger.info("No context found, skipping validation")
|
||||
return self
|
||||
|
||||
# Get the context from the info
|
||||
text_chunks = info.context.get("text_chunk", None)
|
||||
|
||||
# Get the spans of the substring_phrase in the context
|
||||
spans = list(self.get_spans(text_chunks))
|
||||
logger.info(
|
||||
f"Found {len(spans)} span(s) for from {len(self.substring_phrase)} citation(s)."
|
||||
)
|
||||
# Replace the substring_phrase with the actual substring
|
||||
self.substring_phrase = [text_chunks[span[0] : span[1]] for span in spans]
|
||||
return self
|
||||
|
||||
def _get_span(self, quote, context, errs=5):
|
||||
import regex
|
||||
|
||||
minor = quote
|
||||
major = context
|
||||
|
||||
errs_ = 0
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
while s is None and errs_ <= errs:
|
||||
errs_ += 1
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
|
||||
if s is not None:
|
||||
yield from s.spans()
|
||||
|
||||
def get_spans(self, context):
|
||||
for quote in self.substring_phrase:
|
||||
yield from self._get_span(quote, context)
|
||||
|
||||
|
||||
class QuestionAnswer(instructor.ResponseSchema):
|
||||
"""
|
||||
Class representing a question and its answer as a list of facts each one should have a soruce.
|
||||
each sentence contains a body and a list of sources."""
|
||||
|
||||
question: str = Field(..., description="Question that was asked")
|
||||
answer: list[Fact] = Field(
|
||||
...,
|
||||
description="Body of the answer, each fact should be its separate object with a body and a list of sources",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_sources(self) -> "QuestionAnswer":
|
||||
"""
|
||||
Checks that each fact has some sources, and removes those that do not.
|
||||
"""
|
||||
logger.info(f"Validating {len(self.answer)} facts")
|
||||
self.answer = [fact for fact in self.answer if len(fact.substring_phrase) > 0]
|
||||
logger.info(f"Found {len(self.answer)} facts with sources")
|
||||
return self
|
||||
|
||||
|
||||
def ask_ai(question: str, context: str) -> QuestionAnswer:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0,
|
||||
response_model=QuestionAnswer,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a world class algorithm to answer questions with correct and exact citations.",
|
||||
},
|
||||
{"role": "user", "content": f"{context}"},
|
||||
{"role": "user", "content": f"Question: {question}"},
|
||||
],
|
||||
validation_context={"text_chunk": context},
|
||||
)
|
||||
|
||||
|
||||
question = "where did he go to school?"
|
||||
context = """
|
||||
My name is Jason Liu, and I grew up in Toronto Canada but I was born in China.I went to an arts highschool but in university I studied Computational Mathematics and physics. As part of coop I worked at many companies including Stitchfix, Facebook. I also started the Data Science club at the University of Waterloo and I was the president of the club for 2 years.
|
||||
"""
|
||||
|
||||
answer = ask_ai(question, context)
|
||||
print(answer.model_dump_json(indent=2))
|
||||
"""
|
||||
2023-09-09 15:48:11.022 | INFO | __main__:validate_sources:35 - Found 1 span(s) for from 1 citation(s).
|
||||
2023-09-09 15:48:11.023 | INFO | __main__:validate_sources:35 - Found 1 span(s) for from 1 citation(s).
|
||||
2023-09-09 15:48:11.023 | INFO | __main__:validate_sources:78 - Validating 2 facts
|
||||
2023-09-09 15:48:11.023 | INFO | __main__:validate_sources:80 - Found 2 facts with sources
|
||||
{
|
||||
"question": "where did he go to school?",
|
||||
"answer": [
|
||||
{
|
||||
"statement": "Jason Liu went to an arts highschool.",
|
||||
"substring_phrase": [
|
||||
"arts highschool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"statement": "Jason Liu studied Computational Mathematics and physics in university.",
|
||||
"substring_phrase": [
|
||||
"university"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
@@ -1,6 +0,0 @@
|
||||
import erdantic as erd
|
||||
|
||||
from citation_fuzzy_match import QuestionAnswer
|
||||
|
||||
diagram = erd.create(QuestionAnswer)
|
||||
diagram.draw("examples/citation_fuzzy_match/schema.png")
|
||||
@@ -1,147 +0,0 @@
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.params import Depends
|
||||
from instructor import ResponseSchema
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.responses import StreamingResponse
|
||||
|
||||
import os
|
||||
import instructor
|
||||
import logging
|
||||
|
||||
from openai import OpenAI
|
||||
from instructor.dsl.multitask import MultiTaskBase
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# FastAPI app
|
||||
app = FastAPI(
|
||||
title="Citation with Extraction",
|
||||
)
|
||||
|
||||
|
||||
class Fact(BaseModel):
|
||||
"""
|
||||
Class representing single statement.
|
||||
Each fact has a body and a list of sources.
|
||||
If there are multiple facts make sure to break them apart such that each one only uses a set of sources that are relevant to it.
|
||||
"""
|
||||
|
||||
fact: str = Field(
|
||||
...,
|
||||
description="Body of the sentences, as part of a response, it should read like a sentence that answers the question",
|
||||
)
|
||||
substring_quotes: list[str] = Field(
|
||||
...,
|
||||
description="Each source should be a direct quote from the context, as a substring of the original content",
|
||||
)
|
||||
|
||||
def _get_span(self, quote, context):
|
||||
import regex
|
||||
|
||||
minor = quote
|
||||
major = context
|
||||
|
||||
errs_ = 0
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
while s is None and errs_ <= len(context) * 0.05:
|
||||
errs_ += 1
|
||||
s = regex.search(f"({minor}){{e<={errs_}}}", major)
|
||||
|
||||
if s is not None:
|
||||
yield from s.spans()
|
||||
|
||||
def get_spans(self, context):
|
||||
if self.substring_quotes:
|
||||
for quote in self.substring_quotes:
|
||||
yield from self._get_span(quote, context)
|
||||
|
||||
|
||||
class QuestionAnswer(ResponseSchema, MultiTaskBase):
|
||||
"""
|
||||
Class representing a question and its answer as a list of facts each one should have a source.
|
||||
each sentence contains a body and a list of sources."""
|
||||
|
||||
question: str = Field(..., description="Question that was asked")
|
||||
tasks: list[Fact] = Field(
|
||||
...,
|
||||
description="Body of the answer, each fact should be its separate object with a body and a list of sources",
|
||||
)
|
||||
|
||||
|
||||
QuestionAnswer.task_type = Fact
|
||||
|
||||
|
||||
class Question(BaseModel):
|
||||
context: str = Field(..., description="Context to extract answers from")
|
||||
query: str = Field(..., description="Question to answer")
|
||||
|
||||
|
||||
# Function to extract entities from input text using GPT-3.5
|
||||
def stream_extract(question: Question) -> Iterable[Fact]:
|
||||
completion = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0,
|
||||
stream=True,
|
||||
functions=[QuestionAnswer.openai_schema],
|
||||
function_call={"name": QuestionAnswer.openai_schema["name"]},
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a world class algorithm to answer questions with correct and exact citations. ",
|
||||
},
|
||||
{"role": "user", "content": "Answer question using the following context"},
|
||||
{"role": "user", "content": f"{question.context}"},
|
||||
{"role": "user", "content": f"Question: {question.query}"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Tips: Make sure to cite your sources, and use the exact words from the context.",
|
||||
},
|
||||
],
|
||||
max_tokens=2000,
|
||||
)
|
||||
return QuestionAnswer.from_streaming_response(completion)
|
||||
|
||||
|
||||
def get_api_key(request: Request):
|
||||
"""
|
||||
This just gets the API key from the request headers.
|
||||
but tries to read from the environment variable OPENAI_API_KEY first.
|
||||
"""
|
||||
if "OPENAI_API_KEY" in os.environ:
|
||||
return os.environ["OPENAI_API_KEY"]
|
||||
|
||||
auth = request.headers.get("Authorization")
|
||||
if auth is None:
|
||||
raise HTTPException(status_code=401, detail="Missing Authorization header")
|
||||
|
||||
if auth.startswith("Bearer "):
|
||||
return auth.replace("Bearer ", "")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# Route to handle SSE events and return users
|
||||
@app.post("/extract", response_class=StreamingResponse)
|
||||
async def extract(question: Question, openai_key: str = Depends(get_api_key)):
|
||||
raise Exception(
|
||||
"The 'openai.api_key' option isn't read in the client API. You will need to pass it when you instantiate the client, e.g. 'OpenAI(api_key=openai_key)'"
|
||||
)
|
||||
facts = stream_extract(question)
|
||||
|
||||
async def generate():
|
||||
for fact in facts:
|
||||
logger.info(f"Fact: {fact}")
|
||||
spans = list(fact.get_spans(question.context))
|
||||
resp = {
|
||||
"body": fact.fact,
|
||||
"spans": spans,
|
||||
"citation": [question.context[a:b] for (a, b) in spans],
|
||||
}
|
||||
resp_json = json.dumps(resp)
|
||||
yield f"data: {resp_json}"
|
||||
yield "data: [DONE]"
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
@@ -1,12 +0,0 @@
|
||||
from main import app
|
||||
import modal
|
||||
|
||||
stub = modal.Stub("rag-citation")
|
||||
|
||||
image = modal.Image.debian_slim().pip_install("fastapi", "instructor>=0.2.1", "regex")
|
||||
|
||||
|
||||
@stub.function(image=image)
|
||||
@modal.asgi_app()
|
||||
def fastapi_app():
|
||||
return app
|
||||
@@ -1,6 +0,0 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
openai>=1.0.0
|
||||
pydantic
|
||||
instructor
|
||||
regex
|
||||
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,225 +0,0 @@
|
||||
from typing import Optional
|
||||
from openai import OpenAI
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
ValidationError,
|
||||
ValidationInfo,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
|
||||
import instructor
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
"""
|
||||
Example 1) Simple Substring check that compares a citation to a text chunk
|
||||
"""
|
||||
|
||||
|
||||
class Statements(BaseModel):
|
||||
body: str
|
||||
substring_quote: str
|
||||
|
||||
@field_validator("substring_quote")
|
||||
@classmethod
|
||||
def substring_quote_exists(cls, v: str, info: ValidationInfo):
|
||||
context = info.context.get("text_chunks", None)
|
||||
|
||||
# Check if the substring_quote is in the text_chunk
|
||||
# if not, raise an error
|
||||
for text_chunk in context.values():
|
||||
if v in text_chunk:
|
||||
return v
|
||||
raise ValueError(
|
||||
f"Could not find substring_quote `{v}` in contexts",
|
||||
)
|
||||
|
||||
|
||||
class AnswerWithCitaton(BaseModel):
|
||||
question: str
|
||||
answer: list[Statements]
|
||||
|
||||
|
||||
try:
|
||||
AnswerWithCitaton.model_validate(
|
||||
{
|
||||
"question": "What is the capital of France?",
|
||||
"answer": [
|
||||
{"body": "Paris", "substring_quote": "Paris is the capital of France"},
|
||||
],
|
||||
},
|
||||
context={
|
||||
"text_chunks": {
|
||||
1: "Jason is a pirate",
|
||||
2: "Paris is not the capital of France",
|
||||
3: "Irrelevant data",
|
||||
}
|
||||
},
|
||||
)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
"""
|
||||
answer.0.substring_quote
|
||||
Value error, Could not find substring_quote `Paris is the capital of France` in contexts [type=value_error, input_value='Paris is the capital of France', input_type=str]
|
||||
For further information visit https://errors.pydantic.dev/2.4/v/value_error
|
||||
"""
|
||||
|
||||
|
||||
"""
|
||||
Example 2) Using an LLM to verify if a
|
||||
"""
|
||||
|
||||
|
||||
class Validation(BaseModel):
|
||||
"""
|
||||
Verification response from the LLM,
|
||||
the error message should be detailed if the is_valid is False
|
||||
but keep it to less than 100 characters, reference specific
|
||||
attributes that you are comparing, use `...` is the string is too long
|
||||
"""
|
||||
|
||||
is_valid: bool
|
||||
error_messages: Optional[str] = Field(None, description="Error messages if any")
|
||||
|
||||
|
||||
class Statements(BaseModel):
|
||||
body: str
|
||||
substring_quote: str
|
||||
|
||||
@model_validator(mode="after")
|
||||
def substring_quote_exists(self, info: ValidationInfo):
|
||||
context = info.context.get("text_chunks", None)
|
||||
|
||||
resp: Validation = client.chat.completions.create(
|
||||
response_model=Validation,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Does the following citation exist in the following context?\n\nCitation: {self.substring_quote}\n\nContext: {context}",
|
||||
}
|
||||
],
|
||||
model="gpt-3.5-turbo",
|
||||
)
|
||||
|
||||
if resp.is_valid:
|
||||
return self
|
||||
|
||||
raise ValueError(resp.error_messages)
|
||||
|
||||
|
||||
class AnswerWithCitaton(BaseModel):
|
||||
question: str
|
||||
answer: list[Statements]
|
||||
|
||||
|
||||
resp = AnswerWithCitaton.model_validate(
|
||||
{
|
||||
"question": "What is the capital of France?",
|
||||
"answer": [
|
||||
{"body": "Paris", "substring_quote": "Paris is the capital of France"},
|
||||
],
|
||||
},
|
||||
context={
|
||||
"text_chunks": {
|
||||
1: "Jason is a pirate",
|
||||
2: "Paris is the capital of France",
|
||||
3: "Irrelevant data",
|
||||
}
|
||||
},
|
||||
)
|
||||
# output: notice that there are no errors
|
||||
print(resp.model_dump_json(indent=2))
|
||||
{
|
||||
"question": "What is the capital of France?",
|
||||
"answer": [{"body": "Paris", "substring_quote": "Paris is the capital of France"}],
|
||||
}
|
||||
|
||||
# Now we change the text chunk to something else, and we get an error
|
||||
try:
|
||||
AnswerWithCitaton.model_validate(
|
||||
{
|
||||
"question": "What is the capital of France?",
|
||||
"answer": [
|
||||
{"body": "Paris", "substring_quote": "Paris is the capital of France"},
|
||||
],
|
||||
},
|
||||
context={
|
||||
"text_chunks": {
|
||||
1: "Jason is a pirate",
|
||||
2: "Paris is not the capital of France",
|
||||
3: "Irrelevant data",
|
||||
}
|
||||
},
|
||||
)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
"""
|
||||
1 validation error for AnswerWithCitaton
|
||||
answer.0
|
||||
Value error, Citation not found in context [type=value_error, input_value={'body': 'Paris', 'substr... the capital of France'}, input_type=dict]
|
||||
For further information visit https://errors.pydantic.dev/2.4/v/value_error
|
||||
"""
|
||||
|
||||
# Example 3) Using an LLM to verify if the citations and the answers are all aligned
|
||||
|
||||
|
||||
# we keep the same model as above for Statements, but we add a new model for the answer
|
||||
# that also verifies that the citations are aligned with the answers
|
||||
class AnswerWithCitaton(BaseModel):
|
||||
question: str
|
||||
answer: list[Statements]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_answer(self, info: ValidationInfo):
|
||||
context = info.context.get("text_chunks", None)
|
||||
|
||||
resp: Validation = client.chat.completions.create(
|
||||
response_model=Validation,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Does the following answers match the question and the context?\n\nQuestion: {self.question}\n\nAnswer: {self.answer}\n\nContext: {context}",
|
||||
}
|
||||
],
|
||||
model="gpt-3.5-turbo",
|
||||
)
|
||||
|
||||
if resp.is_valid:
|
||||
return self
|
||||
|
||||
raise ValueError(resp.error_messages)
|
||||
|
||||
|
||||
"""
|
||||
Using LLMs for citation verification is inefficient during runtime.
|
||||
However, we can utilize them to create a dataset consisting only of accurate responses
|
||||
where citations must be valid (as determined by LLM, fuzzy text search, etc.).
|
||||
|
||||
This approach would require an initial investment during data generation to obtain
|
||||
a finely-tuned model for improved citation.
|
||||
"""
|
||||
try:
|
||||
AnswerWithCitaton.model_validate(
|
||||
{
|
||||
"question": "What is the capital of France?",
|
||||
"answer": [
|
||||
{"body": "Texas", "substring_quote": "Paris is the capital of France"},
|
||||
],
|
||||
},
|
||||
context={
|
||||
"text_chunks": {
|
||||
1: "Jason is a pirate",
|
||||
2: "Paris is the capital of France",
|
||||
3: "Irrelevant data",
|
||||
}
|
||||
},
|
||||
)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
"""
|
||||
1 validation error for AnswerWithCitaton
|
||||
Value error, The answer does not match the question and context [type=value_error, input_value={'question': 'What is the...he capital of France'}]}, input_type=dict]
|
||||
For further information visit https://errors.pydantic.dev/2.4/v/value_error
|
||||
"""
|
||||
@@ -1,176 +0,0 @@
|
||||
# pip install openai instructor
|
||||
from pydantic import BaseModel, field_validator, Field
|
||||
import openai
|
||||
import instructor
|
||||
from tqdm import tqdm
|
||||
|
||||
client = instructor.from_openai(openai.OpenAI())
|
||||
|
||||
classes = {
|
||||
"11-0000": "Management",
|
||||
"13-0000": "Business and Financial Operations",
|
||||
"15-0000": "Computer and Mathematical",
|
||||
"17-0000": "Architecture and Engineering",
|
||||
"19-0000": "Life, Physical, and Social Science",
|
||||
"21-0000": "Community and Social Service",
|
||||
"23-0000": "Legal",
|
||||
"25-0000": "Education Instruction and Library",
|
||||
"27-0000": "Arts, Design, Entertainment, Sports and Media",
|
||||
"29-0000": "Healthcare Practitioners and Technical",
|
||||
"31-0000": "Healthcare Support",
|
||||
"33-0000": "Protective Service",
|
||||
"35-0000": "Food Preparation and Serving",
|
||||
"37-0000": "Building and Grounds Cleaning and Maintenance",
|
||||
"39-0000": "Personal Care and Service",
|
||||
"41-0000": "Sales and Related",
|
||||
"43-0000": "Office and Administrative Support",
|
||||
"45-0000": "Farming, Fishing and Forestry",
|
||||
"47-0000": "Construction and Extraction",
|
||||
"49-0000": "Installation, Maintenance, and Repair",
|
||||
"51-0000": "Production Occupations",
|
||||
"53-0000": "Transportation and Material Moving",
|
||||
"55-0000": "Military Specific",
|
||||
"99-0000": "Other",
|
||||
}
|
||||
|
||||
|
||||
class SOCCode(BaseModel):
|
||||
reasoning: str = Field(
|
||||
default=None,
|
||||
description="Step-by-step reasoning to get the correct classification",
|
||||
)
|
||||
code: str
|
||||
|
||||
@field_validator("code")
|
||||
def validate_code(cls, v):
|
||||
if v not in classes:
|
||||
raise ValueError(f"Invalid SOC code, {v}")
|
||||
return v
|
||||
|
||||
|
||||
def classify_job(description: str) -> SOCCode:
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=SOCCode,
|
||||
max_retries=3,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You are an expert at classifying job descriptions into Standard Occupational Classification (SOC) codes. from the following list: {classes}",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify this job description into the most appropriate SOC code: {description}",
|
||||
},
|
||||
],
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# gpt-3.5-turbo: 16/20
|
||||
# gpt-3.5-turbo (COT): 18/20
|
||||
# gpt-4-turbo: 20/20
|
||||
|
||||
job_descriptions = [
|
||||
(
|
||||
"Develop and design complex software applications for various industries, including finance, healthcare, and e-commerce",
|
||||
"15-0000", # Computer and Mathematical Occupations
|
||||
),
|
||||
(
|
||||
"Provide comprehensive technical support and troubleshooting for enterprise-level software products, ensuring seamless user experience",
|
||||
"15-0000", # Computer and Mathematical Occupations
|
||||
),
|
||||
(
|
||||
"Teach a diverse range of subjects to elementary school students, fostering their intellectual and social development",
|
||||
"25-0000", # Education, Training, and Library Occupations
|
||||
),
|
||||
(
|
||||
"Conduct cutting-edge research in various academic fields at a renowned university, contributing to the advancement of knowledge",
|
||||
"25-0000", # Education, Training, and Library Occupations
|
||||
),
|
||||
(
|
||||
"Design visually appealing and strategically effective logos, branding, and marketing materials for clients across different industries",
|
||||
"27-0000", # Arts, Design, Entertainment, Sports, and Media Occupations
|
||||
),
|
||||
(
|
||||
"Perform as part of a professional musical group, entertaining audiences and showcasing artistic talent",
|
||||
"27-0000", # Arts, Design, Entertainment, Sports, and Media Occupations
|
||||
),
|
||||
(
|
||||
"Diagnose and treat a wide range of injuries and medical conditions, providing comprehensive healthcare services to patients",
|
||||
"29-0000", # Healthcare Practitioners and Technical Occupations
|
||||
),
|
||||
(
|
||||
"Assist doctors and nurses in delivering high-quality patient care, ensuring the smooth operation of healthcare facilities",
|
||||
"31-0000", # Healthcare Support Occupations
|
||||
),
|
||||
(
|
||||
"Patrol assigned areas to enforce laws and ordinances, maintaining public safety and order in the community",
|
||||
"33-0000", # Protective Service Occupations
|
||||
),
|
||||
(
|
||||
"Prepare and serve a diverse menu of delectable meals in a fast-paced restaurant environment",
|
||||
"35-0000", # Food Preparation and Serving Related Occupations
|
||||
),
|
||||
(
|
||||
"Maintain the cleanliness and upkeep of various buildings and facilities, ensuring a safe and presentable environment",
|
||||
"37-0000", # Building and Grounds Cleaning and Maintenance Occupations
|
||||
),
|
||||
(
|
||||
"Provide a range of beauty services, such as haircuts, styling, and manicures, to help clients look and feel their best",
|
||||
"39-0000", # Personal Care and Service Occupations
|
||||
),
|
||||
(
|
||||
"Engage with customers in a retail setting, providing excellent service and assisting them in finding the products they need",
|
||||
"41-0000", # Sales and Related Occupations
|
||||
),
|
||||
(
|
||||
"Perform a variety of clerical duties in an office environment, supporting the overall operations of the organization",
|
||||
"43-0000", # Office and Administrative Support Occupations
|
||||
),
|
||||
(
|
||||
"Cultivate and harvest a wide range of crops, contributing to the production of food and other agricultural products",
|
||||
"45-0000", # Farming, Fishing, and Forestry Occupations
|
||||
),
|
||||
(
|
||||
"Construct and build various structures, including residential, commercial, and infrastructure projects",
|
||||
"47-0000", # Construction and Extraction Occupations
|
||||
),
|
||||
(
|
||||
"Repair and maintain a diverse range of mechanical equipment, ensuring their proper functioning and longevity",
|
||||
"49-0000", # Installation, Maintenance, and Repair Occupations
|
||||
),
|
||||
(
|
||||
"Operate specialized machinery and equipment in a manufacturing setting to produce high-quality goods",
|
||||
"51-0000", # Production Occupations
|
||||
),
|
||||
(
|
||||
"Transport freight and goods across different regions, ensuring timely and efficient delivery",
|
||||
"53-0000", # Transportation and Material Moving Occupations
|
||||
),
|
||||
(
|
||||
"Serve in the armed forces, protecting the nation and its citizens through various military operations and duties",
|
||||
"55-0000", # Military Specific Occupations
|
||||
),
|
||||
]
|
||||
|
||||
correct = 0
|
||||
errors = []
|
||||
for description, expected_code in tqdm(job_descriptions):
|
||||
try:
|
||||
predicted_code = None
|
||||
result = classify_job(description)
|
||||
predicted_code = result.code
|
||||
assert result.code == expected_code, (
|
||||
f"Expected {expected_code}, got {result.code} for description: {description}"
|
||||
)
|
||||
correct += 1
|
||||
except Exception as e:
|
||||
errors.append(
|
||||
f"Got {classes.get(predicted_code, 'Unknown')} expected {classes.get(expected_code, 'Unknown')}"
|
||||
)
|
||||
|
||||
print(f"{correct} out of {len(job_descriptions)} tests passed!")
|
||||
for error in errors:
|
||||
print(error)
|
||||
@@ -1,41 +0,0 @@
|
||||
import enum
|
||||
import instructor
|
||||
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
# Define new Enum class for multiple labels
|
||||
class MultiLabels(str, enum.Enum):
|
||||
BILLING = "billing"
|
||||
GENERAL_QUERY = "general_query"
|
||||
HARDWARE = "hardware"
|
||||
|
||||
|
||||
# Adjust the prediction model to accommodate a list of labels
|
||||
class MultiClassPrediction(BaseModel):
|
||||
predicted_labels: list[MultiLabels]
|
||||
|
||||
|
||||
# Modify the classify function
|
||||
def multi_classify(data: str) -> MultiClassPrediction:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=MultiClassPrediction,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following support ticket: {data}",
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
# Example using a support ticket
|
||||
ticket = (
|
||||
"My account is locked and I can't access my billing info. Phone is also broken."
|
||||
)
|
||||
prediction = multi_classify(ticket)
|
||||
print(prediction)
|
||||
@@ -1,37 +0,0 @@
|
||||
import enum
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Labels(str, enum.Enum):
|
||||
SPAM = "spam"
|
||||
NOT_SPAM = "not_spam"
|
||||
|
||||
|
||||
class SinglePrediction(BaseModel):
|
||||
"""
|
||||
Correct class label for the given text
|
||||
"""
|
||||
|
||||
class_label: Labels
|
||||
|
||||
|
||||
def classify(data: str) -> SinglePrediction:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=SinglePrediction,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following text: {data}",
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
prediction = classify("Hello there I'm a nigerian prince and I want to give you money")
|
||||
assert prediction.class_label == Labels.SPAM
|
||||
@@ -1,121 +0,0 @@
|
||||
import json
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from jinja2 import Template
|
||||
import re
|
||||
from datamodel_code_generator import InputFileType, generate
|
||||
from pydantic import BaseModel
|
||||
|
||||
APP_TEMPLATE_STR = '''# generated by instructor-codegen:
|
||||
# timestamp: {{timestamp}}
|
||||
# task_name: {{task_name}}
|
||||
# api_path: {{api_path}}
|
||||
# json_schema_path: {{json_schema_path}}
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from jinja2 import Template
|
||||
from models import {{title}}
|
||||
|
||||
import openai
|
||||
import instructor
|
||||
|
||||
instructor.from_openai()
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
class TemplateVariables(BaseModel):
|
||||
{% for var in jinja_vars %}
|
||||
{{var.strip()}}: str
|
||||
{% endfor %}
|
||||
|
||||
class RequestSchema(BaseModel):
|
||||
template_variables: TemplateVariables
|
||||
model: str
|
||||
temperature: int
|
||||
|
||||
PROMPT_TEMPLATE = Template("""{{prompt_template}}""".strip())
|
||||
|
||||
@app.post("{{api_path}}", response_model={{title}})
|
||||
async def {{task_name}}(input: RequestSchema) -> {{title}}:
|
||||
rendered_prompt = PROMPT_TEMPLATE.render(**input.template_variables.model_dump())
|
||||
return await openai.ChatCompletion.acreate(
|
||||
model=input.model,
|
||||
temperature=input.temperature,
|
||||
response_model={{title}},
|
||||
messages=[
|
||||
{"role": "user", "content": rendered_prompt}
|
||||
]
|
||||
) # type: ignore
|
||||
'''
|
||||
|
||||
|
||||
class TemplateVariables(BaseModel):
|
||||
biography: str
|
||||
|
||||
|
||||
def load_json_schema(json_schema_path: str) -> dict:
|
||||
try:
|
||||
with open(json_schema_path) as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Failed to load JSON schema: {e}") from e
|
||||
|
||||
|
||||
def generate_pydantic_model(json_schema_path: str):
|
||||
input_path = Path(json_schema_path)
|
||||
output_path = Path("./models.py")
|
||||
generate(
|
||||
input_=input_path, input_file_type=InputFileType.JsonSchema, output=output_path
|
||||
)
|
||||
|
||||
|
||||
def extract_jinja_vars(prompt_template: str) -> list:
|
||||
return re.findall(r"\{\{(.*?)\}\}", prompt_template)
|
||||
|
||||
|
||||
def render_app_template(template_str: str, **kwargs) -> str:
|
||||
app_template = Template(template_str)
|
||||
return app_template.render(**kwargs)
|
||||
|
||||
|
||||
def create_app(
|
||||
api_path: str, task_name: str, json_schema_path: str, prompt_template: str
|
||||
) -> str:
|
||||
if not api_path.startswith("/"):
|
||||
api_path = "/" + api_path
|
||||
|
||||
schema = load_json_schema(json_schema_path)
|
||||
title = schema["title"]
|
||||
generate_pydantic_model(json_schema_path)
|
||||
|
||||
jinja_vars = extract_jinja_vars(prompt_template)
|
||||
|
||||
return render_app_template(
|
||||
APP_TEMPLATE_STR,
|
||||
timestamp=datetime.datetime.now().isoformat(),
|
||||
task_name=task_name,
|
||||
api_path=api_path,
|
||||
json_schema_path=json_schema_path,
|
||||
title=title,
|
||||
jinja_vars=jinja_vars,
|
||||
prompt_template=prompt_template,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
fastapi_code = create_app(
|
||||
api_path="/api/v1/extract_person",
|
||||
task_name="extract_person",
|
||||
json_schema_path="./input.json",
|
||||
prompt_template="Extract the person from the following: {{biography}}",
|
||||
)
|
||||
|
||||
with open("./run.py", "w") as f:
|
||||
f.write(fastapi_code)
|
||||
|
||||
print("FastAPI application generated and saved to './run.py'")
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "ExtractPerson",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"age": {
|
||||
"type": "integer"
|
||||
},
|
||||
"phoneNumbers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["home", "work", "mobile"]
|
||||
},
|
||||
"number": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["type", "number"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "age", "phoneNumbers"]
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
# generated by datamodel-codegen:
|
||||
# filename: input.json
|
||||
# timestamp: 2023-09-10T00:33:42+00:00
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
home = "home"
|
||||
work = "work"
|
||||
mobile = "mobile"
|
||||
|
||||
|
||||
class PhoneNumber(BaseModel):
|
||||
type: Type
|
||||
number: str
|
||||
|
||||
|
||||
class ExtractPerson(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
phoneNumbers: list[PhoneNumber]
|
||||
@@ -1,35 +0,0 @@
|
||||
# FastAPI Code Generator
|
||||
|
||||
## Overview
|
||||
|
||||
Generates FastAPI application code from API path, task name, JSON schema path, and Jinja2 prompt template. Also creates a `models.py` file for Pydantic models.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- FastAPI
|
||||
- Pydantic
|
||||
- Jinja2
|
||||
- datamodel-code-generator
|
||||
|
||||
## Functions
|
||||
|
||||
### `create_app(api_path: str, task_name: str, json_schema_path: str, prompt_template: str) -> str`
|
||||
|
||||
Main function to generate FastAPI application code.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the script with required parameters.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
fastapi_code = create_app(
|
||||
api_path="/api/v1/extract_person",
|
||||
task_name="extract_person",
|
||||
json_schema_path="./input.json",
|
||||
prompt_template="Extract the person from the following: {{biography}}",
|
||||
)
|
||||
```
|
||||
|
||||
Outputs FastAPI application code to `./run.py` and a Pydantic model to `./models.py`.
|
||||
@@ -1,43 +0,0 @@
|
||||
# This file was generated by instructor
|
||||
# timestamp: 2023-09-09T20:33:42.572627
|
||||
# task_name: extract_person
|
||||
# api_path: /api/v1/extract_person
|
||||
# json_schema_path: ./input.json
|
||||
|
||||
import instructor
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from jinja2 import Template
|
||||
from models import ExtractPerson
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
aclient = instructor.apatch(AsyncOpenAI())
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class TemplateVariables(BaseModel):
|
||||
biography: str
|
||||
|
||||
|
||||
class RequestSchema(BaseModel):
|
||||
template_variables: TemplateVariables
|
||||
model: str
|
||||
temperature: int
|
||||
|
||||
|
||||
PROMPT_TEMPLATE = Template(
|
||||
"""Extract the person from the following: {{biography}}""".strip()
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/v1/extract_person", response_model=ExtractPerson)
|
||||
async def extract_person(input: RequestSchema) -> ExtractPerson:
|
||||
rendered_prompt = PROMPT_TEMPLATE.render(**input.template_variables.model_dump())
|
||||
return await aclient.chat.completions.create(
|
||||
model=input.model,
|
||||
temperature=input.temperature,
|
||||
response_model=ExtractPerson,
|
||||
messages=[{"role": "user", "content": rendered_prompt}],
|
||||
) # type: ignore
|
||||
@@ -1,59 +0,0 @@
|
||||
import cohere
|
||||
import instructor
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# Patching the Cohere client with the instructor for enhanced capabilities
|
||||
client = instructor.from_cohere(
|
||||
cohere.ClientV2(),
|
||||
max_tokens=1000,
|
||||
model="command-a-03-2025",
|
||||
)
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str = Field(description="name of the person")
|
||||
country_of_origin: str = Field(description="country of origin of the person")
|
||||
|
||||
|
||||
class Group(BaseModel):
|
||||
group_name: str = Field(description="name of the group")
|
||||
members: list[Person] = Field(description="list of members in the group")
|
||||
|
||||
|
||||
task = """\
|
||||
Given the following text, create a Group object for 'The Beatles' band
|
||||
|
||||
Text:
|
||||
The Beatles were an English rock band formed in Liverpool in 1960. With a line-up comprising John Lennon, Paul McCartney, George Harrison and Ringo Starr, they are regarded as the most influential band of all time. The group were integral to the development of 1960s counterculture and popular music's recognition as an art form.
|
||||
"""
|
||||
group = client.messages.create(
|
||||
response_model=Group,
|
||||
messages=[{"role": "user", "content": task}],
|
||||
temperature=0,
|
||||
)
|
||||
|
||||
print(group.model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"group_name": "The Beatles",
|
||||
"members": [
|
||||
{
|
||||
"name": "John Lennon",
|
||||
"country_of_origin": "England"
|
||||
},
|
||||
{
|
||||
"name": "Paul McCartney",
|
||||
"country_of_origin": "England"
|
||||
},
|
||||
{
|
||||
"name": "George Harrison",
|
||||
"country_of_origin": "England"
|
||||
},
|
||||
{
|
||||
"name": "Ringo Starr",
|
||||
"country_of_origin": "England"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
@@ -1,87 +0,0 @@
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel, Field
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class CRMSource(Enum):
|
||||
personal = "personal"
|
||||
business = "business"
|
||||
work_contacts = "work_contacts"
|
||||
all = "all"
|
||||
|
||||
|
||||
class CRMSearch(BaseModel):
|
||||
"""A CRM search query
|
||||
|
||||
The search description is a natural language description of the search query
|
||||
the backend will use semantic search so use a range of phrases to describe the search
|
||||
"""
|
||||
|
||||
source: CRMSource
|
||||
city_location: str = Field(
|
||||
..., description="City location used to match the desired customer profile"
|
||||
)
|
||||
search_description: str = Field(
|
||||
..., description="Search query used to match the desired customer profile"
|
||||
)
|
||||
|
||||
|
||||
class CRMSearchQuery(BaseModel):
|
||||
"""
|
||||
A set of CRM queries to be executed against a CRM system,
|
||||
for large locations decompose into multiple queries of smaller locations
|
||||
"""
|
||||
|
||||
queries: list[CRMSearch]
|
||||
|
||||
|
||||
def query_crm(query: str) -> CRMSearchQuery:
|
||||
queries = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=CRMSearchQuery,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": """
|
||||
You are a world class CRM search career generator.
|
||||
You will take the user query and decompose it into a set of CRM queries queries.
|
||||
""",
|
||||
},
|
||||
{"role": "user", "content": query},
|
||||
],
|
||||
)
|
||||
return queries
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
query = "find me all the pottery businesses in San Francisco and my friends in the east coast big cities"
|
||||
print(query_crm(query).model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"queries": [
|
||||
{
|
||||
"source": "business",
|
||||
"city_location": "San Francisco",
|
||||
"search_description": "pottery businesses"
|
||||
},
|
||||
{
|
||||
"source": "personal",
|
||||
"city_location": "New York",
|
||||
"search_description": "friends in New York"
|
||||
},
|
||||
{
|
||||
"source": "personal",
|
||||
"city_location": "Boston",
|
||||
"search_description": "friends in Boston"
|
||||
},
|
||||
{
|
||||
"source": "personal",
|
||||
"city_location": "Philadelphia",
|
||||
"search_description": "friends in Philadelphia"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
@@ -1,34 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, field_validator
|
||||
import instructor
|
||||
|
||||
|
||||
class Receipt(BaseModel):
|
||||
item: str
|
||||
price: Decimal
|
||||
|
||||
@field_validator("price", mode="before")
|
||||
@classmethod
|
||||
def parse_price(cls, v):
|
||||
if isinstance(v, str):
|
||||
return Decimal(v)
|
||||
return v
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
client = instructor.from_provider("openai/gpt-4.1-mini")
|
||||
|
||||
receipt = client.chat.completions.create(
|
||||
messages=[{"role": "user", "content": "Coffee costs $4.99"}],
|
||||
response_model=Receipt,
|
||||
)
|
||||
|
||||
print(f"Item: {receipt.item}")
|
||||
print(f"Price: {receipt.price}") # Decimal('4.99')
|
||||
print(f"Type: {type(receipt.price)}") # <class 'decimal.Decimal'>
|
||||
|
||||
# Test precision
|
||||
total = receipt.price * 2
|
||||
print(f"Total for 2 items: {total}") # Decimal('9.98')
|
||||
@@ -0,0 +1,10 @@
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(540, b=677, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 540,\n \"b\": 677,\n \"result\": 1217\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(798, b=534, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 798,\n \"b\": 534,\n \"result\": 1332\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(608, b=669, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 608,\n \"b\": 669,\n \"result\": 1277\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(982, b=768, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 982,\n \"b\": 768,\n \"result\": 1750\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(994, b=682, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 994,\n \"b\": 682,\n \"result\": 1676\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(467, b=754, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 467,\n \"b\": 754,\n \"result\": 1221\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(497, b=364, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 497,\n \"b\": 364,\n \"result\": 861\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(840, b=821, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 840,\n \"b\": 821,\n \"result\": 1661\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(646, b=835, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 646,\n \"b\": 835,\n \"result\": 1481\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(926, b=196, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 926,\n \"b\": 196,\n \"result\": 1122\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
@@ -1,4 +0,0 @@
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(540, b=677, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 540,\n \"b\": 677,\n \"result\": 1217\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(798, b=534, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 798,\n \"b\": 534,\n \"result\": 1332\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(608, b=669, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 608,\n \"b\": 669,\n \"result\": 1277\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
{"messages": [{"role": "system", "content": "Predict the results of this function:\n\ndef fn(a: int, b: int, c: str) -> __main__.Multiply\n\"\"\"\n_summary_\n\nArgs:\n a (int): _description_\n b (int): _description_\n c (str): _description_\n\nReturns:\n Response: _description_\n\"\"\""}, {"role": "user", "content": "Return `fn(982, b=768, c=\"hello\")`"}, {"role": "assistant", "function_call": {"name": "Multiply", "arguments": "{\n \"a\": 982,\n \"b\": 768,\n \"result\": 1750\n}"}}], "functions": [{"name": "Multiply", "description": "Correctly extracted `Multiply` with all the required parameters with correct types", "parameters": {"properties": {"a": {"type": "integer"}, "b": {"type": "integer"}, "result": {"description": "The result of the multiplication", "type": "integer"}}, "required": ["a", "b", "result"], "type": "object"}}]}
|
||||
@@ -1,47 +0,0 @@
|
||||
# What to Expect
|
||||
This script demonstrates how to use the `Instructor` library for fine-tuning a Python function that performs three-digit multiplication. It uses Pydantic for type validation and logging features to generate a fine-tuning dataset.
|
||||
|
||||
## How to Run
|
||||
|
||||
### Prerequisites
|
||||
- Python 3.9
|
||||
- `Instructor` library
|
||||
|
||||
### Steps
|
||||
1. **Install Dependencies**
|
||||
If you haven't already installed the required libraries, you can do so using pip:
|
||||
```
|
||||
pip install instructor pydantic
|
||||
```
|
||||
|
||||
2. **Set Up Logging**
|
||||
The script uses Python's built-in `logging` module to log the fine-tuning process. Ensure you have write permissions in the directory where the log file `math_finetunes.jsonl` will be saved.
|
||||
|
||||
3. **Run the Script**
|
||||
Navigate to the directory containing `script.py` and run it:
|
||||
```
|
||||
python three_digit_mul.py
|
||||
```
|
||||
|
||||
This will execute the script, running the function ten times with random three-digit numbers for multiplication. The function outputs and logs are saved in `math_finetunes.jsonl`.
|
||||
|
||||
4. **Fine-Tuning**
|
||||
Once you have the log file, you can run a fine-tuning job using the following `Instructor` CLI command:
|
||||
```
|
||||
instructor jobs create-from-file math_finetunes.jsonl
|
||||
```
|
||||
Wait for the fine-tuning job to complete.
|
||||
|
||||
If you have validation date you can run:
|
||||
|
||||
```
|
||||
instructor jobs create-from-file math_finetunes.jsonl --n-epochs 4 --validation-file math_finetunes_val.jsonl
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
That's it! You've successfully run the script and can now proceed to fine-tune your model.
|
||||
|
||||
### Dispatch
|
||||
|
||||
Once you have the model you can replace the model in `three_digit_mul_dispatch.py` with the model you just fine-tuned and run the script again. This time, the script will use the fine-tuned model to predict the output of the function.
|
||||
@@ -1,72 +0,0 @@
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import Instructions
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Usage
|
||||
instructions = Instructions(
|
||||
name="three_digit_multiply",
|
||||
finetune_format="messages",
|
||||
log_handlers=[
|
||||
logging.FileHandler("math_finetunes.jsonl"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class Multiply(BaseModel):
|
||||
a: int
|
||||
b: int
|
||||
result: int = Field(..., description="The result of the multiplication")
|
||||
|
||||
|
||||
@instructions.distil
|
||||
def fn(a: int, b: int) -> Multiply:
|
||||
"""Return the result of multiplying a and b together"""
|
||||
resp = a * b
|
||||
return Multiply(a=a, b=b, result=resp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
|
||||
log_lines = {
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": 'Predict the results of this function:\n\ndef fn(a: int, b: int) -> __main__.Multiply\n"""\nReturn the result of multiplying a and b together\n"""',
|
||||
},
|
||||
{"role": "user", "content": "Return `fn(169, b=166)`"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"function_call": {
|
||||
"name": "Multiply",
|
||||
"arguments": '{\n "a": 169,\n "b": 166,\n "result": 28054\n}',
|
||||
},
|
||||
},
|
||||
],
|
||||
"functions": [
|
||||
{
|
||||
"name": "Multiply",
|
||||
"description": "Correctly extracted `Multiply` with all the required parameters with correct types",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"a": {"title": "A", "type": "integer"},
|
||||
"b": {"title": "B", "type": "integer"},
|
||||
"result": {
|
||||
"description": "The result of the multiplication",
|
||||
"title": "Result",
|
||||
"type": "integer",
|
||||
},
|
||||
},
|
||||
"required": ["a", "b", "result"],
|
||||
"type": "object",
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
for _ in range(10):
|
||||
a = random.randint(100, 999)
|
||||
b = random.randint(100, 999)
|
||||
print("returning", fn(a, b=b))
|
||||
@@ -1,51 +0,0 @@
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import Instructions
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Usage
|
||||
instructions = Instructions(
|
||||
name="three_digit_multiply",
|
||||
finetune_format="messages",
|
||||
include_code_body=True,
|
||||
log_handlers=[
|
||||
logging.FileHandler("math_finetunes.jsonl"),
|
||||
],
|
||||
openai_client=client,
|
||||
)
|
||||
|
||||
|
||||
class Multiply(BaseModel):
|
||||
a: int
|
||||
b: int
|
||||
result: int = Field(..., description="The result of the multiplication")
|
||||
|
||||
|
||||
@instructions.distil(mode="dispatch", model="ft:gpt-3.5-turbo-0125:personal::9i1JeuxJ")
|
||||
def fn(a: int, b: int) -> Multiply:
|
||||
"""Return the result of the multiplication as an integer"""
|
||||
resp = a * b
|
||||
return Multiply(a=a, b=b, result=resp)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import random
|
||||
|
||||
for _ in range(5):
|
||||
a = random.randint(100, 999)
|
||||
b = random.randint(100, 999)
|
||||
result = fn(a, b)
|
||||
print(f"{a} * {b} = {result.result}, expected {a * b}")
|
||||
"""
|
||||
972 * 508 = 493056, expected 493776
|
||||
145 * 369 = 53505, expected 53505
|
||||
940 * 440 = 413600, expected 413600
|
||||
114 * 213 = 24282, expected 24282
|
||||
259 * 650 = 168350, expected 168350
|
||||
"""
|
||||
@@ -1,146 +0,0 @@
|
||||
from collections import Counter, defaultdict
|
||||
from enum import Enum
|
||||
from typing import Any, Union
|
||||
import numpy as np
|
||||
import json
|
||||
from pydantic import ValidationError
|
||||
from pprint import pprint
|
||||
import models as m
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
IS_JSON = "_is_json_"
|
||||
IS_VALID = "_is_valid_"
|
||||
VALIDATION_ERROR = "_validation_error_"
|
||||
|
||||
|
||||
class StreamingAccumulatorManager:
|
||||
def __init__(self):
|
||||
self.accumulator = defaultdict(StreamingAccumulator)
|
||||
|
||||
def validate_string(self, json_string: str, index: int) -> None:
|
||||
try:
|
||||
obj = json.loads(json_string)
|
||||
self.accumulator[Status.IS_JSON.value].update(index, True)
|
||||
try:
|
||||
# Replace this line with your validation logic
|
||||
obj = m.MultiSearch.model_validate(obj)
|
||||
self.update(index, obj.model_dump())
|
||||
self.accumulator[Status.IS_VALID.value].update(index, True)
|
||||
except ValidationError as e:
|
||||
self.accumulator[Status.IS_VALID.value].update(index, False)
|
||||
self.process_validation_error(e, index)
|
||||
except json.JSONDecodeError:
|
||||
self.accumulator[Status.IS_JSON.value].update(index, False)
|
||||
|
||||
def process_validation_error(self, error, index):
|
||||
for err in error.errors():
|
||||
path = (
|
||||
"$."
|
||||
+ ".".join(
|
||||
[str(x) if not isinstance(x, int) else "[*]" for x in err["loc"]]
|
||||
)
|
||||
+ "."
|
||||
+ err["type"]
|
||||
)
|
||||
self.accumulator[Status.VALIDATION_ERROR.value].update(index, path)
|
||||
|
||||
def update(self, index, data: Any, path: str = "$") -> None:
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
new_path = f"{path}.{key}"
|
||||
self.update(index, value, new_path)
|
||||
elif isinstance(data, list):
|
||||
new_path = f"{path}[*]"
|
||||
for value in data:
|
||||
self.update(index, value, new_path)
|
||||
length_path = f"{path}.length"
|
||||
self.accumulator[length_path].update(index, len(data))
|
||||
elif isinstance(data, Enum):
|
||||
enum_path = f"{path}.enum"
|
||||
self.accumulator[enum_path].update(index, data.value)
|
||||
else:
|
||||
self.accumulator[path].update(index, data)
|
||||
|
||||
def summarize(self) -> dict[str, dict]:
|
||||
return {k: v.summarize(key_name=k) for k, v in self.accumulator.items()}
|
||||
|
||||
|
||||
class StreamingAccumulator:
|
||||
def __init__(self):
|
||||
self.counter = Counter()
|
||||
self.min = float("inf")
|
||||
self.max = float("-inf")
|
||||
self.sum = 0
|
||||
self.squared_sum = 0
|
||||
self.unique_values = set()
|
||||
self.missing_values = 0
|
||||
self.str_min_length = float("inf")
|
||||
self.str_max_length = float("-inf")
|
||||
self.str_sum_length = 0
|
||||
self.str_squared_sum_length = 0
|
||||
self.value = []
|
||||
self.str_length = []
|
||||
self.reverse_lookup = defaultdict(list)
|
||||
|
||||
def update(self, index: Any, value: Any) -> None:
|
||||
if isinstance(value, (int, str, bool)):
|
||||
self.counter[value] += 1
|
||||
self.unique_values.add(value)
|
||||
self.value.append(value)
|
||||
self.reverse_lookup[value].append(index)
|
||||
if value is None or value == "":
|
||||
self.missing_values += 1
|
||||
return
|
||||
if isinstance(value, (int, float)):
|
||||
self.min = min(self.min, value)
|
||||
self.max = max(self.max, value)
|
||||
self.sum += value
|
||||
self.squared_sum += value**2
|
||||
if isinstance(value, str):
|
||||
str_len = len(value)
|
||||
self.str_length.append(str_len)
|
||||
self.str_min_length = min(self.str_min_length, str_len)
|
||||
self.str_max_length = max(self.str_max_length, str_len)
|
||||
self.str_sum_length += str_len
|
||||
self.str_squared_sum_length += str_len**2
|
||||
|
||||
def summarize(self, key_name=None) -> dict[str, Union[int, float, dict]]:
|
||||
if key_name is None:
|
||||
key_name = ""
|
||||
n = sum(self.counter.values())
|
||||
summaries = {}
|
||||
summaries["counter"] = self.counter
|
||||
summaries["unique_count"] = len(self.unique_values)
|
||||
summaries["missing_values"] = self.missing_values
|
||||
summaries["_reverse_lookup"] = dict(self.reverse_lookup)
|
||||
if n > 0:
|
||||
if all(isinstance(value, (bool)) for value in self.unique_values):
|
||||
summaries["mean"] = self.sum / n
|
||||
return summaries
|
||||
if all(isinstance(value, (int, float)) for value in self.unique_values):
|
||||
summaries["min"] = self.min
|
||||
summaries["max"] = self.max
|
||||
summaries["mean"] = self.sum / n
|
||||
summaries["std"] = np.sqrt(self.squared_sum / n - (self.sum / n) ** 2)
|
||||
return summaries
|
||||
if all(isinstance(value, str) for value in self.unique_values):
|
||||
summaries["str_min_length"] = self.str_min_length
|
||||
summaries["str_max_length"] = self.str_max_length
|
||||
summaries["str_mean_length"] = self.str_sum_length / n
|
||||
summaries["str_std_length"] = np.sqrt(
|
||||
self.str_squared_sum_length / n - (self.str_sum_length / n) ** 2
|
||||
)
|
||||
return summaries
|
||||
return summaries
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
eval_manager = StreamingAccumulatorManager()
|
||||
|
||||
with open("test.jsonl") as f:
|
||||
lines = f.readlines()
|
||||
for ii, line in enumerate(lines):
|
||||
eval_manager.validate_string(line, ii)
|
||||
|
||||
pprint(eval_manager.summarize())
|
||||
@@ -1,24 +0,0 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class SourceType(str, Enum):
|
||||
CRM = "CRM"
|
||||
WEB = "WEB"
|
||||
EMAIL = "EMAIL"
|
||||
SOCIAL_MEDIA = "SOCIAL_MEDIA"
|
||||
OTHER = "OTHER"
|
||||
|
||||
|
||||
class Search(BaseModel):
|
||||
query: str
|
||||
source_type: SourceType
|
||||
results_limit: Optional[int] = Field(10)
|
||||
is_priority: Optional[bool] = None
|
||||
tags: Optional[list[str]] = None
|
||||
|
||||
|
||||
class MultiSearch(BaseModel):
|
||||
queries: list[Search]
|
||||
user_id: Optional[str]
|
||||
@@ -1,224 +0,0 @@
|
||||
from collections import Counter
|
||||
|
||||
stats_dict = {
|
||||
"$.queries.length": {
|
||||
"_reverse_lookup": {
|
||||
1: [0, 1, 8, 9, 10, 13, 14, 15],
|
||||
2: [7, 11, 16],
|
||||
3: [12, 17],
|
||||
},
|
||||
"counter": Counter({1: 8, 2: 3, 3: 2}),
|
||||
"max": 3,
|
||||
"mean": 1.5384615384615385,
|
||||
"min": 1,
|
||||
"missing_values": 0,
|
||||
"std": 0.7457969011409735,
|
||||
"unique_count": 3,
|
||||
},
|
||||
"$.queries[*].is_priority": {
|
||||
"_reverse_lookup": {False: [13], True: [1, 9, 14, 17]},
|
||||
"counter": Counter({True: 4, False: 1}),
|
||||
"mean": 0.8,
|
||||
"missing_values": 15,
|
||||
"unique_count": 2,
|
||||
},
|
||||
"$.queries[*].query": {
|
||||
"_reverse_lookup": {
|
||||
"customer churn": [1],
|
||||
"customer feedback": [15],
|
||||
"customer satisfaction": [11],
|
||||
"email campaigns": [12],
|
||||
"email open rates": [17],
|
||||
"email outreach": [10],
|
||||
"marketing strategies": [14],
|
||||
"new products": [16],
|
||||
"product sales": [11],
|
||||
"revenue 2022": [9],
|
||||
"revenue streams": [16],
|
||||
"sales Q1": [0, 7, 8, 13],
|
||||
"sales Q2": [7],
|
||||
"social impact": [12],
|
||||
"social trends": [17],
|
||||
"web traffic": [12],
|
||||
"website analytics": [17],
|
||||
},
|
||||
"counter": Counter(
|
||||
{
|
||||
"sales Q1": 4,
|
||||
"customer churn": 1,
|
||||
"sales Q2": 1,
|
||||
"revenue 2022": 1,
|
||||
"email outreach": 1,
|
||||
"product sales": 1,
|
||||
"customer satisfaction": 1,
|
||||
"social impact": 1,
|
||||
"email campaigns": 1,
|
||||
"web traffic": 1,
|
||||
"marketing strategies": 1,
|
||||
"customer feedback": 1,
|
||||
"revenue streams": 1,
|
||||
"new products": 1,
|
||||
"social trends": 1,
|
||||
"email open rates": 1,
|
||||
"website analytics": 1,
|
||||
}
|
||||
),
|
||||
"missing_values": 0,
|
||||
"str_max_length": 21,
|
||||
"str_mean_length": 13.15,
|
||||
"str_min_length": 8,
|
||||
"str_std_length": 3.8376425054973518,
|
||||
"unique_count": 17,
|
||||
},
|
||||
"$.queries[*].results_limit": {
|
||||
"_reverse_lookup": {
|
||||
5: [17],
|
||||
10: [0, 1, 7, 7, 8, 9, 10, 11, 11, 12, 12, 12, 13, 15, 16, 16, 17, 17],
|
||||
15: [14],
|
||||
},
|
||||
"counter": Counter({10: 18, 15: 1, 5: 1}),
|
||||
"max": 15,
|
||||
"mean": 10.0,
|
||||
"min": 5,
|
||||
"missing_values": 0,
|
||||
"std": 1.5811388300841898,
|
||||
"unique_count": 3,
|
||||
},
|
||||
"$.queries[*].source_type.enum": {
|
||||
"_reverse_lookup": {
|
||||
"CRM": [0, 7, 8, 11, 13, 16],
|
||||
"EMAIL": [10, 11, 12, 15, 17],
|
||||
"SOCIAL_MEDIA": [12, 17],
|
||||
"WEB": [1, 7, 9, 12, 14, 16, 17],
|
||||
},
|
||||
"counter": Counter({"WEB": 7, "CRM": 6, "EMAIL": 5, "SOCIAL_MEDIA": 2}),
|
||||
"missing_values": 0,
|
||||
"str_max_length": 12,
|
||||
"str_mean_length": 4.4,
|
||||
"str_min_length": 3,
|
||||
"str_std_length": 2.672077843177477,
|
||||
"unique_count": 4,
|
||||
},
|
||||
"$.queries[*].tags": {
|
||||
"_reverse_lookup": {},
|
||||
"counter": Counter(),
|
||||
"missing_values": 16,
|
||||
"unique_count": 0,
|
||||
},
|
||||
"$.queries[*].tags.length": {
|
||||
"_reverse_lookup": {1: [15, 17], 2: [10, 14]},
|
||||
"counter": Counter({2: 2, 1: 2}),
|
||||
"max": 2,
|
||||
"mean": 1.5,
|
||||
"min": 1,
|
||||
"missing_values": 0,
|
||||
"std": 0.5,
|
||||
"unique_count": 2,
|
||||
},
|
||||
"$.queries[*].tags[*]": {
|
||||
"_reverse_lookup": {
|
||||
"2022": [10],
|
||||
"2023": [14],
|
||||
"analytics": [17],
|
||||
"feedback": [15],
|
||||
"outreach": [10],
|
||||
"strategy": [14],
|
||||
},
|
||||
"counter": Counter(
|
||||
{
|
||||
"outreach": 1,
|
||||
"2022": 1,
|
||||
"strategy": 1,
|
||||
"2023": 1,
|
||||
"feedback": 1,
|
||||
"analytics": 1,
|
||||
}
|
||||
),
|
||||
"missing_values": 0,
|
||||
"str_max_length": 9,
|
||||
"str_mean_length": 6.833333333333333,
|
||||
"str_min_length": 4,
|
||||
"str_std_length": 2.034425935955618,
|
||||
"unique_count": 6,
|
||||
},
|
||||
"$.user_id": {
|
||||
"_reverse_lookup": {
|
||||
"user_1": [0],
|
||||
"user_10": [10],
|
||||
"user_11": [11],
|
||||
"user_12": [12],
|
||||
"user_13": [13],
|
||||
"user_14": [14],
|
||||
"user_15": [15],
|
||||
"user_16": [16],
|
||||
"user_17": [17],
|
||||
"user_2": [1],
|
||||
"user_7": [7],
|
||||
"user_8": [8],
|
||||
"user_9": [9],
|
||||
},
|
||||
"counter": Counter(
|
||||
{
|
||||
"user_1": 1,
|
||||
"user_2": 1,
|
||||
"user_7": 1,
|
||||
"user_8": 1,
|
||||
"user_9": 1,
|
||||
"user_10": 1,
|
||||
"user_11": 1,
|
||||
"user_12": 1,
|
||||
"user_13": 1,
|
||||
"user_14": 1,
|
||||
"user_15": 1,
|
||||
"user_16": 1,
|
||||
"user_17": 1,
|
||||
}
|
||||
),
|
||||
"missing_values": 0,
|
||||
"str_max_length": 7,
|
||||
"str_mean_length": 6.615384615384615,
|
||||
"str_min_length": 6,
|
||||
"str_std_length": 0.48650425541052295,
|
||||
"unique_count": 13,
|
||||
},
|
||||
"_is_json_": {
|
||||
"_reverse_lookup": {
|
||||
False: [2, 4],
|
||||
True: [0, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
},
|
||||
"counter": Counter({True: 16, False: 2}),
|
||||
"mean": 0.8888888888888888,
|
||||
"missing_values": 0,
|
||||
"unique_count": 2,
|
||||
},
|
||||
"_is_valid_": {
|
||||
"_reverse_lookup": {
|
||||
False: [3, 5, 6],
|
||||
True: [0, 1, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
|
||||
},
|
||||
"counter": Counter({True: 13, False: 3}),
|
||||
"mean": 0.8125,
|
||||
"missing_values": 0,
|
||||
"unique_count": 2,
|
||||
},
|
||||
"_validation_error_": {
|
||||
"_reverse_lookup": {
|
||||
"$.queries.[*].is_priority.bool_parsing": [6],
|
||||
"$.queries.[*].source_type.enum": [3],
|
||||
"$.user_id.missing": [5],
|
||||
},
|
||||
"counter": Counter(
|
||||
{
|
||||
"$.queries.[*].source_type.enum": 1,
|
||||
"$.user_id.missing": 1,
|
||||
"$.queries.[*].is_priority.bool_parsing": 1,
|
||||
}
|
||||
),
|
||||
"missing_values": 0,
|
||||
"str_max_length": 38,
|
||||
"str_mean_length": 28.333333333333332,
|
||||
"str_min_length": 17,
|
||||
"str_std_length": 8.653836657164781,
|
||||
"unique_count": 3,
|
||||
},
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import streamlit as st
|
||||
from stats_dict import stats_dict
|
||||
|
||||
# Sample data
|
||||
query_data = {i: line.strip() for i, line in enumerate(open("test.jsonl"))}
|
||||
|
||||
# Initialize selected keys
|
||||
selected_keys = {}
|
||||
|
||||
|
||||
# Function to get lines
|
||||
def get_lines(stats_key, keys):
|
||||
indices = []
|
||||
for key in keys:
|
||||
indices.extend(stats_dict[stats_key]["_reverse_lookup"][key])
|
||||
return "\n".join([query_data[i] for i in indices])
|
||||
|
||||
|
||||
# Function to render dropdown and button
|
||||
def render_dropdown_and_button(stats_key):
|
||||
st.subheader(f"Stats for `{stats_key}`")
|
||||
st.json(stats_dict[stats_key]["counter"])
|
||||
st.json(
|
||||
{k: v for k, v in stats_dict[stats_key].items() if isinstance(v, (int, float))}
|
||||
)
|
||||
st.subheader("Histogram")
|
||||
st.bar_chart(stats_dict[stats_key]["counter"], use_container_width=True)
|
||||
|
||||
options = list(stats_dict[stats_key]["counter"].keys())
|
||||
selected_keys[stats_key] = st.multiselect(
|
||||
f"View samples with {stats_key}",
|
||||
options,
|
||||
default=selected_keys.get(stats_key, []),
|
||||
)
|
||||
st.code(get_lines(stats_key, selected_keys[stats_key]))
|
||||
|
||||
|
||||
# Sidebar for navigation
|
||||
st.sidebar.title("Navigation")
|
||||
page = st.sidebar.selectbox(
|
||||
"Select a page:",
|
||||
["Validation Stats", "Individual Path Views"],
|
||||
)
|
||||
|
||||
# Main Streamlit App
|
||||
st.title("Structured Output Evaluation")
|
||||
|
||||
# Validation Stats
|
||||
if page == "Validation Stats":
|
||||
st.header("Validation Stats")
|
||||
for key in [k for k in stats_dict.keys() if k.startswith("_")]:
|
||||
render_dropdown_and_button(key)
|
||||
|
||||
# Individual Path Views
|
||||
elif page == "Individual Path Views":
|
||||
st.header("Individual Path Views")
|
||||
path = st.selectbox(
|
||||
"Choose a path:",
|
||||
[key for key in stats_dict.keys() if not key.startswith("_")],
|
||||
)
|
||||
if "counter" in stats_dict[path]:
|
||||
render_dropdown_and_button(path)
|
||||
@@ -1,18 +0,0 @@
|
||||
{"queries": [{"query": "sales Q1", "source_type": "CRM"}], "user_id": "user_1"}
|
||||
{"queries": [{"query": "customer churn", "source_type": "WEB", "is_priority": true}], "user_id": "user_2", "total_queries": 1}
|
||||
{"queries": ["query": "email campaigns", "source_type": "EMAIL"}, {"query": "social ads", "source_type": "SOCIAL_MEDIA"}], "user_id": "user_3", "total_queries": 2}
|
||||
{"queries": [{"query": "sales Q2", "source_type": "INVALID_ENUM"}], "user_id": "user_4"}
|
||||
{queries: [{"query": "sales Q3", "source_type": "CRM"}], "user_id": "user_5"}
|
||||
{"queries": [{"query": "sales Q4", "source_type": "CRM", "timestamp": "2023-09-10T12:00:00Z"}], "total_queries": 1}
|
||||
{"queries": [{"query": "customer retention", "source_type": "EMAIL", "is_priority": "should_be_bool"}], "user_id": "user_6"}
|
||||
{"queries": [{"query": "sales Q1", "source_type": "CRM"}, {"query": "sales Q2", "source_type": "WEB"}], "user_id": "user_7", "total_queries": 2}
|
||||
{"queries": [{"query": "sales Q1", "source_type": "CRM", "timestamp": "2023-09-10T12:00:00Z"}], "user_id": "user_8", "total_queries": 1}
|
||||
{"queries": [{"query": "revenue 2022", "source_type": "WEB", "results_limit": 10, "is_priority": true}], "user_id": "user_9", "total_queries": 1}
|
||||
{"queries": [{"query": "email outreach", "source_type": "EMAIL", "tags": ["outreach", "2022"]}], "user_id": "user_10", "total_queries": 1}
|
||||
{"queries": [{"query": "product sales", "source_type": "CRM"}, {"query": "customer satisfaction", "source_type": "EMAIL"}], "user_id": "user_11", "total_queries": 2}
|
||||
{"queries": [{"query": "social impact", "source_type": "SOCIAL_MEDIA"}, {"query": "email campaigns", "source_type": "EMAIL"}, {"query": "web traffic", "source_type": "WEB"}], "user_id": "user_12", "total_queries": 3}
|
||||
{"queries": [{"query": "sales Q1", "source_type": "CRM", "is_priority": false}], "user_id": "user_13", "total_queries": 1}
|
||||
{"queries": [{"query": "marketing strategies", "source_type": "WEB", "results_limit": 15, "is_priority": true, "tags": ["strategy", "2023"]}], "user_id": "user_14", "total_queries": 1}
|
||||
{"queries": [{"query": "customer feedback", "source_type": "EMAIL", "tags": ["feedback"]}], "user_id": "user_15", "total_queries": 1}
|
||||
{"queries": [{"query": "revenue streams", "source_type": "CRM"}, {"query": "new products", "source_type": "WEB"}], "user_id": "user_16", "total_queries": 2}
|
||||
{"queries": [{"query": "social trends", "source_type": "SOCIAL_MEDIA", "is_priority": true}, {"query": "email open rates", "source_type": "EMAIL", "results_limit": 5}, {"query": "website analytics", "source_type": "WEB", "tags": ["analytics"]}], "user_id": "user_17", "total_queries": 3}
|
||||
@@ -1,152 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from io import StringIO
|
||||
from typing import Annotated, Any
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
PlainSerializer,
|
||||
InstanceOf,
|
||||
WithJsonSchema,
|
||||
)
|
||||
import instructor
|
||||
import pandas as pd
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
client = instructor.from_openai(
|
||||
client=OpenAI(),
|
||||
mode=instructor.Mode.TOOLS,
|
||||
)
|
||||
|
||||
|
||||
def md_to_df(data: Any) -> Any:
|
||||
if isinstance(data, str):
|
||||
return (
|
||||
pd.read_csv(
|
||||
StringIO(data), # Get rid of whitespaces
|
||||
sep="|",
|
||||
index_col=1,
|
||||
)
|
||||
.dropna(axis=1, how="all")
|
||||
.iloc[1:]
|
||||
.map(lambda x: x.strip())
|
||||
) # type: ignore
|
||||
return data
|
||||
|
||||
|
||||
MarkdownDataFrame = Annotated[
|
||||
InstanceOf[pd.DataFrame],
|
||||
BeforeValidator(md_to_df),
|
||||
PlainSerializer(lambda x: x.to_markdown()),
|
||||
WithJsonSchema(
|
||||
{
|
||||
"type": "string",
|
||||
"description": """
|
||||
The markdown representation of the table,
|
||||
each one should be tidy, do not try to join tables
|
||||
that should be separate""",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Table(BaseModel):
|
||||
caption: str
|
||||
dataframe: MarkdownDataFrame
|
||||
|
||||
|
||||
class MultipleTables(BaseModel):
|
||||
tables: list[Table]
|
||||
|
||||
|
||||
example = MultipleTables(
|
||||
tables=[
|
||||
Table(
|
||||
caption="This is a caption",
|
||||
dataframe=pd.DataFrame(
|
||||
{
|
||||
"Chart A": [10, 40],
|
||||
"Chart B": [20, 50],
|
||||
"Chart C": [30, 60],
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def extract(url: str) -> MultipleTables:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4-turbo",
|
||||
max_tokens=4000,
|
||||
response_model=MultipleTables,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": """
|
||||
First, analyze the image to determine the most appropriate headers for the tables.
|
||||
Generate a descriptive h1 for the overall image, followed by a brief summary of the data it contains.
|
||||
For each identified table, create an informative h2 title and a concise description of its contents.
|
||||
Finally, output the markdown representation of each table.
|
||||
|
||||
|
||||
Make sure to escape the markdown table properly, and make sure to include the caption and the dataframe.
|
||||
including escaping all the newlines and quotes. Only return a markdown table in dataframe, nothing else.
|
||||
""",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
urls = [
|
||||
"https://a.storyblok.com/f/47007/2400x1260/f816b031cb/uk-ireland-in-three-charts_chart_a.png/m/2880x0",
|
||||
"https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png/m/2880x0",
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
for table in extract(url).tables:
|
||||
console.print(table.caption, "\n", table.dataframe)
|
||||
"""
|
||||
Growth in app installations and sessions across different app categories in Q3 2022 compared to Q2 2022 for Ireland and U.K.
|
||||
Install Growth (%) Session Growth (%)
|
||||
Category
|
||||
Education 7 6
|
||||
Games 13 3
|
||||
Social 4 -3
|
||||
Utilities 6 -0.4
|
||||
Top 10 Grossing Android Apps in Ireland, October 2023
|
||||
App Name Category
|
||||
Rank
|
||||
1 Google One Productivity
|
||||
2 Disney+ Entertainment
|
||||
3 TikTok - Videos, Music & LIVE Entertainment
|
||||
4 Candy Crush Saga Games
|
||||
5 Tinder: Dating, Chat & Friends Social networking
|
||||
6 Coin Master Games
|
||||
7 Roblox Games
|
||||
8 Bumble - Dating & Make Friends Dating
|
||||
9 Royal Match Games
|
||||
10 Spotify: Music and Podcasts Music & Audio
|
||||
Top 10 Grossing iOS Apps in Ireland, October 2023
|
||||
App Name Category
|
||||
Rank
|
||||
1 Tinder: Dating, Chat & Friends Social networking
|
||||
2 Disney+ Entertainment
|
||||
3 YouTube: Watch, Listen, Stream Entertainment
|
||||
4 Audible: Audio Entertainment Entertainment
|
||||
5 Candy Crush Saga Games
|
||||
6 TikTok - Videos, Music & LIVE Entertainment
|
||||
7 Bumble - Dating & Make Friends Dating
|
||||
8 Roblox Games
|
||||
9 LinkedIn: Job Search & News Business
|
||||
10 Duolingo - Language Lessons Education
|
||||
"""
|
||||
@@ -1,126 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from io import StringIO
|
||||
from typing import Annotated, Any
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
PlainSerializer,
|
||||
InstanceOf,
|
||||
WithJsonSchema,
|
||||
)
|
||||
import instructor
|
||||
import pandas as pd
|
||||
from langsmith.wrappers import wrap_openai
|
||||
from langsmith import traceable
|
||||
|
||||
|
||||
client = wrap_openai(OpenAI())
|
||||
client = instructor.from_openai(
|
||||
client, mode=instructor.processing.function_calls.Mode.MD_JSON
|
||||
)
|
||||
|
||||
|
||||
def md_to_df(data: Any) -> Any:
|
||||
if isinstance(data, str):
|
||||
return (
|
||||
pd.read_csv(
|
||||
StringIO(data), # Get rid of whitespaces
|
||||
sep="|",
|
||||
index_col=1,
|
||||
)
|
||||
.dropna(axis=1, how="all")
|
||||
.iloc[1:]
|
||||
.map(lambda x: x.strip())
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
MarkdownDataFrame = Annotated[
|
||||
InstanceOf[pd.DataFrame],
|
||||
BeforeValidator(md_to_df),
|
||||
PlainSerializer(lambda x: x.to_markdown()),
|
||||
WithJsonSchema(
|
||||
{
|
||||
"type": "string",
|
||||
"description": """
|
||||
The markdown representation of the table,
|
||||
each one should be tidy, do not try to join tables
|
||||
that should be separate""",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Table(BaseModel):
|
||||
caption: str
|
||||
dataframe: MarkdownDataFrame
|
||||
|
||||
|
||||
class MultipleTables(BaseModel):
|
||||
tables: list[Table]
|
||||
|
||||
|
||||
example = MultipleTables(
|
||||
tables=[
|
||||
Table(
|
||||
caption="This is a caption",
|
||||
dataframe=pd.DataFrame(
|
||||
{
|
||||
"Chart A": [10, 40],
|
||||
"Chart B": [20, 50],
|
||||
"Chart C": [30, 60],
|
||||
}
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@traceable(name="extract-table")
|
||||
def extract(url: str) -> MultipleTables:
|
||||
tables = client.chat.completions.create(
|
||||
model="gpt-4-vision-preview",
|
||||
max_tokens=4000,
|
||||
response_model=MultipleTables,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": f"Describe this data accurately as a table in markdown format. {example.model_dump_json(indent=2)}",
|
||||
},
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": """
|
||||
First take a moment to reason about the best set of headers for the tables.
|
||||
Write a good h1 for the image above. Then follow up with a short description of the what the data is about.
|
||||
Then for each table you identified, write a h2 tag that is a descriptive title of the table.
|
||||
Then follow up with a short description of the what the data is about.
|
||||
Lastly, produce the markdown table for each table you identified.
|
||||
|
||||
|
||||
Make sure to escape the markdown table properly, and make sure to include the caption and the dataframe.
|
||||
including escaping all the newlines and quotes. Only return a markdown table in dataframe, nothing else.
|
||||
""",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
return tables.model_dump()
|
||||
|
||||
|
||||
urls = [
|
||||
"https://a.storyblok.com/f/47007/2400x1260/f816b031cb/uk-ireland-in-three-charts_chart_a.png/m/2880x0",
|
||||
"https://a.storyblok.com/f/47007/2400x2000/bf383abc3c/231031_uk-ireland-in-three-charts_table_v01_b.png/m/2880x0",
|
||||
]
|
||||
|
||||
|
||||
for url in urls:
|
||||
tables = extract(url)
|
||||
print(tables)
|
||||
@@ -1,89 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel, Field
|
||||
from rich.console import Console
|
||||
|
||||
import instructor
|
||||
|
||||
console = Console()
|
||||
client = instructor.from_openai(
|
||||
client=OpenAI(),
|
||||
mode=instructor.Mode.TOOLS,
|
||||
)
|
||||
|
||||
|
||||
class People(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
role: str
|
||||
reports: list[str] = Field(
|
||||
default_factory=list, description="People who report to this person"
|
||||
)
|
||||
manages: list[str] = Field(
|
||||
default_factory=list, description="People who this person manages"
|
||||
)
|
||||
|
||||
|
||||
class Organization(BaseModel):
|
||||
people: list[People]
|
||||
|
||||
|
||||
def extract(url: str):
|
||||
return client.chat.completions.create_partial(
|
||||
model="gpt-4-turbo",
|
||||
max_tokens=4000,
|
||||
response_model=Organization,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": """
|
||||
Analyze the organizational chart image and extract the relevant information to reconstruct the hierarchy.
|
||||
|
||||
Create a list of People objects, where each person has the following attributes:
|
||||
- id: A unique identifier for the person
|
||||
- name: The person's name
|
||||
- role: The person's role or position in the organization
|
||||
- reports: A list of IDs of people who report directly to this person
|
||||
- manages: A list of IDs of people who this person manages
|
||||
|
||||
Ensure that the relationships between people are accurately captured in the reports and manages attributes.
|
||||
|
||||
Return the list of People objects as the people attribute of an Organization object.
|
||||
""",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
console.print(
|
||||
extract(
|
||||
"https://www.mindmanager.com/static/mm/images/features/org-chart/hierarchical-chart.png"
|
||||
)
|
||||
)
|
||||
"""
|
||||
Organization(
|
||||
people=[
|
||||
People(id='A1', name='Adele Morana', role='Founder, Chairman & CEO', reports=[], manages=['B1', 'C1', 'D1']),
|
||||
People(id='B1', name='Winston Cole', role='COO', reports=['A1'], manages=['E1']),
|
||||
People(id='C1', name='Marcus Kim', role='CFO', reports=['A1'], manages=['F1']),
|
||||
People(id='D1', name='Karin Ludovicicus', role='CPO', reports=['A1'], manages=['G1']),
|
||||
People(id='E1', name='Lea Erastos', role='Chief Business Officer', reports=['B1'], manages=['H1', 'I1']),
|
||||
People(id='F1', name='John McKinley', role='Chief Accounting Officer', reports=['C1'], manages=[]),
|
||||
People(id='G1', name='Ayda Williams', role='VP, Global Customer & Business Marketing', reports=['D1'], manages=['J1', 'K1']),
|
||||
People(id='H1', name='Zahida Mahtab', role='VP, Global Affairs & Communication', reports=['E1'], manages=[]),
|
||||
People(id='I1', name='Adelaide Zhu', role='VP, Central Services', reports=['E1'], manages=[]),
|
||||
People(id='J1', name='Gabriel Drummond', role='VP, Investor Relations', reports=['G1'], manages=[]),
|
||||
People(id='K1', name='Nicholas Brambilla', role='VP, Company Brand', reports=['G1'], manages=[]),
|
||||
People(id='L1', name='Felice Vasili', role='VP Finance', reports=['C1'], manages=[]),
|
||||
People(id='M1', name='Sandra Herminius', role='VP, Product Marketing', reports=['D1'], manages=[])
|
||||
]
|
||||
)
|
||||
"""
|
||||
@@ -1,115 +0,0 @@
|
||||
from openai import OpenAI
|
||||
from io import StringIO
|
||||
from typing import Annotated, Any
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
BeforeValidator,
|
||||
PlainSerializer,
|
||||
InstanceOf,
|
||||
WithJsonSchema,
|
||||
)
|
||||
import instructor
|
||||
import pandas as pd
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
client = instructor.from_openai(
|
||||
client=OpenAI(),
|
||||
mode=instructor.Mode.TOOLS,
|
||||
)
|
||||
|
||||
|
||||
def md_to_df(data: Any) -> Any:
|
||||
if isinstance(data, str):
|
||||
return (
|
||||
pd.read_csv(
|
||||
StringIO(data), # Get rid of whitespaces
|
||||
sep="|",
|
||||
index_col=1,
|
||||
)
|
||||
.dropna(axis=1, how="all")
|
||||
.iloc[1:]
|
||||
.map(lambda x: x.strip())
|
||||
) # type: ignore
|
||||
return data
|
||||
|
||||
|
||||
MarkdownDataFrame = Annotated[
|
||||
InstanceOf[pd.DataFrame],
|
||||
BeforeValidator(md_to_df),
|
||||
PlainSerializer(lambda x: x.to_markdown()),
|
||||
WithJsonSchema(
|
||||
{
|
||||
"type": "string",
|
||||
"description": """
|
||||
The markdown representation of the table,
|
||||
each one should be tidy, do not try to join tables
|
||||
that should be separate""",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Table(BaseModel):
|
||||
caption: str
|
||||
dataframe: MarkdownDataFrame
|
||||
|
||||
|
||||
def extract(url: str):
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4-turbo",
|
||||
max_tokens=4000,
|
||||
response_model=Table,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": """
|
||||
Analyze the organizational chart image and extract the relevant information to reconstruct the hierarchy.
|
||||
|
||||
Create a list of People objects, where each person has the following attributes:
|
||||
- id: A unique identifier for the person
|
||||
- name: The person's name
|
||||
- role: The person's role or position in the organization
|
||||
- manager_name: The name of the person who manages this person
|
||||
- manager_role: The role of the person who manages this person
|
||||
|
||||
Ensure that the relationships between people are accurately captured in the reports and manages attributes.
|
||||
|
||||
Return the list of People objects as the people attribute of an Organization object.
|
||||
""",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
print(
|
||||
extract(
|
||||
"https://www.mindmanager.com/static/mm/images/features/org-chart/hierarchical-chart.png"
|
||||
).model_dump()["dataframe"]
|
||||
)
|
||||
"""
|
||||
| id | name | role | manager_name | manager_role |
|
||||
|-------:|:-------------------|:-----------------------------------------|:------------------|:-----------------------------|
|
||||
| 1 | Adele Morana | Founder, Chairman & CEO | | |
|
||||
| 2 | Winston Cole | COO | Adele Morana | Founder, Chairman & CEO |
|
||||
| 3 | Marcus Kim | CFO | Adele Morana | Founder, Chairman & CEO |
|
||||
| 4 | Karin Ludovicus | CPO | Adele Morana | Founder, Chairman & CEO |
|
||||
| 5 | Lea Erastos | Chief Business Officer | Winston Cole | COO |
|
||||
| 6 | John McKinley | Chief Accounting Officer | Winston Cole | COO |
|
||||
| 7 | Zahida Mahtab | VP, Global Affairs & Communication | Winston Cole | COO |
|
||||
| 8 | Adelaide Zhu | VP, Central Services | Winston Cole | COO |
|
||||
| 9 | Gabriel Drummond | VP, Investor Relations | Marcus Kim | CFO |
|
||||
| 10 | Felicie Vasili | VP, Finance | Marcus Kim | CFO |
|
||||
| 11 | Ayda Williams | VP, Global Customer & Business Marketing | Karin Ludovicius | CPO |
|
||||
| 12 | Nicholas Brambilla | VP, Company Brand | Karin Ludovicius | CPO |
|
||||
| 13 | Sandra Herminius | VP, Product Marketing | Karin Ludovicius | CPO |
|
||||
"""
|
||||
@@ -1,65 +0,0 @@
|
||||
from pydantic import BaseModel, model_validator
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
|
||||
client = instructor.from_openai(
|
||||
client=OpenAI(),
|
||||
mode=instructor.Mode.TOOLS,
|
||||
)
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
quantity: int
|
||||
|
||||
|
||||
class Receipt(BaseModel):
|
||||
items: list[Item]
|
||||
total: float
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_total(cls, values: "Receipt"):
|
||||
items = values.items
|
||||
total = values.total
|
||||
calculated_total = sum(item.price * item.quantity for item in items)
|
||||
if calculated_total != total:
|
||||
raise ValueError(
|
||||
f"Total {total} does not match the sum of item prices {calculated_total}"
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
def extract(url: str) -> Receipt:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o",
|
||||
max_tokens=4000,
|
||||
response_model=Receipt,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "image_url",
|
||||
"image_url": {"url": url},
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Analyze the image and return the items in the receipt and the total amount.",
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# URLs of images containing receipts. Exhibits the use of the model validator to check the total amount.
|
||||
urls = [
|
||||
"https://templates.mediamodifier.com/645124ff36ed2f5227cbf871/supermarket-receipt-template.jpg",
|
||||
"https://ocr.space/Content/Images/receipt-ocr-original.jpg",
|
||||
]
|
||||
|
||||
for url in urls:
|
||||
receipt = extract(url)
|
||||
print(receipt)
|
||||
@@ -1,106 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
client = instructor.from_openai(client)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
email: str
|
||||
|
||||
|
||||
class MeetingInfo(BaseModel):
|
||||
user: User
|
||||
date: str
|
||||
location: str
|
||||
budget: int
|
||||
deadline: str
|
||||
|
||||
|
||||
data = """
|
||||
Jason Liu jason@gmail.com
|
||||
Meeting Date: 2024-01-01
|
||||
Meeting Location: 1234 Main St
|
||||
Meeting Budget: $1000
|
||||
Meeting Deadline: 2024-01-31
|
||||
"""
|
||||
stream1 = client.chat.completions.create_partial(
|
||||
model="gpt-4",
|
||||
response_model=MeetingInfo,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Get the information about the meeting and the users {data}",
|
||||
},
|
||||
],
|
||||
stream=True,
|
||||
) # type: ignore
|
||||
|
||||
for message in stream1:
|
||||
print(message)
|
||||
"""
|
||||
ser={} date=None location=None budget=None deadline=None
|
||||
user={} date=None location=None budget=None deadline=None
|
||||
user={} date=None location=None budget=None deadline=None
|
||||
user={} date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name=None, email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email=None) date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date=None location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location=None budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=None deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=100 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline=None
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline='2024-01-31'
|
||||
user=PartialUser(name='Jason Liu', email='jason@gmail.com') date='2024-01-01' location='1234 Main St' budget=1000 deadline='2024-01-31'
|
||||
"""
|
||||
@@ -1,118 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Data(BaseModel):
|
||||
index: int
|
||||
data_type: str
|
||||
pii_value: str
|
||||
|
||||
|
||||
class PIIDataExtraction(BaseModel):
|
||||
"""
|
||||
Extracted PII data from a document, all data_types should try to have consistent property names
|
||||
"""
|
||||
|
||||
private_data: list[Data]
|
||||
|
||||
def scrub_data(self, content):
|
||||
"""
|
||||
Iterates over the private data and replaces the value with a placeholder in the form of
|
||||
<{data_type}_{i}>
|
||||
"""
|
||||
|
||||
for i, data in enumerate(self.private_data):
|
||||
content = content.replace(data.pii_value, f"<{data.data_type}_{i}>")
|
||||
|
||||
return content
|
||||
|
||||
|
||||
EXAMPLE_DOCUMENT = """
|
||||
# Fake Document with PII for Testing PII Scrubbing Model
|
||||
|
||||
## Personal Story
|
||||
|
||||
John Doe was born on 01/02/1980. His social security number is 123-45-6789. He has been using the email address john.doe@email.com for years, and he can always be reached at 555-123-4567.
|
||||
|
||||
## Residence
|
||||
|
||||
John currently resides at 123 Main St, Springfield, IL, 62704. He's been living there for about 5 years now.
|
||||
|
||||
## Career
|
||||
|
||||
At the moment, John is employed at Company A. He started his role as a Software Engineer in January 2015 and has been with the company since then.
|
||||
"""
|
||||
|
||||
# Define the PII Scrubbing Model
|
||||
pii_data: PIIDataExtraction = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=PIIDataExtraction,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a world class PII scrubbing model, Extract the PII data from the following document",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": EXAMPLE_DOCUMENT,
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
print("Extracted PII Data:")
|
||||
print(pii_data.model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"private_data": [
|
||||
{
|
||||
"index": 0,
|
||||
"data_type": "date",
|
||||
"pii_value": "01/02/1980"
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"data_type": "ssn",
|
||||
"pii_value": "123-45-6789"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"data_type": "email",
|
||||
"pii_value": "john.doe@email.com"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"data_type": "phone",
|
||||
"pii_value": "555-123-4567"
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"data_type": "address",
|
||||
"pii_value": "123 Main St, Springfield, IL, 62704"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Scrub the PII Data from the document
|
||||
print("Scrubbed Document:")
|
||||
print(pii_data.scrub_data(EXAMPLE_DOCUMENT))
|
||||
"""
|
||||
# Fake Document with PII for Testing PII Scrubbing Model
|
||||
|
||||
## Personal Story
|
||||
|
||||
John Doe was born on <date_of_birth_0>. His social security number is <social_security_number_1>. He has been using the email address <email_address_2> for years, and he can always be reached at <phone_number_3>.
|
||||
|
||||
## Residence
|
||||
|
||||
John currently resides at <address_4>. He's been living there for about 5 years now.
|
||||
|
||||
## Career
|
||||
|
||||
At the moment, John is employed at <employment_5>. He started his role as a <job_title_6> in <employment_start_date_7> and has been with the company since then.
|
||||
"""
|
||||
@@ -1,42 +0,0 @@
|
||||
from fastapi import FastAPI
|
||||
from instructor import ResponseSchema
|
||||
import instructor.dsl as dsl
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
app = FastAPI(title="Example Application using instructor")
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
body: str
|
||||
|
||||
|
||||
class SearchQuery(ResponseSchema):
|
||||
title: str = Field(..., description="Question that the query answers")
|
||||
query: str = Field(
|
||||
...,
|
||||
description="Detailed, comprehensive, and specific query to be used for semantic search",
|
||||
)
|
||||
|
||||
|
||||
SearchResponse = dsl.MultiTask(
|
||||
subtask_class=SearchQuery,
|
||||
description="Correctly segmented set of search queries",
|
||||
)
|
||||
|
||||
|
||||
@app.post("/search", response_model=SearchResponse)
|
||||
async def search(request: SearchRequest):
|
||||
task = (
|
||||
dsl.ChatCompletion(name="Segmenting Search requests example")
|
||||
| dsl.SystemTask(task="Segment search results")
|
||||
| dsl.TaggedMessage(content=request.body, tag="query")
|
||||
| dsl.TipsMessage(
|
||||
tips=[
|
||||
"Expand query to contain multiple forms of the same word (SSO -> Single Sign On)",
|
||||
"Use the title to explain what the query should return, but use the query to complete the search",
|
||||
"The query should be detailed, specific, and cast a wide net when possible",
|
||||
]
|
||||
)
|
||||
| SearchRequest
|
||||
)
|
||||
return await task.acreate()
|
||||
@@ -1,49 +0,0 @@
|
||||
from instructor import ResponseSchema, dsl
|
||||
from pydantic import Field
|
||||
import json
|
||||
|
||||
|
||||
class SearchQuery(ResponseSchema):
|
||||
query: str = Field(
|
||||
...,
|
||||
description="Detailed, comprehensive, and specific query to be used for semantic search",
|
||||
)
|
||||
|
||||
|
||||
SearchResponse = dsl.MultiTask(
|
||||
subtask_class=SearchQuery,
|
||||
description="Correctly segmented set of search queries",
|
||||
)
|
||||
|
||||
|
||||
task = (
|
||||
dsl.ChatCompletion(name="Segmenting Search requests example")
|
||||
| dsl.SystemTask(task="Segment search results")
|
||||
| dsl.TaggedMessage(
|
||||
content="can you send me the data about the video investment and the one about spot the dog?",
|
||||
tag="query",
|
||||
)
|
||||
| dsl.TipsMessage(
|
||||
tips=[
|
||||
"Expand query to contain multiple forms of the same word (SSO -> Single Sign On)",
|
||||
"Use the title to explain what the query should return, but use the query to complete the search",
|
||||
"The query should be detailed, specific, and cast a wide net when possible",
|
||||
]
|
||||
)
|
||||
| SearchResponse
|
||||
)
|
||||
|
||||
|
||||
print(json.dumps(task.kwargs, indent=1))
|
||||
"""
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"query": "data about video investment"
|
||||
},
|
||||
{
|
||||
"query": "data about spot the dog"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
@@ -1,24 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
def fizzbuzz_gpt(n) -> list[int | str]:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=list[int | str],
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Return the first {n} numbers in fizzbuzz",
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(fizzbuzz_gpt(n=15))
|
||||
# > [1, 2, 'Fizz', 4, 'Buzz', 'Fizz', 7, 8, 'Fizz', 'Buzz', 11, 'Fizz', 13, 14, 'FizzBuzz']
|
||||
@@ -1,83 +0,0 @@
|
||||
--- readme.md
|
||||
+++ readme.md
|
||||
@@ -1,9 +1,9 @@
|
||||
# FastAPI App
|
||||
|
||||
-This is a FastAPI app that provides some basic math functions.
|
||||
+This is a Flask app that provides some basic math functions.
|
||||
|
||||
## Usage
|
||||
|
||||
To use this app, follow the instructions below:
|
||||
|
||||
1. Install the required dependencies by running `pip install -r requirements.txt`.
|
||||
-2. Start the app by running `uvicorn main:app --reload`.
|
||||
+2. Start the app by running `flask run`.
|
||||
3. Open your browser and navigate to `http://localhost:5000/docs` to access the Swagger UI documentation.
|
||||
|
||||
## Example
|
||||
|
||||
To perform a basic math operation, you can use the following curl command:
|
||||
|
||||
```bash
|
||||
-curl -X POST -H "Content-Type: application/json" -d '{"operation": "add", "operands": [2, 3]}' http://localhost:8000/calculate
|
||||
+curl -X POST -H "Content-Type: application/json" -d '{"operation": "add", "operands": [2, 3]}' http://localhost:5000/calculate
|
||||
```
|
||||
|
||||
--- main.py
|
||||
+++ main.py
|
||||
@@ -1,29 +1,29 @@
|
||||
-from fastapi import FastAPI
|
||||
-from pydantic import BaseModel
|
||||
+from flask import Flask, request, jsonify
|
||||
|
||||
-app = FastAPI()
|
||||
+app = Flask(__name__)
|
||||
|
||||
|
||||
-class Operation(BaseModel):
|
||||
- operation: str
|
||||
- operands: list
|
||||
+@app.route('/calculate', methods=['POST'])
|
||||
+def calculate():
|
||||
+ data = request.get_json()
|
||||
+ operation = data.get('operation')
|
||||
+ operands = data.get('operands')
|
||||
|
||||
|
||||
-@app.post('/calculate')
|
||||
-async def calculate(operation: Operation):
|
||||
- if operation.operation == 'add':
|
||||
- result = sum(operation.operands)
|
||||
- elif operation.operation == 'subtract':
|
||||
- result = operation.operands[0] - sum(operation.operands[1:])
|
||||
- elif operation.operation == 'multiply':
|
||||
+ if operation == 'add':
|
||||
+ result = sum(operands)
|
||||
+ elif operation == 'subtract':
|
||||
+ result = operands[0] - sum(operands[1:])
|
||||
+ elif operation == 'multiply':
|
||||
result = 1
|
||||
- for operand in operation.operands:
|
||||
+ for operand in operands:
|
||||
result *= operand
|
||||
- elif operation.operation == 'divide':
|
||||
- result = operation.operands[0]
|
||||
- for operand in operation.operands[1:]:
|
||||
+ elif operation == 'divide':
|
||||
+ result = operands[0]
|
||||
+ for operand in operands[1:]:
|
||||
result /= operand
|
||||
else:
|
||||
result = None
|
||||
- return {'result': result}
|
||||
+ return jsonify({'result': result})
|
||||
|
||||
--- requirements.txt
|
||||
+++ requirements.txt
|
||||
@@ -1,3 +1,2 @@
|
||||
-fastapi
|
||||
-uvicorn
|
||||
-pydantic
|
||||
+flask
|
||||
+flask-cors
|
||||
@@ -1,139 +0,0 @@
|
||||
import instructor
|
||||
|
||||
from openai import OpenAI
|
||||
from pydantic import Field
|
||||
from instructor import ResponseSchema
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class File(ResponseSchema):
|
||||
"""
|
||||
Correctly named file with contents.
|
||||
"""
|
||||
|
||||
file_name: str = Field(
|
||||
..., description="The name of the file including the extension"
|
||||
)
|
||||
body: str = Field(..., description="Correct contents of a file")
|
||||
|
||||
def save(self):
|
||||
with open(self.file_name, "w") as f:
|
||||
f.write(self.body)
|
||||
|
||||
|
||||
class Program(ResponseSchema):
|
||||
"""
|
||||
Set of files that represent a complete and correct program
|
||||
"""
|
||||
|
||||
files: list[File] = Field(..., description="List of files")
|
||||
|
||||
|
||||
def develop(data: str) -> Program:
|
||||
completion = client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
temperature=0.1,
|
||||
functions=[Program.openai_schema],
|
||||
function_call={"name": Program.openai_schema["name"]},
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a world class programming AI capable of writing correct python scripts and modules. You will name files correct, include __init__.py files and write correct python code. with correct imports.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": data,
|
||||
},
|
||||
],
|
||||
max_tokens=1000,
|
||||
)
|
||||
return Program.from_response(completion)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
program = develop(
|
||||
"""
|
||||
Create a fastapi app with a readme.md file and a main.py file with
|
||||
some basic math functions. the datamodels should use pydantic and
|
||||
the main.py should use fastapi. the readme.md should have a title
|
||||
and a description. The readme should contain some helpful infromation
|
||||
and a curl example"""
|
||||
)
|
||||
|
||||
for file in program.files:
|
||||
print(file.file_name)
|
||||
print("-")
|
||||
print(file.body)
|
||||
print("\n\n\n")
|
||||
"""
|
||||
readme.md
|
||||
-
|
||||
# FastAPI App
|
||||
|
||||
This is a FastAPI app that provides some basic math functions.
|
||||
|
||||
## Usage
|
||||
|
||||
To use this app, follow the instructions below:
|
||||
|
||||
1. Install the required dependencies by running `pip install -r requirements.txt`.
|
||||
2. Start the app by running `uvicorn main:app --reload`.
|
||||
3. Open your browser and navigate to `http://localhost:8000/docs` to access the Swagger UI documentation.
|
||||
|
||||
## Example
|
||||
|
||||
To perform a basic math operation, you can use the following curl command:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"operation": "add", "operands": [2, 3]}' http://localhost:8000/calculate
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
main.py
|
||||
-
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Operation(BaseModel):
|
||||
operation: str
|
||||
operands: list
|
||||
|
||||
|
||||
@app.post('/calculate')
|
||||
async def calculate(operation: Operation):
|
||||
if operation.operation == 'add':
|
||||
result = sum(operation.operands)
|
||||
elif operation.operation == 'subtract':
|
||||
result = operation.operands[0] - sum(operation.operands[1:])
|
||||
elif operation.operation == 'multiply':
|
||||
result = 1
|
||||
for operand in operation.operands:
|
||||
result *= operand
|
||||
elif operation.operation == 'divide':
|
||||
result = operation.operands[0]
|
||||
for operand in operation.operands[1:]:
|
||||
result /= operand
|
||||
else:
|
||||
result = None
|
||||
return {'result': result}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
requirements.txt
|
||||
-
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
"""
|
||||
|
||||
with open("program.json", "w") as f:
|
||||
f.write(Program.parse_obj(program).json())
|
||||
@@ -1 +0,0 @@
|
||||
{"files": [{"file_name": "readme.md", "body": "# FastAPI App\n\nThis is a FastAPI app that provides some basic math functions.\n\n## Usage\n\nTo use this app, follow the instructions below:\n\n1. Install the required dependencies by running `pip install -r requirements.txt`.\n2. Start the app by running `uvicorn main:app --reload`.\n3. Open your browser and navigate to `http://localhost:8000/docs` to access the Swagger UI documentation.\n\n## Example\n\nTo perform a basic math operation, you can use the following curl command:\n\n```bash\ncurl -X POST -H \"Content-Type: application/json\" -d '{\"operation\": \"add\", \"operands\": [2, 3]}' http://localhost:8000/calculate\n```\n"}, {"file_name": "main.py", "body": "from fastapi import FastAPI\nfrom pydantic import BaseModel\n\napp = FastAPI()\n\n\nclass Operation(BaseModel):\n operation: str\n operands: list\n\n\n@app.post('/calculate')\nasync def calculate(operation: Operation):\n if operation.operation == 'add':\n result = sum(operation.operands)\n elif operation.operation == 'subtract':\n result = operation.operands[0] - sum(operation.operands[1:])\n elif operation.operation == 'multiply':\n result = 1\n for operand in operation.operands:\n result *= operand\n elif operation.operation == 'divide':\n result = operation.operands[0]\n for operand in operation.operands[1:]:\n result /= operand\n else:\n result = None\n return {'result': result}\n"}, {"file_name": "requirements.txt", "body": "fastapi\nuvicorn\npydantic"}]}
|
||||
@@ -1,191 +0,0 @@
|
||||
import instructor
|
||||
|
||||
from openai import OpenAI
|
||||
from pydantic import Field, parse_file_as
|
||||
from instructor import ResponseSchema
|
||||
from generate import Program
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Diff(ResponseSchema):
|
||||
"""
|
||||
Changes that must be correctly made in a program's code repository defined as a
|
||||
complete diff (Unified Format) file which will be used to `patch` the repository.
|
||||
|
||||
Example:
|
||||
--- /path/to/original timestamp
|
||||
+++ /path/to/new timestamp
|
||||
@@ -1,3 +1,9 @@
|
||||
+This is an important
|
||||
+notice! It should
|
||||
+therefore be located at
|
||||
+the beginning of this
|
||||
+document!
|
||||
+
|
||||
This part of the
|
||||
document has stayed the
|
||||
same from version to
|
||||
@@ -8,13 +14,8 @@
|
||||
compress the size of the
|
||||
changes.
|
||||
-This paragraph contains
|
||||
-text that is outdated.
|
||||
-It will be deleted in the
|
||||
-near future.
|
||||
-
|
||||
It is important to spell
|
||||
-check this dokument. On
|
||||
+check this document. On
|
||||
the other hand, a
|
||||
misspelled word isn't
|
||||
the end of the world.
|
||||
@@ -22,3 +23,7 @@
|
||||
this paragraph needs to
|
||||
be changed. Things can
|
||||
be added after it.
|
||||
+
|
||||
+This paragraph contains
|
||||
+important new additions
|
||||
+to this document.
|
||||
"""
|
||||
|
||||
diff: str = Field(
|
||||
...,
|
||||
description=(
|
||||
"Changes in a code repository correctly represented in 'diff' format, "
|
||||
"correctly escaped so it could be used in a JSON"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def refactor(new_requirements: str, program: Program) -> Diff:
|
||||
program_description = "\n".join(
|
||||
[f"{code.file_name}\n[[[\n{code.body}\n]]]\n" for code in program.files]
|
||||
)
|
||||
completion = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
temperature=0,
|
||||
functions=[Diff.openai_schema],
|
||||
function_call={"name": Diff.openai_schema["name"]},
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a world class programming AI capable of refactor "
|
||||
"existing python repositories. You will name files correct, include "
|
||||
"__init__.py files and write correct python code, with correct imports. "
|
||||
"You'll deliver your changes in valid 'diff' format so that they could "
|
||||
"be applied using the 'patch' command. "
|
||||
"Make sure you put the correct line numbers, "
|
||||
"and that all lines that must be changed are correctly marked.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": new_requirements,
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": program_description,
|
||||
},
|
||||
],
|
||||
max_tokens=1000,
|
||||
)
|
||||
return Diff.from_response(completion)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
program = parse_file_as(path="program.json", type_=Program)
|
||||
|
||||
changes = refactor(
|
||||
new_requirements="Refactor this code to use flask instead.",
|
||||
program=program,
|
||||
)
|
||||
print(changes.diff)
|
||||
"""
|
||||
--- readme.md
|
||||
+++ readme.md
|
||||
@@ -1,9 +1,9 @@
|
||||
# FastAPI App
|
||||
|
||||
-This is a FastAPI app that provides some basic math functions.
|
||||
+This is a Flask app that provides some basic math functions.
|
||||
|
||||
## Usage
|
||||
|
||||
To use this app, follow the instructions below:
|
||||
|
||||
1. Install the required dependencies by running `pip install -r requirements.txt`.
|
||||
-2. Start the app by running `uvicorn main:app --reload`.
|
||||
+2. Start the app by running `flask run`.
|
||||
3. Open your browser and navigate to `http://localhost:5000/docs` to access the Swagger UI documentation.
|
||||
|
||||
## Example
|
||||
|
||||
To perform a basic math operation, you can use the following curl command:
|
||||
|
||||
```bash
|
||||
-curl -X POST -H "Content-Type: application/json" -d '{"operation": "add", "operands": [2, 3]}' http://localhost:8000/calculate
|
||||
+curl -X POST -H "Content-Type: application/json" -d '{"operation": "add", "operands": [2, 3]}' http://localhost:5000/calculate
|
||||
```
|
||||
|
||||
--- main.py
|
||||
+++ main.py
|
||||
@@ -1,29 +1,29 @@
|
||||
-from fastapi import FastAPI
|
||||
-from pydantic import BaseModel
|
||||
+from flask import Flask, request, jsonify
|
||||
|
||||
-app = FastAPI()
|
||||
+app = Flask(__name__)
|
||||
|
||||
|
||||
-class Operation(BaseModel):
|
||||
- operation: str
|
||||
- operands: list
|
||||
+@app.route('/calculate', methods=['POST'])
|
||||
+def calculate():
|
||||
+ data = request.get_json()
|
||||
+ operation = data.get('operation')
|
||||
+ operands = data.get('operands')
|
||||
|
||||
|
||||
-@app.post('/calculate')
|
||||
-async def calculate(operation: Operation):
|
||||
- if operation.operation == 'add':
|
||||
- result = sum(operation.operands)
|
||||
- elif operation.operation == 'subtract':
|
||||
- result = operation.operands[0] - sum(operation.operands[1:])
|
||||
- elif operation.operation == 'multiply':
|
||||
+ if operation == 'add':
|
||||
+ result = sum(operands)
|
||||
+ elif operation == 'subtract':
|
||||
+ result = operands[0] - sum(operands[1:])
|
||||
+ elif operation == 'multiply':
|
||||
result = 1
|
||||
- for operand in operation.operands:
|
||||
+ for operand in operands:
|
||||
result *= operand
|
||||
- elif operation.operation == 'divide':
|
||||
- result = operation.operands[0]
|
||||
- for operand in operation.operands[1:]:
|
||||
+ elif operation == 'divide':
|
||||
+ result = operands[0]
|
||||
+ for operand in operands[1:]:
|
||||
result /= operand
|
||||
else:
|
||||
result = None
|
||||
- return {'result': result}
|
||||
+ return jsonify({'result': result})
|
||||
|
||||
--- requirements.txt
|
||||
+++ requirements.txt
|
||||
@@ -1,3 +1,2 @@
|
||||
-fastapi
|
||||
-uvicorn
|
||||
-pydantic
|
||||
+flask
|
||||
+flask-cors
|
||||
"""
|
||||
|
||||
with open("changes.diff", "w") as f:
|
||||
f.write(changes.diff)
|
||||
@@ -1,44 +0,0 @@
|
||||
import os
|
||||
from pydantic import BaseModel, Field
|
||||
from groq import Groq
|
||||
import instructor
|
||||
|
||||
|
||||
class Character(BaseModel):
|
||||
name: str
|
||||
fact: list[str] = Field(..., description="A list of facts about the subject")
|
||||
|
||||
|
||||
client = Groq(
|
||||
api_key=os.environ.get("GROQ_API_KEY"),
|
||||
)
|
||||
|
||||
client = instructor.from_groq(client, mode=instructor.Mode.TOOLS)
|
||||
|
||||
resp = client.chat.completions.create(
|
||||
model="mixtral-8x7b-32768",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Tell me about the company Tesla",
|
||||
}
|
||||
],
|
||||
response_model=Character,
|
||||
)
|
||||
print(resp.model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"name": "Tesla",
|
||||
"fact": [
|
||||
"An American electric vehicle and clean energy company.",
|
||||
"Co-founded by Elon Musk, JB Straubel, Martin Eberhard, Marc Tarpenning, and Ian Wright in 2003.",
|
||||
"Headquartered in Austin, Texas.",
|
||||
"Produces electric vehicles, energy storage solutions, and more recently, solar energy products.",
|
||||
"Known for its premium electric vehicles, such as the Model S, Model 3, Model X, and Model Y.",
|
||||
"One of the world's most valuable car manufacturers by market capitalization.",
|
||||
"Tesla's CEO, Elon Musk, is also the CEO of SpaceX, Neuralink, and The Boring Company.",
|
||||
"Tesla operates the world's largest global network of electric vehicle supercharging stations.",
|
||||
"The company aims to accelerate the world's transition to sustainable transport and energy through innovative technologies and products."
|
||||
]
|
||||
}
|
||||
"""
|
||||
@@ -1,36 +0,0 @@
|
||||
import os
|
||||
from pydantic import BaseModel
|
||||
from groq import Groq
|
||||
import instructor
|
||||
|
||||
client = Groq(
|
||||
api_key=os.environ.get("GROQ_API_KEY"),
|
||||
)
|
||||
|
||||
client = instructor.from_groq(client, mode=instructor.Mode.TOOLS)
|
||||
|
||||
|
||||
class UserExtract(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
user: UserExtract = client.chat.completions.create(
|
||||
model="mixtral-8x7b-32768",
|
||||
response_model=UserExtract,
|
||||
messages=[
|
||||
{"role": "user", "content": "Extract jason is 25 years old"},
|
||||
],
|
||||
)
|
||||
|
||||
assert isinstance(user, UserExtract), "Should be instance of UserExtract"
|
||||
assert user.name.lower() == "jason"
|
||||
assert user.age == 25
|
||||
|
||||
print(user.model_dump_json(indent=2))
|
||||
"""
|
||||
{
|
||||
"name": "jason",
|
||||
"age": 25
|
||||
}
|
||||
"""
|
||||
@@ -1,61 +0,0 @@
|
||||
# Instructor Hooks Example
|
||||
|
||||
This example demonstrates how to use the Hooks system in the Instructor library to monitor, log, and debug your LLM interactions.
|
||||
|
||||
## What are Hooks?
|
||||
|
||||
Hooks provide a powerful mechanism for intercepting and handling events during the completion and parsing process. They allow you to add custom behavior, logging, or error handling at various stages of the API interaction.
|
||||
|
||||
The Instructor library supports several predefined hooks:
|
||||
|
||||
- `completion:kwargs`: Emitted when completion arguments are provided
|
||||
- `completion:response`: Emitted when a completion response is received
|
||||
- `completion:error`: Emitted when an error occurs during completion
|
||||
- `completion:last_attempt`: Emitted when the last retry attempt is made
|
||||
- `parse:error`: Emitted when an error occurs during response parsing
|
||||
|
||||
## What This Example Shows
|
||||
|
||||
This example demonstrates:
|
||||
|
||||
1. **Basic Hook Registration**: How to register handlers for different hook events
|
||||
2. **Multiple Handlers**: How to register multiple handlers for the same event
|
||||
3. **Statistics Collection**: How to collect and track API usage statistics
|
||||
4. **Error Handling**: How to catch and process different types of errors
|
||||
5. **Hook Cleanup**: How to remove hooks when they're no longer needed
|
||||
|
||||
## Usage Examples
|
||||
|
||||
The code demonstrates three scenarios:
|
||||
|
||||
1. **Successful Extraction**: A basic example that works correctly
|
||||
2. **Parse Error**: An example that triggers a validation error
|
||||
3. **Multiple Hooks**: Shows how to attach multiple handlers to the same event
|
||||
|
||||
## How to Run the Example
|
||||
|
||||
```bash
|
||||
# Navigate to the hooks example directory
|
||||
cd examples/hooks
|
||||
|
||||
# Run the example
|
||||
python run.py
|
||||
```
|
||||
|
||||
## Expected Output
|
||||
|
||||
The example will print detailed information about each request, including:
|
||||
|
||||
- 🔍 Request details (model, prompt)
|
||||
- 📏 Approximate input token count
|
||||
- 📊 Token usage statistics
|
||||
- ✅ Successful responses
|
||||
- ⚠️ Parse errors
|
||||
- ❌ Completion errors
|
||||
- 🔄 Retry attempt notifications
|
||||
|
||||
At the end, it will print a summary of the statistics collected.
|
||||
|
||||
## Learn More
|
||||
|
||||
For more information about hooks in Instructor, see the [hooks documentation](https://instructor-ai.github.io/instructor/concepts/hooks/).
|
||||
@@ -1,204 +0,0 @@
|
||||
"""
|
||||
This example demonstrates how to use hooks in Instructor for monitoring,
|
||||
logging, and debugging your LLM interactions.
|
||||
|
||||
Hooks allow you to attach handlers to events that occur during the completion
|
||||
and parsing process. This can be useful for:
|
||||
- Logging API requests and responses
|
||||
- Debugging parsing errors
|
||||
- Collecting statistics about API usage
|
||||
- Adding custom error handling
|
||||
"""
|
||||
|
||||
import instructor
|
||||
import openai
|
||||
import pydantic
|
||||
|
||||
|
||||
class User(pydantic.BaseModel):
|
||||
"""A simple user model with validation."""
|
||||
|
||||
name: str
|
||||
age: int
|
||||
|
||||
@pydantic.field_validator("age")
|
||||
def validate_age(cls, v: int) -> int:
|
||||
if v < 0:
|
||||
raise ValueError("Age must be non-negative")
|
||||
return v
|
||||
|
||||
|
||||
class CompletionStats:
|
||||
"""A simple class to collect statistics about completions."""
|
||||
|
||||
def __init__(self):
|
||||
self.total_completions = 0
|
||||
self.errors = 0
|
||||
self.successful = 0
|
||||
self.tokens_used = 0
|
||||
|
||||
def report(self):
|
||||
"""Print a report of the statistics."""
|
||||
print("\n--- Completion Statistics ---")
|
||||
print(f"Total completions: {self.total_completions}")
|
||||
print(f"Successful: {self.successful}")
|
||||
print(f"Errors: {self.errors}")
|
||||
print(f"Total tokens used: {self.tokens_used}")
|
||||
|
||||
|
||||
def main():
|
||||
# Initialize the OpenAI client with Instructor
|
||||
client = instructor.from_openai(openai.OpenAI())
|
||||
|
||||
# Create a statistics collector
|
||||
stats = CompletionStats()
|
||||
|
||||
# Define hook handlers
|
||||
def log_completion_kwargs(_, **kwargs):
|
||||
"""Handler for completion:kwargs hook."""
|
||||
stats.total_completions += 1
|
||||
print(
|
||||
f"\n🔍 Sending completion request using model: {kwargs.get('model', 'unknown')}"
|
||||
)
|
||||
if "messages" in kwargs:
|
||||
for msg in kwargs["messages"]:
|
||||
if msg.get("role") == "user":
|
||||
print(f"📝 User prompt: {msg.get('content')}")
|
||||
|
||||
def log_completion_response(response):
|
||||
"""Handler for completion:response hook."""
|
||||
stats.successful += 1
|
||||
|
||||
# Extract token usage if available
|
||||
if hasattr(response, "usage") and response.usage:
|
||||
token_usage = response.usage.total_tokens
|
||||
stats.tokens_used += token_usage
|
||||
print(f"📊 Token usage: {token_usage}")
|
||||
|
||||
print(f"✅ Received completion response")
|
||||
|
||||
def log_completion_error(error):
|
||||
"""Handler for completion:error hook."""
|
||||
stats.errors += 1
|
||||
print(f"❌ Completion error: {type(error).__name__}: {str(error)}")
|
||||
|
||||
def log_parse_error(error):
|
||||
"""Handler for parse:error hook."""
|
||||
stats.errors += 1
|
||||
print(f"⚠️ Parse error: {type(error).__name__}: {str(error)}")
|
||||
|
||||
# Register the hooks
|
||||
client.on("completion:kwargs", log_completion_kwargs)
|
||||
client.on("completion:response", log_completion_response)
|
||||
client.on("completion:error", log_completion_error)
|
||||
client.on(
|
||||
"completion:last_attempt", lambda _: print(f"🔄 Last retry attempt failed")
|
||||
)
|
||||
client.on("parse:error", log_parse_error)
|
||||
|
||||
# Example 1: Successful extraction
|
||||
try:
|
||||
print("\n--- Example 1: Successful Extraction ---")
|
||||
user = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Extract: John is 30 years old."}],
|
||||
response_model=User,
|
||||
)
|
||||
print(f"Result: {user}")
|
||||
except Exception as e:
|
||||
print(f"Main exception: {e}")
|
||||
|
||||
# Example 2: Parse error (validation fails)
|
||||
try:
|
||||
print("\n--- Example 2: Parse Error (Age Validation) ---")
|
||||
user = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Extract: Alice is -5 years old."}],
|
||||
response_model=User,
|
||||
)
|
||||
print(f"Result: {user}")
|
||||
except Exception as e:
|
||||
print(f"Main exception: {e}")
|
||||
|
||||
# Example 3: Multiple hooks for the same event
|
||||
print("\n--- Example 3: Multiple Hooks ---")
|
||||
|
||||
# Add another hook for completion:kwargs that counts message tokens
|
||||
def count_input_tokens(_, **kwargs):
|
||||
"""Handler for counting approximate tokens in input messages."""
|
||||
if "messages" in kwargs:
|
||||
total_chars = sum(len(msg.get("content", "")) for msg in kwargs["messages"])
|
||||
# Rough approximation of tokens (not accurate)
|
||||
approx_tokens = total_chars / 4
|
||||
print(f"📏 Approximate input tokens: {approx_tokens:.0f}")
|
||||
|
||||
# Register the additional hook
|
||||
client.on("completion:kwargs", count_input_tokens)
|
||||
|
||||
try:
|
||||
user = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[{"role": "user", "content": "Extract: Bob is 25 years old."}],
|
||||
response_model=User,
|
||||
)
|
||||
print(f"Result: {user}")
|
||||
except Exception as e:
|
||||
print(f"Main exception: {e}")
|
||||
|
||||
# Print the final statistics
|
||||
stats.report()
|
||||
|
||||
# Clean up hooks
|
||||
print("\n--- Cleaning Up Hooks ---")
|
||||
client.clear()
|
||||
print("All hooks cleared")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
"""
|
||||
|
||||
--- Example 1: Successful Extraction ---
|
||||
|
||||
🔍 Sending completion request using model: gpt-3.5-turbo
|
||||
📝 User prompt: Extract: John is 30 years old.
|
||||
📊 Token usage: 82
|
||||
✅ Received completion response
|
||||
Result: name='John' age=30
|
||||
|
||||
--- Example 2: Parse Error (Age Validation) ---
|
||||
|
||||
🔍 Sending completion request using model: gpt-3.5-turbo
|
||||
📝 User prompt: Extract: Alice is -5 years old.
|
||||
📊 Token usage: 82
|
||||
✅ Received completion response
|
||||
⚠️ Parse error: ValidationError: 1 validation error for User
|
||||
age
|
||||
Value error, Age must be non-negative [type=value_error, input_value=-5, input_type=int]
|
||||
For further information visit https://errors.pydantic.dev/2.9/v/value_error
|
||||
|
||||
🔍 Sending completion request using model: gpt-3.5-turbo
|
||||
📝 User prompt: Extract: Alice is -5 years old.
|
||||
📊 Token usage: 170
|
||||
✅ Received completion response
|
||||
Result: name='Alice' age=5
|
||||
|
||||
--- Example 3: Multiple Hooks ---
|
||||
|
||||
🔍 Sending completion request using model: gpt-3.5-turbo
|
||||
📝 User prompt: Extract: Bob is 25 years old.
|
||||
📏 Approximate input tokens: 7
|
||||
📊 Token usage: 82
|
||||
✅ Received completion response
|
||||
Result: name='Bob' age=25
|
||||
|
||||
--- Completion Statistics ---
|
||||
Total completions: 4
|
||||
Successful: 4
|
||||
Errors: 1
|
||||
Total tokens used: 416
|
||||
|
||||
--- Cleaning Up Hooks ---
|
||||
All hooks cleared
|
||||
"""
|
||||
@@ -1,56 +0,0 @@
|
||||
import time
|
||||
|
||||
from collections.abc import Iterable
|
||||
from openai import OpenAI
|
||||
from pydantic import BaseModel
|
||||
|
||||
import instructor
|
||||
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
job: str
|
||||
age: int
|
||||
|
||||
|
||||
def stream_extract(input: str) -> Iterable[User]:
|
||||
return client.chat.completions.create_iterable(
|
||||
model="gpt-4o",
|
||||
temperature=0.1,
|
||||
stream=True,
|
||||
response_model=User,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a perfect entity extraction system",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
f"Consider the data below:\n{input}"
|
||||
"Correctly segment it into entitites"
|
||||
"Make sure the JSON is correct"
|
||||
),
|
||||
},
|
||||
],
|
||||
max_tokens=1000,
|
||||
)
|
||||
|
||||
|
||||
start = time.time()
|
||||
for user in stream_extract(
|
||||
input="Create 5 characters from the book Three Body Problem"
|
||||
):
|
||||
delay = round(time.time() - start, 1)
|
||||
print(f"{delay} s: User({user})")
|
||||
"""
|
||||
0.8 s: User(name='Ye Wenjie' job='Astrophysicist' age=60)
|
||||
1.1 s: User(name='Wang Miao' job='Nanomaterials Researcher' age=40)
|
||||
1.7 s: User(name='Shi Qiang' job='Detective' age=50)
|
||||
1.9 s: User(name='Ding Yi' job='Theoretical Physicist' age=45)
|
||||
1.9 s: User(name='Chang Weisi' job='Military Strategist' age=55)
|
||||
"""
|
||||
# Notice that the first one would return at 5s bu the last one returned in 10s!
|
||||
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 241 KiB |
@@ -1,58 +0,0 @@
|
||||
import instructor
|
||||
|
||||
from graphviz import Digraph
|
||||
from pydantic import BaseModel, Field
|
||||
from openai import OpenAI
|
||||
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Node(BaseModel):
|
||||
id: int
|
||||
label: str
|
||||
color: str
|
||||
|
||||
|
||||
class Edge(BaseModel):
|
||||
source: int
|
||||
target: int
|
||||
label: str
|
||||
color: str = "black"
|
||||
|
||||
|
||||
class KnowledgeGraph(BaseModel):
|
||||
nodes: list[Node] = Field(..., default_factory=list)
|
||||
edges: list[Edge] = Field(..., default_factory=list)
|
||||
|
||||
|
||||
def generate_graph(input) -> KnowledgeGraph:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-3.5-turbo-16k",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Help me understand following by describing as a detailed knowledge graph: {input}",
|
||||
}
|
||||
],
|
||||
response_model=KnowledgeGraph,
|
||||
) # type: ignore
|
||||
|
||||
|
||||
def visualize_knowledge_graph(kg: KnowledgeGraph):
|
||||
dot = Digraph(comment="Knowledge Graph")
|
||||
|
||||
# Add nodes
|
||||
for node in kg.nodes:
|
||||
dot.node(str(node.id), node.label, color=node.color)
|
||||
|
||||
# Add edges
|
||||
for edge in kg.edges:
|
||||
dot.edge(str(edge.source), str(edge.target), label=edge.label, color=edge.color)
|
||||
|
||||
# Render the graph
|
||||
dot.render("knowledge_graph.gv", view=True)
|
||||
|
||||
|
||||
graph: KnowledgeGraph = generate_graph("Teach me about quantum mechanics")
|
||||
visualize_knowledge_graph(graph)
|
||||
@@ -1,103 +0,0 @@
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
|
||||
from graphviz import Digraph
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Node(BaseModel):
|
||||
id: int
|
||||
label: str
|
||||
color: str
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((id, self.label))
|
||||
|
||||
|
||||
class Edge(BaseModel):
|
||||
source: int
|
||||
target: int
|
||||
label: str
|
||||
color: str = "black"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.source, self.target, self.label))
|
||||
|
||||
|
||||
class KnowledgeGraph(BaseModel):
|
||||
nodes: Optional[list[Node]] = Field(..., default_factory=list)
|
||||
edges: Optional[list[Edge]] = Field(..., default_factory=list)
|
||||
|
||||
def update(self, other: "KnowledgeGraph") -> "KnowledgeGraph":
|
||||
"""Updates the current graph with the other graph, deduplicating nodes and edges."""
|
||||
return KnowledgeGraph(
|
||||
nodes=list(set(self.nodes + other.nodes)),
|
||||
edges=list(set(self.edges + other.edges)),
|
||||
)
|
||||
|
||||
def draw(self, prefix: str = None):
|
||||
dot = Digraph(comment="Knowledge Graph")
|
||||
|
||||
# Add nodes
|
||||
for node in self.nodes:
|
||||
dot.node(str(node.id), node.label, color=node.color)
|
||||
|
||||
# Add edges
|
||||
for edge in self.edges:
|
||||
dot.edge(
|
||||
str(edge.source), str(edge.target), label=edge.label, color=edge.color
|
||||
)
|
||||
dot.render(prefix, format="png", view=True)
|
||||
|
||||
|
||||
def generate_graph(input: list[str]) -> KnowledgeGraph:
|
||||
cur_state = KnowledgeGraph()
|
||||
num_iterations = len(input)
|
||||
for i, inp in enumerate(input):
|
||||
new_updates = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo-16k",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": """You are an iterative knowledge graph builder.
|
||||
You are given the current state of the graph, and you must append the nodes and edges
|
||||
to it Do not procide any duplcates and try to reuse nodes as much as possible.""",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"""Extract any new nodes and edges from the following:
|
||||
# Part {i}/{num_iterations} of the input:
|
||||
|
||||
{inp}""",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"""Here is the current state of the graph:
|
||||
{cur_state.model_dump_json(indent=2)}""",
|
||||
},
|
||||
],
|
||||
response_model=KnowledgeGraph,
|
||||
) # type: ignore
|
||||
|
||||
# Update the current state
|
||||
cur_state = cur_state.update(new_updates)
|
||||
cur_state.draw(prefix=f"iteration_{i}")
|
||||
return cur_state
|
||||
|
||||
|
||||
# here we assume that we have to process the text in chunks
|
||||
# one at a time since they may not fit in the prompt otherwise
|
||||
text_chunks = [
|
||||
"Jason knows a lot about quantum mechanics. He is a physicist. He is a professor",
|
||||
"Professors are smart.",
|
||||
"Sarah knows Jason and is a student of his.",
|
||||
"Sarah is a student at the University of Toronto. and UofT is in Canada.",
|
||||
]
|
||||
|
||||
graph: KnowledgeGraph = generate_graph(text_chunks)
|
||||
|
||||
graph.draw(prefix="final")
|
||||
@@ -1,126 +0,0 @@
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
import instructor
|
||||
from pydantic import BaseModel
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
|
||||
client = instructor.apatch(AsyncOpenAI())
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.start = None
|
||||
self.end = None
|
||||
|
||||
async def __aenter__(self):
|
||||
self.start = time.time()
|
||||
|
||||
async def __aexit__(self, *args, **kwargs):
|
||||
self.end = time.time()
|
||||
print(f"{self.name} took {(self.end - self.start):.2f} seconds")
|
||||
|
||||
|
||||
class Person(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
async def extract_person(text: str) -> Person:
|
||||
return await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
messages=[
|
||||
{"role": "user", "content": text},
|
||||
],
|
||||
response_model=Person,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
"""We'll use this to run the example. and time how long each one takes!
|
||||
|
||||
0. for loop
|
||||
1. asyncio.gather
|
||||
2. asyncio.as_completed
|
||||
"""
|
||||
dataset = [
|
||||
"My name is John and I am 20 years old",
|
||||
"My name is Mary and I am 21 years old",
|
||||
"My name is Bob and I am 22 years old",
|
||||
"My name is Alice and I am 23 years old",
|
||||
"My name is Jane and I am 24 years old",
|
||||
"My name is Joe and I am 25 years old",
|
||||
"My name is Jill and I am 26 years old",
|
||||
]
|
||||
|
||||
"""
|
||||
This is the simplest way to run multiple async functions in series.
|
||||
It will wait for each function to complete before continuing.
|
||||
"""
|
||||
async with Timer("for loop"):
|
||||
persons = []
|
||||
for text in dataset:
|
||||
person = await extract_person(text)
|
||||
persons.append(person)
|
||||
print("for loop:", persons)
|
||||
|
||||
"""
|
||||
This is the simplest way to run multiple async functions in parallel.
|
||||
It will wait for all of the functions to complete before continuing.
|
||||
"""
|
||||
async with Timer("asyncio.gather"):
|
||||
tasks_get_persons = [extract_person(text) for text in dataset]
|
||||
all_person = await asyncio.gather(*tasks_get_persons)
|
||||
print("asyncio.gather:", all_person)
|
||||
|
||||
"""
|
||||
This is a bit more complicated, but it allows us to process each
|
||||
person as soon as they are ready. This is useful if you have a
|
||||
large dataset and want to start processing the results as soon
|
||||
as they are ready.
|
||||
"""
|
||||
async with Timer("asyncio.as_completed"):
|
||||
all_persons = []
|
||||
tasks_get_persons = [extract_person(text) for text in dataset]
|
||||
for person in asyncio.as_completed(tasks_get_persons):
|
||||
all_persons.append(await person)
|
||||
print("asyncio.as_copmleted:", all_persons)
|
||||
|
||||
"""
|
||||
If we want to rate limit our requests, we can use the
|
||||
semaphore to limit the number of concurrent requests.
|
||||
"""
|
||||
|
||||
# Create a semaphore that will only allow 2 concurrent requests
|
||||
sem = asyncio.Semaphore(2)
|
||||
|
||||
async def rate_limited_extract_person(text: str) -> Person:
|
||||
async with sem:
|
||||
return await extract_person(text)
|
||||
|
||||
async with Timer("asyncio.gather (rate limited)"):
|
||||
tasks_get_persons = [rate_limited_extract_person(text) for text in dataset]
|
||||
resp = await asyncio.gather(*tasks_get_persons)
|
||||
print("asyncio.gather (rate limited):", resp)
|
||||
|
||||
async with Timer("asyncio.as_completed (rate limited)"):
|
||||
all_persons = []
|
||||
tasks_get_persons = [rate_limited_extract_person(text) for text in dataset]
|
||||
for person in asyncio.as_completed(tasks_get_persons):
|
||||
all_persons.append(await person)
|
||||
print("asyncio.as_completed (rate limited):", all_persons)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
"""
|
||||
for loop took 6.17 seconds
|
||||
|
||||
asyncio.gather took 1.11 seconds
|
||||
asyncio.as_completed took 0.87 seconds
|
||||
|
||||
asyncio.gather (rate limited) took 3.04 seconds
|
||||
asyncio.as_completed (rate limited) took 3.26 seconds
|
||||
"""
|
||||
@@ -1,96 +0,0 @@
|
||||
import instructor
|
||||
import openai
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
client = instructor.from_openai(openai.OpenAI())
|
||||
|
||||
|
||||
class Judgment(BaseModel):
|
||||
thought: str = Field(
|
||||
description="The step-by-step reasoning process used to analyze the question and text"
|
||||
)
|
||||
justification: str = Field(
|
||||
description="Explanation for the similarity judgment, detailing key factors that led to the conclusion"
|
||||
)
|
||||
similarity: bool = Field(
|
||||
description="Boolean judgment indicating whether the question and text are similar or relevant (True) or not (False)"
|
||||
)
|
||||
|
||||
|
||||
prompt = """
|
||||
You are tasked with comparing a question and a piece of text to determine if they are relevant to each other or similar in some way. Your goal is to analyze the content, context, and potential connections between the two.
|
||||
|
||||
|
||||
To determine if the question and text are relevant or similar, please follow these steps:
|
||||
|
||||
1. Carefully read and understand both the question and the text.
|
||||
2. Identify the main topic, keywords, and concepts in the question.
|
||||
3. Analyze the text for any mention of these topics, keywords, or concepts.
|
||||
4. Consider any potential indirect connections or implications that might link the question and text.
|
||||
5. Evaluate the overall context and purpose of both the question and the text.
|
||||
|
||||
As you go through this process, please use a chain of thought approach. Write out your reasoning for each step inside <thought> tags.
|
||||
|
||||
After your analysis, provide a boolean judgment on whether the question and text are similar or relevant to each other. Use "true" if they are similar or relevant, and "false" if they are not.
|
||||
|
||||
Before giving your final judgment, provide a justification for your decision. Explain the key factors that led to your conclusion.
|
||||
"""
|
||||
|
||||
|
||||
def judge_relevance(question: str, text: str) -> Judgment:
|
||||
return client.chat.create(
|
||||
model="gpt-4o-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": prompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": """
|
||||
Here is the question:
|
||||
|
||||
<question>
|
||||
{{question}}
|
||||
</question>
|
||||
|
||||
Here is the text:
|
||||
<text>
|
||||
{{text}}
|
||||
</text>
|
||||
""",
|
||||
},
|
||||
],
|
||||
response_model=Judgment,
|
||||
context={"question": question, "text": text},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_pairs = [
|
||||
{
|
||||
"question": "What are the main causes of climate change?",
|
||||
"text": "Global warming is primarily caused by human activities, such as burning fossil fuels, deforestation, and industrial processes. These activities release greenhouse gases into the atmosphere, trapping heat and leading to a rise in global temperatures.",
|
||||
"is_similar": True,
|
||||
},
|
||||
{
|
||||
"question": "How does photosynthesis work?",
|
||||
"text": "Photosynthesis is the process by which plants use sunlight, water, and carbon dioxide to produce oxygen and energy in the form of sugar. It occurs in the chloroplasts of plant cells and is essential for life on Earth.",
|
||||
"is_similar": True,
|
||||
},
|
||||
{
|
||||
"question": "What are the benefits of regular exercise?",
|
||||
"text": "The Eiffel Tower, located in Paris, France, was completed in 1889. It stands 324 meters tall and was originally built as the entrance arch for the 1889 World's Fair.",
|
||||
"is_similar": False,
|
||||
},
|
||||
{
|
||||
"question": "How do vaccines work?",
|
||||
"text": "The process of baking bread involves mixing flour, water, yeast, and salt to form a dough. The dough is then kneaded, left to rise, shaped, and finally baked in an oven.",
|
||||
"is_similar": False,
|
||||
},
|
||||
]
|
||||
|
||||
score = 0
|
||||
for pair in test_pairs:
|
||||
result = judge_relevance(pair["question"], pair["text"])
|
||||
if result.similarity == pair["is_similar"]:
|
||||
score += 1
|
||||
|
||||
print(f"Score: {score}/{len(test_pairs)}")
|
||||
@@ -1,11 +0,0 @@
|
||||
# Instructions
|
||||
|
||||
1. Create a virtual environment and install all of the packages inside `requirements.txt`
|
||||
|
||||
2. Run the server using
|
||||
|
||||
```
|
||||
uvicorn server:app --reload
|
||||
```
|
||||
|
||||
3. Open up the documentation at `http://127.0.0.1:8000/docs` to start experimenting with fastapi! You can print out the streaming example using `test.py`.
|
||||
@@ -1,7 +0,0 @@
|
||||
pydantic==2.7.1
|
||||
openai==1.24.1
|
||||
instructor==1.0.3
|
||||
logfire==0.28.0
|
||||
fastapi==0.110.3
|
||||
uvicorn[standard]
|
||||
logfire[fastapi]
|
||||
@@ -1,84 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from fastapi import FastAPI
|
||||
from openai import AsyncOpenAI
|
||||
import instructor
|
||||
import logfire
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
|
||||
class UserData(BaseModel):
|
||||
query: str
|
||||
|
||||
|
||||
class MultipleUserData(BaseModel):
|
||||
queries: list[str]
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
openai_client = AsyncOpenAI()
|
||||
logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all"))
|
||||
logfire.instrument_fastapi(app)
|
||||
logfire.instrument_openai(openai_client)
|
||||
client = instructor.from_openai(openai_client)
|
||||
|
||||
|
||||
@app.post("/user", response_model=UserDetail)
|
||||
async def endpoint_function(data: UserData) -> UserDetail:
|
||||
user_detail = await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": f"Extract: `{data.query}`"},
|
||||
],
|
||||
)
|
||||
logfire.info("/User returning", value=user_detail)
|
||||
return user_detail
|
||||
|
||||
|
||||
@app.post("/many-users", response_model=list[UserDetail])
|
||||
async def extract_many_users(data: MultipleUserData):
|
||||
async def extract_user(query: str):
|
||||
user_detail = await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": f"Extract: `{query}`"},
|
||||
],
|
||||
)
|
||||
logfire.info("/User returning", value=user_detail)
|
||||
return user_detail
|
||||
|
||||
coros = [extract_user(query) for query in data.queries]
|
||||
return await asyncio.gather(*coros)
|
||||
|
||||
|
||||
@app.post("/extract", response_class=StreamingResponse)
|
||||
async def extract(data: UserData):
|
||||
supressed_client = AsyncOpenAI()
|
||||
logfire.instrument_openai(supressed_client, suppress_other_instrumentation=False)
|
||||
client = instructor.from_openai(supressed_client)
|
||||
users = await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=Iterable[UserDetail],
|
||||
stream=True,
|
||||
messages=[
|
||||
{"role": "user", "content": data.query},
|
||||
],
|
||||
)
|
||||
|
||||
async def generate():
|
||||
with logfire.span("Generating User Response Objects"):
|
||||
async for user in users:
|
||||
resp_json = user.model_dump_json()
|
||||
logfire.info("Returning user object", value=resp_json)
|
||||
|
||||
yield resp_json
|
||||
|
||||
return StreamingResponse(generate(), media_type="text/event-stream")
|
||||
@@ -1,13 +0,0 @@
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
"http://127.0.0.1:3000/extract",
|
||||
json={
|
||||
"query": "Alice and Bob are best friends. They are currently 32 and 43 respectively. "
|
||||
},
|
||||
stream=True,
|
||||
)
|
||||
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
print(str(chunk, encoding="utf-8"), end="\n")
|
||||
@@ -1,52 +0,0 @@
|
||||
import enum
|
||||
from pydantic import BaseModel
|
||||
from openai import OpenAI
|
||||
import instructor
|
||||
import logfire
|
||||
|
||||
|
||||
class Labels(str, enum.Enum):
|
||||
"""Enumeration for single-label text classification."""
|
||||
|
||||
SPAM = "spam"
|
||||
NOT_SPAM = "not_spam"
|
||||
|
||||
|
||||
class SinglePrediction(BaseModel):
|
||||
"""
|
||||
Class for a single class label prediction.
|
||||
"""
|
||||
|
||||
class_label: Labels
|
||||
|
||||
|
||||
openai_client = OpenAI()
|
||||
logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all"))
|
||||
logfire.instrument_openai(openai_client)
|
||||
client = instructor.from_openai(openai_client)
|
||||
|
||||
|
||||
@logfire.instrument("classification", extract_args=True)
|
||||
def classify(data: str) -> SinglePrediction:
|
||||
"""Perform single-label classification on the input text."""
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4o-mini",
|
||||
response_model=SinglePrediction,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Classify the following text: {data}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
emails = [
|
||||
"Hello there I'm a Nigerian prince and I want to give you money",
|
||||
"Meeting with Thomas has been set at Friday next week",
|
||||
"Here are some weekly product updates from our marketing team",
|
||||
]
|
||||
|
||||
for email in emails:
|
||||
classify(email)
|
||||
@@ -1,79 +0,0 @@
|
||||
import instructor
|
||||
from io import StringIO
|
||||
from typing import Annotated, Any
|
||||
from collections.abc import Iterable
|
||||
from pydantic import (
|
||||
BeforeValidator,
|
||||
InstanceOf,
|
||||
WithJsonSchema,
|
||||
BaseModel,
|
||||
)
|
||||
import pandas as pd
|
||||
from openai import OpenAI
|
||||
import logfire
|
||||
|
||||
openai_client = OpenAI()
|
||||
logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all"))
|
||||
logfire.instrument_openai(openai_client)
|
||||
client = instructor.from_openai(openai_client, mode=instructor.Mode.MD_JSON)
|
||||
|
||||
|
||||
def md_to_df(data: Any) -> Any:
|
||||
# Convert markdown to DataFrame
|
||||
if isinstance(data, str):
|
||||
return (
|
||||
pd.read_csv(
|
||||
StringIO(data), # Process data
|
||||
sep="|",
|
||||
index_col=1,
|
||||
)
|
||||
.dropna(axis=1, how="all")
|
||||
.iloc[1:]
|
||||
.applymap(lambda x: x.strip())
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
MarkdownDataFrame = Annotated[
|
||||
InstanceOf[pd.DataFrame],
|
||||
BeforeValidator(md_to_df),
|
||||
WithJsonSchema(
|
||||
{
|
||||
"type": "string",
|
||||
"description": "The markdown representation of the table, each one should be tidy, do not try to join tables that should be separate",
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Table(BaseModel):
|
||||
caption: str
|
||||
dataframe: MarkdownDataFrame
|
||||
|
||||
|
||||
@logfire.instrument("extract-table", extract_args=True)
|
||||
def extract_table_from_image(url: str) -> Iterable[Table]:
|
||||
return client.chat.completions.create(
|
||||
model="gpt-4-vision-preview",
|
||||
response_model=Iterable[Table],
|
||||
max_tokens=1800,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Extract out a table from the image. Only extract out the total number of skiiers.",
|
||||
},
|
||||
{"type": "image_url", "image_url": {"url": url}},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
url = "https://cdn.statcdn.com/Infographic/images/normal/16330.jpeg"
|
||||
tables = extract_table_from_image(url)
|
||||
for table in tables:
|
||||
print(table.caption, end="\n")
|
||||
print(table.dataframe.to_markdown())
|
||||
@@ -1,4 +0,0 @@
|
||||
pydantic==2.7.1
|
||||
openai==1.24.1
|
||||
instructor==1.0.3
|
||||
logfire==0.28.0
|
||||
@@ -1,33 +0,0 @@
|
||||
from typing import Annotated
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic.functional_validators import AfterValidator
|
||||
from instructor import llm_validator
|
||||
import logfire
|
||||
import instructor
|
||||
from openai import OpenAI
|
||||
|
||||
openai_client = OpenAI()
|
||||
logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record="all"))
|
||||
logfire.instrument_openai(openai_client)
|
||||
client = instructor.from_openai(openai_client)
|
||||
|
||||
|
||||
class Statement(BaseModel):
|
||||
message: Annotated[
|
||||
str,
|
||||
AfterValidator(
|
||||
llm_validator("Don't allow any objectionable content", client=client)
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
messages = [
|
||||
"I think we should always treat violence as the best solution",
|
||||
"There are some great pastries down the road at this bakery I know",
|
||||
]
|
||||
|
||||
for message in messages:
|
||||
try:
|
||||
Statement(message=message)
|
||||
except ValidationError as e:
|
||||
print(e)
|
||||
@@ -1,51 +0,0 @@
|
||||
import instructor
|
||||
import openai
|
||||
import logging
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Set logging to DEBUG
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
client = instructor.from_openai(openai.OpenAI())
|
||||
|
||||
|
||||
class UserDetail(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
user = client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=UserDetail,
|
||||
messages=[
|
||||
{"role": "user", "content": "Extract Jason is 25 years old"},
|
||||
],
|
||||
) # type: ignore
|
||||
|
||||
"""
|
||||
DEBUG:httpx:load_ssl_context verify=True cert=None trust_env=True http2=False
|
||||
DEBUG:httpx:load_verify_locations cafile='/Users/jasonliu/dev/instructor/.venv/lib/python3.11/site-packages/certifi/cacert.pem'
|
||||
DEBUG:instructor:Patching `client.chat.completions.create` with mode=<Mode.TOOLS: 'tool_call'>
|
||||
DEBUG:instructor:max_retries: 1
|
||||
DEBUG:openai._base_client:Request options: {'method': 'post', 'url': '/chat/completions', 'files': None, 'json_data': {'messages': [{'role': 'user', 'content': 'Extract Jason is 25 years old'}], 'model': 'gpt-3.5-turbo', 'function_call': {'name': 'UserDetail'}, 'functions': [{'name': 'UserDetail', 'description': 'Correctly extracted `UserDetail` with all the required parameters with correct types', 'parameters': {'properties': {'name': {'title': 'Name', 'type': 'string'}, 'age': {'title': 'Age', 'type': 'integer'}}, 'required': ['age', 'name'], 'type': 'object'}}]}}
|
||||
DEBUG:httpcore.connection:connect_tcp.started host='api.openai.com' port=443 local_address=None timeout=5.0 socket_options=None
|
||||
DEBUG:httpcore.connection:connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x105062c90>
|
||||
DEBUG:httpcore.connection:start_tls.started ssl_context=<ssl.SSLContext object at 0x100748680> server_hostname='api.openai.com' timeout=5.0
|
||||
DEBUG:httpcore.connection:start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x101caa150>
|
||||
DEBUG:httpcore.http11:send_request_headers.started request=<Request [b'POST']>
|
||||
DEBUG:httpcore.http11:send_request_headers.complete
|
||||
DEBUG:httpcore.http11:send_request_body.started request=<Request [b'POST']>
|
||||
DEBUG:httpcore.http11:send_request_body.complete
|
||||
DEBUG:httpcore.http11:receive_response_headers.started request=<Request [b'POST']>
|
||||
DEBUG:httpcore.http11:receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Date', b'Mon, 12 Feb 2024 14:55:45 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'access-control-allow-origin', b'*'), (b'Cache-Control', b'no-cache, must-revalidate'), (b'openai-model', b'gpt-3.5-turbo-0613'), (b'openai-organization', b'scribe-ai'), (b'openai-processing-ms', b'483'), (b'openai-version', b'2020-10-01'), (b'strict-transport-security', b'max-age=15724800; includeSubDomains'), (b'x-ratelimit-limit-requests', b'10000'), (b'x-ratelimit-limit-tokens', b'2000000'), (b'x-ratelimit-remaining-requests', b'9999'), (b'x-ratelimit-remaining-tokens', b'1999975'), (b'x-ratelimit-reset-requests', b'6ms'), (b'x-ratelimit-reset-tokens', b'0s'), (b'x-request-id', b'req_f0fa476897ae165fc50fa90b7968595b'), (b'CF-Cache-Status', b'DYNAMIC'), (b'Set-Cookie', b'__cf_bm=e2_yCrwo4frh6Oq4ZufCEhNJ4lSGJ2.MMtk45X8lrMM-1707749745-1-AfWk8CyACc7aZo6GpCI82FBfI/wmPEFZLNO/Cr3eavTW3xKVFCS7G9jvwYTFLXjJr0cttYsXeLAnjwipw18R0Vo=; path=/; expires=Mon, 12-Feb-24 15:25:45 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None'), (b'Set-Cookie', b'_cfuvid=PyVVCGSMxTg1p.woYvHVVC9E3n69faOs5FOxaDdjXOM-1707749745711-0-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None'), (b'Server', b'cloudflare'), (b'CF-RAY', b'8545aca30c1fa22f-YYZ'), (b'Content-Encoding', b'gzip'), (b'alt-svc', b'h3=":443"; ma=86400')])
|
||||
INFO:httpx:HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
|
||||
DEBUG:httpcore.http11:receive_response_body.started request=<Request [b'POST']>
|
||||
DEBUG:httpcore.http11:receive_response_body.complete
|
||||
DEBUG:httpcore.http11:response_closed.started
|
||||
DEBUG:httpcore.http11:response_closed.complete
|
||||
DEBUG:openai._base_client:HTTP Request: POST https://api.openai.com/v1/chat/completions "200 OK"
|
||||
DEBUG:httpcore.connection:close.started
|
||||
DEBUG:httpcore.connection:close.complete
|
||||
"""
|
||||
@@ -1,99 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from instructor import patch
|
||||
from openai import AsyncOpenAI
|
||||
from langdetect import detect
|
||||
|
||||
docs = map(
|
||||
lambda x: x.strip(),
|
||||
"""
|
||||
Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։
|
||||
|
||||
---
|
||||
|
||||
Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap.
|
||||
|
||||
---
|
||||
|
||||
Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu.
|
||||
|
||||
---
|
||||
|
||||
Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor.
|
||||
|
||||
---
|
||||
|
||||
Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai.
|
||||
|
||||
---
|
||||
|
||||
Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains.
|
||||
|
||||
---
|
||||
|
||||
近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。
|
||||
|
||||
---
|
||||
|
||||
In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte.
|
||||
|
||||
---
|
||||
|
||||
पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है।
|
||||
|
||||
---
|
||||
|
||||
近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。
|
||||
""".split("---"),
|
||||
)
|
||||
|
||||
# Patch the OpenAI client to enable response_model
|
||||
client = patch(AsyncOpenAI())
|
||||
|
||||
|
||||
class GeneratedSummary(BaseModel):
|
||||
summary: str
|
||||
|
||||
|
||||
async def summarize_text(text: str):
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=GeneratedSummary,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Generate a concise summary in the language of the article. ",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Summarize the following text in a concise way:\n{text}",
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
return response.summary, text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
results = await asyncio.gather(*[summarize_text(doc) for doc in docs])
|
||||
for summary, doc in results:
|
||||
source_lang = detect(doc)
|
||||
target_lang = detect(summary)
|
||||
print(
|
||||
f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}"
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
"""
|
||||
Source: et, Summary: en, Match: False
|
||||
Source: tl, Summary: tl, Match: True
|
||||
Source: sw, Summary: en, Match: False
|
||||
Source: tr, Summary: tr, Match: True
|
||||
Source: vi, Summary: en, Match: False
|
||||
Source: fr, Summary: fr, Match: True
|
||||
Source: zh-cn, Summary: en, Match: False
|
||||
Source: de, Summary: de, Match: True
|
||||
Source: hi, Summary: en, Match: False
|
||||
Source: ja, Summary: en, Match: False
|
||||
"""
|
||||
@@ -1,102 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from instructor import patch
|
||||
from openai import AsyncOpenAI
|
||||
from langdetect import detect
|
||||
|
||||
docs = map(
|
||||
lambda x: x.strip(),
|
||||
"""
|
||||
Լեզվական մոդելները վերջին տարիներին դարձել են ավելի հարուստ և կատարյալ, հնարավորություն ընձեռելով ստեղծել սահուն և բնական տեքստեր, ինչպես նաև գերազանց արդյունքներ ցուցաբերել մեքենայական թարգմանության, հարցերի պատասխանման և ստեղծագործ տեքստերի ստեղծման նման տարբեր առաջադրանքներում։ Այս մոդելները մշակվում են հսկայական տեքստային տվյալների հիման վրա և կարող են բռնել բնական լեզվի կառուցվածքն ու նրբությունները՝ հեղափոխություն առաջացնելով համակարգիչների և մարդկանց միջև հաղորդակցության ոլորտում։
|
||||
|
||||
---
|
||||
|
||||
Mga modelo ng wika ay naging mas sopistikado sa nagdaang mga taon, na nagbibigay-daan sa pagbuo ng mga natural at madaling basahing teksto, at nagpapakita ng mahusay na pagganap sa iba't ibang gawain tulad ng awtomatikong pagsasalin, pagsagot sa mga tanong, at pagbuo ng malikhain na teksto. Ang mga modelo na ito ay sinanay sa napakalaking mga dataset ng teksto at kayang hulihin ang istruktura at mga nuances ng natural na wika. Ang mga pagpapabuti sa mga modelo ng wika ay maaaring magdulot ng rebolusyon sa komunikasyon sa pagitan ng mga computer at tao, at inaasahan ang higit pang pag-unlad sa hinaharap.
|
||||
|
||||
---
|
||||
|
||||
Ngaahi motuʻa lea kuo nau hoko ʻo fakaʻofoʻofa ange ʻi he ngaahi taʻu fakamuimui ni, ʻo fakafaingofuaʻi e fakatupu ʻo e ngaahi konga tohi ʻoku lelei mo fakanatula pea ʻoku nau fakahaaʻi ʻa e ngaahi ola lelei ʻi he ngaahi ngāue kehekehe ʻo hangē ko e liliu fakaʻētita, tali fehuʻi, mo e fakatupu ʻo e konga tohi fakaʻatamai. Ko e ako ʻa e ngaahi motuʻa ni ʻi he ngaahi seti ʻo e fakamatala tohi lahi pea ʻoku nau malava ʻo puke ʻa e fakafuofua mo e ngaahi meʻa iiki ʻo e lea fakanatula. ʻE lava ke fakatupu ʻe he ngaahi fakaleleiʻi ki he ngaahi motuʻa lea ha liliu lahi ʻi he fetu'utaki ʻi he vahaʻa ʻo e ngaahi komipiuta mo e kakai, pea ʻoku ʻamanaki ʻe toe fakalakalaka ange ia ʻi he kahaʻu.
|
||||
|
||||
---
|
||||
|
||||
Dil modelleri son yıllarda daha da gelişti, akıcı ve doğal metinler üretmeyi mümkün kılıyor ve makine çevirisi, soru cevaplama ve yaratıcı metin oluşturma gibi çeşitli görevlerde mükemmel performans gösteriyor. Bu modeller, devasa metin veri setlerinde eğitilir ve doğal dilin yapısını ve nüanslarını yakalayabilir. Dil modellerindeki iyileştirmeler, bilgisayarlar ve insanlar arasındaki iletişimde devrim yaratabilir ve gelecekte daha da ilerleme bekleniyor.
|
||||
|
||||
---
|
||||
|
||||
Mô hình ngôn ngữ đã trở nên tinh vi hơn trong những năm gần đây, cho phép tạo ra các văn bản trôi chảy và tự nhiên, đồng thời thể hiện hiệu suất xuất sắc trong các nhiệm vụ khác nhau như dịch máy, trả lời câu hỏi và tạo văn bản sáng tạo. Các mô hình này được huấn luyện trên các tập dữ liệu văn bản khổng lồ và có thể nắm bắt cấu trúc và sắc thái của ngôn ngữ tự nhiên. Những cải tiến trong mô hình ngôn ngữ có thể mang lại cuộc cách mạng trong giao tiếp giữa máy tính và con người, và người ta kỳ vọng sẽ có những tiến bộ hơn nữa trong tương lai.
|
||||
|
||||
---
|
||||
|
||||
Les modèles de langage sont devenus de plus en plus sophistiqués ces dernières années, permettant de générer des textes fluides et naturels, et de performer dans une variété de tâches telles que la traduction automatique, la réponse aux questions et la génération de texte créatif. Entraînés sur d'immenses ensembles de données textuelles, ces modèles sont capables de capturer la structure et les nuances du langage naturel, ouvrant la voie à une révolution dans la communication entre les ordinateurs et les humains.
|
||||
|
||||
---
|
||||
|
||||
近年来,语言模型变得越来越复杂,能够生成流畅自然的文本,并在机器翻译、问答和创意文本生成等各种任务中表现出色。这些模型在海量文本数据集上训练,可以捕捉自然语言的结构和细微差别。语言模型的改进有望彻底改变计算机和人类之间的交流方式,未来有望实现更大的突破。
|
||||
|
||||
---
|
||||
|
||||
In den letzten Jahren sind Sprachmodelle immer ausgefeilter geworden und können flüssige, natürlich klingende Texte generieren und in verschiedenen Aufgaben wie maschineller Übersetzung, Beantwortung von Fragen und Generierung kreativer Texte hervorragende Leistungen erbringen. Diese Modelle werden auf riesigen Textdatensätzen trainiert und können die Struktur und Nuancen natürlicher Sprache erfassen, was zu einer Revolution in der Kommunikation zwischen Computern und Menschen führen könnte.
|
||||
|
||||
---
|
||||
|
||||
पिछले कुछ वर्षों में भाषा मॉडल बहुत अधिक परिष्कृत हो गए हैं, जो प्राकृतिक और प्रवाहमय पाठ उत्पन्न कर सकते हैं, और मशीन अनुवाद, प्रश्नोत्तर, और रचनात्मक पाठ उत्पादन जैसे विभिन्न कार्यों में उत्कृष्ट प्रदर्शन कर सकते हैं। ये मॉडल विशाल पाठ डेटासेट पर प्रशिक्षित होते हैं और प्राकृतिक भाषा की संरचना और बारीकियों को समझ सकते हैं। भाषा मॉडल में सुधार कंप्यूटर और मानव के बीच संवाद में क्रांति ला सकता है, और भविष्य में और प्रगति की उम्मीद है।
|
||||
|
||||
---
|
||||
|
||||
近年、言語モデルは非常に洗練され、自然で流暢なテキストを生成できるようになり、機械翻訳、質問応答、クリエイティブなテキスト生成など、様々なタスクで優れたパフォーマンスを発揮しています。これらのモデルは膨大なテキストデータセットで学習され、自然言語の構造とニュアンスを捉えることができます。言語モデルの改善により、コンピューターと人間のコミュニケーションに革命が起こる可能性があり、将来のさらなる進歩が期待されています。
|
||||
""".split("---"),
|
||||
)
|
||||
|
||||
# Patch the OpenAI client to enable response_model
|
||||
client = patch(AsyncOpenAI())
|
||||
|
||||
|
||||
class GeneratedSummary(BaseModel):
|
||||
detected_language: str = Field(
|
||||
description="The language code of the original article. The summary must be generated in this same language.",
|
||||
)
|
||||
summary: str
|
||||
|
||||
|
||||
async def summarize_text(text: str):
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-3.5-turbo",
|
||||
response_model=GeneratedSummary,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Generate a concise summary in the language of the article. ",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Summarize the following text in a concise way:\n{text}",
|
||||
},
|
||||
],
|
||||
) # type: ignore
|
||||
return response.detected_language, response.summary, text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
async def main():
|
||||
results = await asyncio.gather(*[summarize_text(doc) for doc in docs])
|
||||
for lang, summary, doc in results:
|
||||
source_lang = detect(doc)
|
||||
target_lang = detect(summary)
|
||||
print(
|
||||
f"Source: {source_lang}, Summary: {target_lang}, Match: {source_lang == target_lang}, Detected: {lang}"
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
"""
|
||||
Source: et, Summary: et, Match: True, Detected: hy
|
||||
Source: tl, Summary: tl, Match: True, Detected: tl
|
||||
Source: sw, Summary: sw, Match: True, Detected: to
|
||||
Source: tr, Summary: tr, Match: True, Detected: tr
|
||||
Source: vi, Summary: vi, Match: True, Detected: vi
|
||||
Source: fr, Summary: fr, Match: True, Detected: fr
|
||||
Source: zh-cn, Summary: zh-cn, Match: True, Detected: zh
|
||||
Source: de, Summary: de, Match: True, Detected: de
|
||||
Source: hi, Summary: hi, Match: True, Detected: hi
|
||||
Source: ja, Summary: ja, Match: True, Detected: ja
|
||||
"""
|
||||
@@ -1,28 +0,0 @@
|
||||
from pydantic import BaseModel
|
||||
from mistralai.client import MistralClient
|
||||
from instructor import from_mistral
|
||||
from instructor.mode import Mode
|
||||
import os
|
||||
|
||||
|
||||
class UserDetails(BaseModel):
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
# enables `response_model` in chat call
|
||||
client = MistralClient(api_key=os.environ.get("MISTRAL_API_KEY"))
|
||||
instructor_client = from_mistral(
|
||||
client=client,
|
||||
model="mistral-large-latest",
|
||||
mode=Mode.TOOLS,
|
||||
max_tokens=1000,
|
||||
)
|
||||
|
||||
resp = instructor_client.messages.create(
|
||||
response_model=UserDetails,
|
||||
messages=[{"role": "user", "content": "Jason is 10"}],
|
||||
temperature=0,
|
||||
)
|
||||
|
||||
print(resp)
|
||||
@@ -1,116 +0,0 @@
|
||||
import instructor
|
||||
import enum
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from openai import OpenAI
|
||||
|
||||
client = instructor.from_openai(OpenAI())
|
||||
|
||||
|
||||
class Action(enum.Enum):
|
||||
CREATE = "create_task"
|
||||
DELETE = "close_task"
|
||||
UPDATE = "update_task"
|
||||
|
||||
|
||||
class Projects(enum.Enum):
|
||||
FRONTLINE_QA_AI = "frontline_qa_ai"
|
||||
FUTURE_OF_PROGRAMMING = "future_of_programming"
|
||||
PERSONAL_SITE = "personal_site"
|
||||
NORDIC_HAMSTRING_CURLS = "nordic_hamstring_curls"
|
||||
|
||||
|
||||
class Buckets(enum.Enum):
|
||||
FINANCE = "finance"
|
||||
PURVIEW_OPERATIONS = "purview_operations"
|
||||
TASKBOT = "taskbot"
|
||||
CHECKBOT = "checkbot"
|
||||
NIGHT_HACKING = "night_hacking"
|
||||
TICKLER = "tickler"
|
||||
|
||||
|
||||
class TaskAction(BaseModel):
|
||||
id: int
|
||||
method: Action = Field(
|
||||
description="Method of creating and closing a task: to close a task, only an ID is required"
|
||||
)
|
||||
waiting_on: Optional[list[int]] = Field(
|
||||
None, description="IDs of tasks that this task is waiting on"
|
||||
)
|
||||
name: Optional[str] = Field(None, description="Name of the task")
|
||||
notes: Optional[str] = Field(None, description="Notes about the task")
|
||||
bucket: Optional[Buckets] = Field(
|
||||
None, description="Bucket of the task, to set, or update"
|
||||
)
|
||||
project: Optional[Projects] = Field(
|
||||
None, description="Project of the task, to set, or update"
|
||||
)
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
text: str = Field(description="The text of the response")
|
||||
task_action: Optional[list[TaskAction]] = Field(
|
||||
description="The action to take on the task"
|
||||
)
|
||||
|
||||
|
||||
initial_messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an AI assistant. have the ability to create, update, and close tasks.",
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": """
|
||||
The task is below. When assisting the user, reference the details from this task.
|
||||
|
||||
[BEGIN TASK]
|
||||
id: 23
|
||||
Name: Create 10 new GIFs
|
||||
Description: Create 10 new GIFs for the Taskbot page on the user's personal site. They should be similar to the existing GIFs, but with different use cases.
|
||||
Projects: Personal site
|
||||
Buckets: Taskbot
|
||||
Updates:
|
||||
[BEGIN UPDATE]
|
||||
**User Update - September 01, 2023 03:58:00 PM EDT**
|
||||
The user plans to create the GIFs in the background as they work through their daily tasks. They aim to produce about one to two GIFs per day. If this plan doesn't work, they will reconsider their strategy.
|
||||
[END UPDATE]
|
||||
[END TASK]
|
||||
""",
|
||||
},
|
||||
{"role": "assistant", "content": "What's up with this task?"},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Change it to 20, then make a new task for when its done make 20 more that moves.",
|
||||
},
|
||||
]
|
||||
|
||||
response: Response = client.chat.completions.create(
|
||||
messages=initial_messages, response_model=Response, model="gpt-4"
|
||||
) # type: ignore
|
||||
|
||||
print(response.model_dump_json(indent=2))
|
||||
{
|
||||
"text": "Updating task to create 20 GIFs and creating a new task to create an additional 20 animated GIFs after the initial task is done.",
|
||||
"task_action": [
|
||||
{
|
||||
"id": 23,
|
||||
"method": "update_task",
|
||||
"waiting_on": None,
|
||||
"name": "Create 20 new GIFs",
|
||||
"notes": "The user increased the number of GIFs from 10 to 20. They plan to create these as they work through their daily tasks, creating about one to two GIFs per day. If this plan doesn't work, they will reconsider their strategy.",
|
||||
"bucket": "taskbot",
|
||||
"project": "personal_site",
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"method": "create_task",
|
||||
"waiting_on": [23],
|
||||
"name": "Create 20 new animated GIFs",
|
||||
"notes": "The task will be initiated once the task with id 23 is completed.",
|
||||
"bucket": "taskbot",
|
||||
"project": "personal_site",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import erdantic as erd
|
||||
|
||||
from segment_search_queries import MultiSearch
|
||||
|
||||
diagram = erd.create(MultiSearch)
|
||||
diagram.draw("examples/segment_search_queries/schema.png")
|
||||
|
Before Width: | Height: | Size: 18 KiB |