diff --git a/README.md b/README.md index d7f8233..f8a34db 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,9 @@ for point in run.negative_points: WorkflowAI supports a long list of models. The source of truth for models we support is on [workflowai.com](https://workflowai.com). The [Model enum](./workflowai/core/domain/model.py) is a good indication of what models are supported at the time of the sdk release, although it may be missing some models since new ones are added all the time. -You can set the model explicitly in the agent decorator: +You can specify the model in two ways: + +1. In the agent decorator: ```python from workflowai import Model @@ -194,6 +196,19 @@ async def analyze_call_feedback(input: CallFeedbackInput) -> CallFeedbackOutput: ... ``` +2. As a function parameter when calling the agent: + +```python +@workflowai.agent(id="analyze-call-feedback") +async def analyze_call_feedback(input: CallFeedbackInput) -> CallFeedbackOutput: + ... + +# Call with specific model +result = await analyze_call_feedback(input_data, model=Model.GPT_4O_LATEST) +``` + +This flexibility allows you to either fix the model in the agent definition or dynamically choose different models at runtime. + > Models do not become invalid on WorkflowAI. When a model is retired, it will be replaced dynamically by > a newer version of the same model with the same or a lower price so calling the api with > a retired model will always work. diff --git a/examples/15_text_to_sql.py b/examples/15_text_to_sql.py new file mode 100644 index 0000000..908b61e --- /dev/null +++ b/examples/15_text_to_sql.py @@ -0,0 +1,162 @@ +""" +This example demonstrates how to convert natural language questions to SQL queries. +It uses a sample e-commerce database schema and shows how to generate safe and efficient SQL queries. + +Like example 14 (templated instructions), this example shows how to use variables in the agent's +instructions. The template variables ({{ db_schema }} and {{ question }}) are automatically populated +from the input model's fields, allowing the instructions to adapt based on the input. + +The example includes: +1. Simple SELECT query with conditions +2. JOIN query with aggregation +3. Complex query with multiple JOINs, grouping, and ordering +""" + +import asyncio + +from pydantic import BaseModel, Field + +import workflowai +from workflowai import Model, Run + + +class SQLGenerationInput(BaseModel): + """Input model for the SQL generation agent.""" + + db_schema: str = Field( + description="The complete SQL schema with CREATE TABLE statements", + ) + question: str = Field( + description="The natural language question to convert to SQL", + ) + + +class SQLGenerationOutput(BaseModel): + """Output model containing the generated SQL query and explanation.""" + + sql_query: str = Field( + description="The generated SQL query", + ) + explanation: str = Field( + description="Explanation of what the query does and why certain choices were made", + ) + tables_used: list[str] = Field( + description="List of tables referenced in the query", + ) + + +@workflowai.agent( + id="text-to-sql", + model=Model.CLAUDE_3_5_SONNET_LATEST, +) +async def generate_sql(review_input: SQLGenerationInput) -> Run[SQLGenerationOutput]: + """ + Convert natural language questions to SQL queries based on the provided schema. + + You are a SQL expert that converts natural language questions into safe and efficient SQL queries. + The queries should be compatible with standard SQL databases. + + Important guidelines: + 1. NEVER trust user input directly in queries to prevent SQL injection + 2. Use proper quoting and escaping for string values + 3. Use meaningful table aliases for better readability + 4. Format queries with proper indentation and line breaks + 5. Use explicit JOIN conditions (no implicit joins) + 6. Include column names in GROUP BY rather than positions + + Schema: + {{ db_schema }} + + Question to convert to SQL: + {{ question }} + + Please provide: + 1. A safe and efficient SQL query + 2. An explanation of the query and any important considerations + 3. List of tables used in the query + """ + ... + + +async def main(): + # Example schema for an e-commerce database + schema = """ + CREATE TABLE customers ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE products ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + price DECIMAL(10,2) NOT NULL, + category TEXT NOT NULL, + stock_quantity INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE orders ( + id INTEGER PRIMARY KEY, + customer_id INTEGER NOT NULL, + order_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL DEFAULT 'pending', + total_amount DECIMAL(10,2) NOT NULL, + FOREIGN KEY (customer_id) REFERENCES customers(id) + ); + + CREATE TABLE order_items ( + id INTEGER PRIMARY KEY, + order_id INTEGER NOT NULL, + product_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + unit_price DECIMAL(10,2) NOT NULL, + FOREIGN KEY (order_id) REFERENCES orders(id), + FOREIGN KEY (product_id) REFERENCES products(id) + ); + """ + + # Example 1: Simple SELECT with conditions + print("\nExample 1: Find expensive products") + print("-" * 50) + run = await generate_sql( + SQLGenerationInput( + db_schema=schema, + question="Show me all products that cost more than $100, ordered by price descending", + ), + ) + print(run) + + # Example 2: JOIN with aggregation + print("\nExample 2: Customer order summary") + print("-" * 50) + run = await generate_sql( + SQLGenerationInput( + db_schema=schema, + question=( + "List all customers with their total number of orders and total spend, " + "only showing customers who have made at least 2 orders" + ), + ), + ) + print(run) + + # Example 3: Complex query + print("\nExample 3: Product category analysis") + print("-" * 50) + run = await generate_sql( + SQLGenerationInput( + db_schema=schema, + question=( + "What are the top 3 product categories by revenue in the last 30 days, " + "including the number of unique customers who bought from each category?" + ), + ), + ) + print(run) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/16_multi_model_consensus.py b/examples/16_multi_model_consensus.py new file mode 100644 index 0000000..da0fbbf --- /dev/null +++ b/examples/16_multi_model_consensus.py @@ -0,0 +1,118 @@ +""" +This example demonstrates how to ask the same question to multiple different LLMs +and then combine their responses into a single coherent answer using another LLM. + +The example uses three different models for answering: +- GPT-4O Mini +- Gemini 2.0 Flash +- Llama 3.3 70B + +Then uses O3 Mini (with medium reasoning effort) to analyze and combine their responses. +""" + +import asyncio + +from pydantic import BaseModel, Field + +import workflowai +from workflowai import Model, Run + + +class MultiModelInput(BaseModel): + """Input model containing the question to ask all models.""" + + question: str = Field( + description="The question to ask all models", + ) + model_name: str = Field( + description="Name of the model providing the response", + ) + + +class ModelResponse(BaseModel): + """Response from an individual model.""" + + model_name: str = Field(description="Name of the model that provided this response") + response: str = Field(description="The model's response to the question") + + +class CombinerInput(BaseModel): + """Input for the response combiner.""" + + responses: list[ModelResponse] = Field(description="List of responses to combine") + + +class CombinedOutput(BaseModel): + """Final output combining responses from all models.""" + + combined_answer: str = Field( + description="Synthesized answer combining insights from all models", + ) + explanation: str = Field( + description="Explanation of how the responses were combined and why", + ) + + +@workflowai.agent( + id="question-answerer", +) +async def get_model_response(query: MultiModelInput) -> Run[ModelResponse]: + """Get response from the specified model.""" + ... + + +@workflowai.agent( + id="response-combiner", + model=Model.O3_MINI_2025_01_31_MEDIUM_REASONING_EFFORT, +) +async def combine_responses(responses_input: CombinerInput) -> Run[CombinedOutput]: + """ + Analyze and combine responses from multiple models into a single coherent answer. + + You are an expert at analyzing and synthesizing information from multiple sources. + Your task is to: + 1. Review the responses from different models + 2. Identify key insights and unique perspectives from each + 3. Create a comprehensive answer that combines the best elements + 4. Explain your synthesis process + + Please ensure the combined answer is: + - Accurate and well-reasoned + - Incorporates unique insights from each model + - Clear and coherent + - Properly attributed when using specific insights + """ + ... + + +async def main(): + # Example: Scientific explanation + print("\nExample: Scientific Concept") + print("-" * 50) + question = "What is dark matter and why is it important for our understanding of the universe?" + + # Get responses from all models + models = [ + (Model.GPT_4O_MINI_LATEST, "GPT-4O Mini"), + (Model.GEMINI_2_0_FLASH_LATEST, "Gemini 2.0 Flash"), + (Model.LLAMA_3_3_70B, "Llama 3.3 70B"), + ] + + responses = [] + for model, model_name in models: + run = await get_model_response( + MultiModelInput( + question=question, + model_name=model_name, + ), + model=model, + ) + responses.append(run.output) + + # Combine responses + combined = await combine_responses(CombinerInput(responses=responses)) + print(combined) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/17_multi_model_consensus_with_tools.py b/examples/17_multi_model_consensus_with_tools.py new file mode 100644 index 0000000..761e284 --- /dev/null +++ b/examples/17_multi_model_consensus_with_tools.py @@ -0,0 +1,149 @@ +""" +This example builds upon example 16 (multi_model_consensus.py) but gives more agency to +the response-combiner. While example 16 uses a fixed set of models defined in the main +function, this version lets the response-combiner itself decide which models are most +appropriate for the question. + +This example demonstrates agent delegation, where one agent (the response-combiner) can +dynamically invoke another agent (get_model_response) via tools. By providing the ask_model +function as a tool, the response-combiner can: + +1. Choose which models to query based on the nature and complexity of the question +2. Adapt its strategy based on initial responses (e.g. asking specialized models for clarification) +3. Use its own reasoning to determine when it has enough perspectives to synthesize an answer + +This hierarchical approach allows the response-combiner agent to orchestrate multiple model +queries by delegating to the get_model_response agent through tool calls. The response-combiner +acts as a "manager" agent that can strategically coordinate with "worker" agents to gather +the insights needed. +""" + +import asyncio + +from pydantic import BaseModel, Field + +import workflowai +from workflowai import Model, Run + + +class AskModelInput(BaseModel): + """Input for asking a question to a specific model.""" + + question: str = Field(description="The question to ask") + model: Model = Field(description="The model to ask the question to") + + +class AskModelOutput(BaseModel): + """Output from asking a question to a model.""" + + response: str = Field(description="The model's response to the question") + + +# This function acts as a tool that allows one agent to delegate to another agent. +# The response-combiner agent can use this tool to dynamically query different models +# through the get_model_response agent. This creates a hierarchy where the +# response-combiner orchestrates multiple model queries by delegating to get_model_response. +async def ask_model(query_input: AskModelInput) -> AskModelOutput: + """Ask a specific model a question and get its response.""" + run = await get_model_response( + MultiModelInput( + question=query_input.question, + ), + model=query_input.model, + ) + return AskModelOutput(response=run.output.response) + + +class MultiModelInput(BaseModel): + """Input model containing the question to ask all models.""" + + question: str = Field( + description="The question to ask all models", + ) + + +class ModelResponse(BaseModel): + """Response from an individual model.""" + + response: str = Field(description="The model's response to the question") + + +class CombinerInput(BaseModel): + """Input for the response combiner.""" + + original_question: str = Field(description="The question to ask multiple models") + + +class CombinedOutput(BaseModel): + """Final output combining responses from all models.""" + + combined_answer: str = Field( + description="Synthesized answer combining insights from all models", + ) + explanation: str = Field( + description="Explanation of how the responses were combined and why", + ) + models_used: list[str] = Field( + description="List of models whose responses were combined", + ) + + +@workflowai.agent( + id="question-answerer", +) +async def get_model_response(query: MultiModelInput) -> Run[ModelResponse]: + """ + Make sure to: + 1. Provide a clear and detailed response + 2. Stay focused on the question asked + 3. Be specific about any assumptions made + 4. Highlight areas of uncertainty + """ + ... + + +@workflowai.agent( + id="response-combiner", + model=Model.GPT_4O_MINI_LATEST, + tools=[ask_model], +) +async def combine_responses(responses_input: CombinerInput) -> Run[CombinedOutput]: + """ + Analyze and combine responses from multiple models into a single coherent answer. + You should ask at least 3 different models to get a diverse set of perspectives. + + You are an expert at analyzing and synthesizing information from multiple sources. + Your task is to: + 1. Review the responses from different models (at least 3) + 2. Identify key insights and unique perspectives from each + 3. Create a comprehensive answer that combines the best elements + 4. Explain your synthesis process + 5. List all models whose responses were used in the synthesis + + Please ensure the combined answer is: + - Accurate and well-reasoned + - Incorporates unique insights from each model + - Clear and coherent + - Properly attributed when using specific insights + - Based on responses from at least 3 different models + """ + ... + + +async def main(): + # Example: Scientific explanation + print("\nExample: Scientific Concept") + print("-" * 50) + question = "What is dark matter and why is it important for our understanding of the universe?" + + # Let the response-combiner handle asking the models + combined = await combine_responses( + CombinerInput( + original_question=question, + ), + ) + print(combined) + + +if __name__ == "__main__": + asyncio.run(main())