From 046d250ef81a862a0a92fe0b7eab646ceb85bd9d Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 06:46:27 -0600 Subject: [PATCH 01/13] Create 13_templated_instructions.py adds an example on how to use variables in instructions --- examples/13_templated_instructions.py | 176 ++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 examples/13_templated_instructions.py diff --git a/examples/13_templated_instructions.py b/examples/13_templated_instructions.py new file mode 100644 index 0000000..335eaea --- /dev/null +++ b/examples/13_templated_instructions.py @@ -0,0 +1,176 @@ +""" +This example demonstrates how to use templated instructions that adapt based on input variables. +The template variables are automatically populated from the input model's fields. + +The templating uses Jinja2 syntax (server-side rendering): +- {{ variable }} for variable substitution +- {% if condition %} ... {% endif %} for conditionals +- {% for item in items %} ... {% endfor %} for loops +- {{ loop.index }} for loop indices + +For full Jinja2 template syntax documentation, see: +https://jinja.palletsprojects.com/en/stable/ + +It showcases: +1. Simple variable substitution +2. Conditional logic +3. Loops +4. Nested conditionals +""" + +import asyncio +from typing import List + +from pydantic import BaseModel, Field + +import workflowai +from workflowai import Model, Run + + +class CodeReviewInput(BaseModel): + """Input model for the code review agent.""" + language: str = Field( + description="The programming language of the code to review", + examples=["python", "javascript", "typescript"], + ) + code: str = Field( + description="The code to review", + ) + style_guide: str = Field( + description="The style guide to follow", + examples=["PEP 8", "Google Style", "Airbnb"], + ) + is_production: bool = Field( + description="Whether this is a production code review", + default=False, + ) + required_checks: List[str] = Field( + description="List of specific checks to perform", + default=["code style", "performance", "maintainability"], + ) + security_level: str = Field( + description="Required security level", + default="standard", + examples=["standard", "high"], + ) + + +class CodeReviewOutput(BaseModel): + """Output model containing the code review results.""" + overall_assessment: str = Field( + description="Overall assessment of the code quality", + ) + style_violations: List[str] = Field( + description="List of style guide violations", + ) + security_issues: List[str] = Field( + description="List of security concerns", + default_factory=list, + ) + suggested_improvements: List[str] = Field( + description="List of suggested improvements", + ) + + +@workflowai.agent( + id="templated-code-reviewer", + model=Model.CLAUDE_3_5_SONNET_LATEST, +) +async def review_code(review_input: CodeReviewInput) -> Run[CodeReviewOutput]: + """ + Review code based on specified parameters and guidelines. + + You are a code reviewer for {{ language }} code. + Please review the code according to the {{ style_guide }} style guide. + + {% if is_production %} + This is a PRODUCTION code review - please be extra thorough and strict about best practices. + {% else %} + This is a development code review - focus on maintainability and clarity. + {% endif %} + + Required checks to perform: + {% for check in required_checks %}{{ loop.index }}. {{ check }} + {% endfor %} + + {% if security_level == "high" %} + Additional security requirements: + - Must follow secure coding practices + - Check for potential security vulnerabilities + - Ensure all inputs are properly sanitized + {% endif %} + + Guidelines: + 1. Check for adherence to {{ style_guide }} conventions + 2. Look for potential bugs and performance issues + 3. Suggest improvements while keeping the {{ language }} best practices in mind + + {% if language == "python" %} + Python-specific checks: + - Type hints usage + - PEP 8 compliance + - Docstring format + {% elif language == "javascript" or language == "typescript" %} + JavaScript/TypeScript-specific checks: + - ESLint rules compliance + - Modern ES6+ features usage + - Browser compatibility + {% endif %} + + Please analyze the following code and provide: + 1. An overall assessment + 2. Style guide violations + 3. Security issues (if any) + 4. Suggested improvements + """ + ... + + +async def main(): + # Example 1: Python code review for development + print("\nExample 1: Python Development Code Review") + print("-" * 50) + python_code = """ +def calculate_sum(numbers): + result = 0 + for n in numbers: + result = result + n + return result + """ + + run = await review_code( + CodeReviewInput( + language="python", + code=python_code, + style_guide="PEP 8", + required_checks=["type hints", "docstring", "performance"], + ), + ) + print(run) + + # Example 2: TypeScript production code with high security + print("\nExample 2: TypeScript Production Code Review (High Security)") + print("-" * 50) + typescript_code = """ +function processUserData(data: any) { + const userId = data.id; + const query = `SELECT * FROM users WHERE id = ${userId}`; + return executeQuery(query); +} + """ + + run = await review_code( + CodeReviewInput( + language="typescript", + code=typescript_code, + style_guide="Airbnb", + is_production=True, + security_level="high", + required_checks=["security", "type safety", "SQL injection"], + ), + ) + print(run) + + +if __name__ == "__main__": + asyncio.run(main()) From 3dc3793e9a75782719832c6ce1d056c9dc503ae0 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 06:50:16 -0600 Subject: [PATCH 02/13] rename to example 14 --- ...{13_templated_instructions.py => 14_templated_instructions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/{13_templated_instructions.py => 14_templated_instructions.py} (100%) diff --git a/examples/13_templated_instructions.py b/examples/14_templated_instructions.py similarity index 100% rename from examples/13_templated_instructions.py rename to examples/14_templated_instructions.py From 1da33359af89e2c16cac3212d5a65d021f8cfb87 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 07:29:20 -0600 Subject: [PATCH 03/13] Create 15_text_to_sql.py --- examples/15_text_to_sql.py | 163 +++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 examples/15_text_to_sql.py diff --git a/examples/15_text_to_sql.py b/examples/15_text_to_sql.py new file mode 100644 index 0000000..cb073a9 --- /dev/null +++ b/examples/15_text_to_sql.py @@ -0,0 +1,163 @@ +""" +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 ({{ 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 typing import List + +from pydantic import BaseModel, Field + +import workflowai +from workflowai import Model, Run + + +class SQLGenerationInput(BaseModel): + """Input model for the SQL generation agent.""" + + 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: + {{ 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( + 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( + 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( + 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()) From 77a24615039753563340b28236addd20ddc38828 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 07:29:22 -0600 Subject: [PATCH 04/13] Create 16_multi_model_consensus.py --- examples/16_multi_model_consensus.py | 128 +++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 examples/16_multi_model_consensus.py diff --git a/examples/16_multi_model_consensus.py b/examples/16_multi_model_consensus.py new file mode 100644 index 0000000..9d1e349 --- /dev/null +++ b/examples/16_multi_model_consensus.py @@ -0,0 +1,128 @@ +""" +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 typing import List + +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", + ) + + +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.""" + individual_responses: List[ModelResponse] = Field( + description="List of responses from each individual model", + ) + 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="gpt4o-mini-agent", + model=Model.GPT_4O_MINI_LATEST, +) +async def get_gpt4_response(query: MultiModelInput) -> Run[ModelResponse]: + """Get response from GPT-4O Mini model.""" + ... + + +@workflowai.agent( + id="gemini-agent", + model=Model.GEMINI_2_0_FLASH_LATEST, +) +async def get_gemini_response(query: MultiModelInput) -> Run[ModelResponse]: + """Get response from Gemini 2.0 Flash model.""" + ... + + +@workflowai.agent( + id="llama-agent", + model=Model.LLAMA_3_3_70B, +) +async def get_llama_response(query: MultiModelInput) -> Run[ModelResponse]: + """Get response from Llama 3.3 70B 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 + gpt4_run = await get_gpt4_response(MultiModelInput(question=question)) + gemini_run = await get_gemini_response(MultiModelInput(question=question)) + llama_run = await get_llama_response(MultiModelInput(question=question)) + + # Combine responses + combined = await combine_responses(CombinerInput( + responses=[ + gpt4_run.output, + gemini_run.output, + llama_run.output, + ], + )) + print(combined) + + +if __name__ == "__main__": + asyncio.run(main()) From a7556a7751b65d75c8a9657590004eb721387fca Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 07:31:38 -0600 Subject: [PATCH 05/13] Update 15_text_to_sql.py --- examples/15_text_to_sql.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/15_text_to_sql.py b/examples/15_text_to_sql.py index cb073a9..92b59b9 100644 --- a/examples/15_text_to_sql.py +++ b/examples/15_text_to_sql.py @@ -3,7 +3,7 @@ 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 ({{ schema }} and {{ question }}) are automatically populated +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: @@ -24,7 +24,7 @@ class SQLGenerationInput(BaseModel): """Input model for the SQL generation agent.""" - schema: str = Field( + db_schema: str = Field( description="The complete SQL schema with CREATE TABLE statements", ) question: str = Field( @@ -66,7 +66,7 @@ async def generate_sql(review_input: SQLGenerationInput) -> Run[SQLGenerationOut 6. Include column names in GROUP BY rather than positions Schema: - {{ schema }} + {{ db_schema }} Question to convert to SQL: {{ question }} @@ -124,7 +124,7 @@ async def main(): print("-" * 50) run = await generate_sql( SQLGenerationInput( - schema=schema, + db_schema=schema, question="Show me all products that cost more than $100, ordered by price descending", ), ) @@ -135,7 +135,7 @@ async def main(): print("-" * 50) run = await generate_sql( SQLGenerationInput( - schema=schema, + 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" @@ -149,7 +149,7 @@ async def main(): print("-" * 50) run = await generate_sql( SQLGenerationInput( - schema=schema, + 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?" From d11741dedcc548d082c1de35bceea110e2331dcd Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 09:33:36 -0600 Subject: [PATCH 06/13] Update 16_multi_model_consensus.py use model= in the agent function. --- examples/16_multi_model_consensus.py | 59 +++++++++++----------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/examples/16_multi_model_consensus.py b/examples/16_multi_model_consensus.py index 9d1e349..a7df9d5 100644 --- a/examples/16_multi_model_consensus.py +++ b/examples/16_multi_model_consensus.py @@ -24,6 +24,9 @@ class MultiModelInput(BaseModel): 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): @@ -39,9 +42,6 @@ class CombinerInput(BaseModel): class CombinedOutput(BaseModel): """Final output combining responses from all models.""" - individual_responses: List[ModelResponse] = Field( - description="List of responses from each individual model", - ) combined_answer: str = Field( description="Synthesized answer combining insights from all models", ) @@ -51,36 +51,16 @@ class CombinedOutput(BaseModel): @workflowai.agent( - id="gpt4o-mini-agent", - model=Model.GPT_4O_MINI_LATEST, -) -async def get_gpt4_response(query: MultiModelInput) -> Run[ModelResponse]: - """Get response from GPT-4O Mini model.""" - ... - - -@workflowai.agent( - id="gemini-agent", - model=Model.GEMINI_2_0_FLASH_LATEST, + id="question-answerer", ) -async def get_gemini_response(query: MultiModelInput) -> Run[ModelResponse]: - """Get response from Gemini 2.0 Flash model.""" - ... - - -@workflowai.agent( - id="llama-agent", - model=Model.LLAMA_3_3_70B, -) -async def get_llama_response(query: MultiModelInput) -> Run[ModelResponse]: - """Get response from Llama 3.3 70B model.""" +async def get_model_response(query: MultiModelInput, *, model: Model) -> 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]: """ @@ -109,18 +89,25 @@ async def main(): question = "What is dark matter and why is it important for our understanding of the universe?" # Get responses from all models - gpt4_run = await get_gpt4_response(MultiModelInput(question=question)) - gemini_run = await get_gemini_response(MultiModelInput(question=question)) - llama_run = await get_llama_response(MultiModelInput(question=question)) + 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=[ - gpt4_run.output, - gemini_run.output, - llama_run.output, - ], - )) + combined = await combine_responses(CombinerInput(responses=responses)) print(combined) From 491d59239ae87b4c0b4f53c66ddb09408ccb9900 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 09:37:30 -0600 Subject: [PATCH 07/13] Update README.md --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fab7f1..4ee58aa 100644 --- a/README.md +++ b/README.md @@ -182,8 +182,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 @@ -192,6 +193,18 @@ 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, *, model: Model) -> 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. From 2f8244507478984149c63afcd27d9356e16a4c23 Mon Sep 17 00:00:00 2001 From: Pierre Date: Mon, 10 Feb 2025 10:04:16 -0600 Subject: [PATCH 08/13] Create 17_multi_model_consensus_with_tools.py --- .../17_multi_model_consensus_with_tools.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 examples/17_multi_model_consensus_with_tools.py 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..613160c --- /dev/null +++ b/examples/17_multi_model_consensus_with_tools.py @@ -0,0 +1,135 @@ +""" +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. + +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 approach leverages the LLM's ability to reason about which models would be most helpful +for different types of questions. +""" + +import asyncio +from typing import List + +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") + + +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, *, model: Model) -> 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()) From 9a400f0eb86e7690fbbef25787243713533b402e Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 11 Feb 2025 17:49:50 -0600 Subject: [PATCH 09/13] Update 17_multi_model_consensus_with_tools.py --- examples/17_multi_model_consensus_with_tools.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/17_multi_model_consensus_with_tools.py b/examples/17_multi_model_consensus_with_tools.py index 613160c..54f0314 100644 --- a/examples/17_multi_model_consensus_with_tools.py +++ b/examples/17_multi_model_consensus_with_tools.py @@ -4,13 +4,18 @@ function, this version lets the response-combiner itself decide which models are most appropriate for the question. -By providing the ask_model function as a tool, the response-combiner can: +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 approach leverages the LLM's ability to reason about which models would be most helpful -for different types of questions. +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 @@ -32,7 +37,10 @@ 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( From 4865804f6b2a25663d55479b3569162dc3a6d3ac Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 13 Feb 2025 19:03:35 -0600 Subject: [PATCH 10/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ee58aa..28b0ec9 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ 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, *, model: Model) -> CallFeedbackOutput: +async def analyze_call_feedback(input: CallFeedbackInput) -> CallFeedbackOutput: ... # Call with specific model From b4103815685f1b48268e03f873a923baf0c550c8 Mon Sep 17 00:00:00 2001 From: Pierre Date: Thu, 13 Feb 2025 19:07:21 -0600 Subject: [PATCH 11/13] remove additional fields "*, model:" from agent --- examples/16_multi_model_consensus.py | 2 +- examples/17_multi_model_consensus_with_tools.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/16_multi_model_consensus.py b/examples/16_multi_model_consensus.py index a7df9d5..6027782 100644 --- a/examples/16_multi_model_consensus.py +++ b/examples/16_multi_model_consensus.py @@ -53,7 +53,7 @@ class CombinedOutput(BaseModel): @workflowai.agent( id="question-answerer", ) -async def get_model_response(query: MultiModelInput, *, model: Model) -> Run[ModelResponse]: +async def get_model_response(query: MultiModelInput) -> Run[ModelResponse]: """Get response from the specified model.""" ... diff --git a/examples/17_multi_model_consensus_with_tools.py b/examples/17_multi_model_consensus_with_tools.py index 54f0314..56578fb 100644 --- a/examples/17_multi_model_consensus_with_tools.py +++ b/examples/17_multi_model_consensus_with_tools.py @@ -85,7 +85,7 @@ class CombinedOutput(BaseModel): @workflowai.agent( id="question-answerer", ) -async def get_model_response(query: MultiModelInput, *, model: Model) -> Run[ModelResponse]: +async def get_model_response(query: MultiModelInput) -> Run[ModelResponse]: """ Make sure to: 1. Provide a clear and detailed response From c60577d0ba66a881fbeaca619704d4b58e521ff4 Mon Sep 17 00:00:00 2001 From: Guillaume Aquilina Date: Fri, 14 Feb 2025 10:44:54 -0500 Subject: [PATCH 12/13] fix: linter issues --- README.md | 17 +++++++++-------- examples/08_pdf_agent.py | 3 +-- examples/11_ecommerce_chatbot.py | 6 +++--- examples/13_rag.py | 15 +++++++++++---- examples/14_templated_instructions.py | 11 ++++++----- examples/15_text_to_sql.py | 3 +-- examples/16_multi_model_consensus.py | 7 +++++-- examples/17_multi_model_consensus_with_tools.py | 10 ++++++++-- examples/workflows/chain_of_agents.py | 5 ++--- examples/workflows/evaluator_optimizer.py | 10 +++++----- examples/workflows/orchestrator_worker.py | 8 ++++---- examples/workflows/parallel_processing.py | 14 +++++++------- tests/e2e/run_test.py | 3 ++- tests/e2e/tools_test.py | 2 +- tests/integration/run_test.py | 2 +- tests/integration/stream_agent_builder_test.py | 2 +- workflowai/core/client/_api.py | 3 ++- workflowai/core/client/_fn_utils.py | 11 ++++------- workflowai/core/client/_fn_utils_test.py | 3 ++- workflowai/core/client/_types.py | 2 +- workflowai/core/fields/local_date_time_test.py | 2 +- workflowai/core/fields/zone_info.py | 2 +- workflowai/core/fields/zone_info_test.py | 3 ++- workflowai/core/utils/_iter.py | 4 ++-- workflowai/core/utils/_tools_test.py | 2 +- 25 files changed, 83 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index efce7b9..1e5b6c3 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,11 @@ class FeedbackPoint(BaseModel): # Model representing the structured analysis of the customer feedback call class CallFeedbackOutput(BaseModel): """Structured analysis of the customer feedback call.""" - positive_points: List[FeedbackPoint] = Field( + positive_points: list[FeedbackPoint] = Field( default_factory=list, description="List of positive feedback points, each with a supporting quote." ) - negative_points: List[FeedbackPoint] = Field( + negative_points: list[FeedbackPoint] = Field( default_factory=list, description="List of negative feedback points, each with a supporting quote." ) @@ -187,6 +187,7 @@ WorkflowAI supports a long list of models. The source of truth for models we sup You can specify the model in two ways: 1. In the agent decorator: + ```python from workflowai import Model @@ -196,6 +197,7 @@ 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: @@ -280,7 +282,6 @@ async for chunk in analyze_call_feedback(feedback_input): > For example, a function with the signature `async def foo() -> AsyncIterator[int]` may be called > `async for c in await foo():...` in certain cases... - #### Streaming the run object Use `AsyncIterator[Run[Output]]` to get the **run** object as it is generated, which allows you, for the **last chunk**, to access the cost and duration of the run. @@ -344,7 +345,7 @@ class PDFQuestionInput(BaseModel): class PDFAnswerOutput(BaseModel): answer: str = Field(description="The answer to the question based on the PDF content") - quotes: List[str] = Field(description="Relevant quotes from the PDF that support the answer") + quotes: list[str] = Field(description="Relevant quotes from the PDF that support the answer") @workflowai.agent(id="pdf-answer", model=Model.CLAUDE_3_5_SONNET_LATEST) async def answer_pdf_question(input: PDFQuestionInput) -> PDFAnswerOutput: @@ -623,8 +624,8 @@ values. ```python class CallFeedbackOutputStrict(BaseModel): - positive_points: List[FeedbackPoint] - negative_points: List[FeedbackPoint] + positive_points: list[FeedbackPoint] + negative_points: list[FeedbackPoint] @workflowai.agent() async def analyze_call_feedback_strict(input: CallFeedbackInput) -> CallFeedbackOutputStrict: @@ -641,8 +642,8 @@ except WorkflowAIError as e: print(e.code) # "invalid_generation" error code means that the generation did not match the schema class CallFeedbackOutputTolerant(BaseModel): - positive_points: List[FeedbackPoint] = Field(default_factory=list) - negative_points: List[FeedbackPoint] = Field(default_factory=list) + positive_points: list[FeedbackPoint] = Field(default_factory=list) + negative_points: list[FeedbackPoint] = Field(default_factory=list) @workflowai.agent() async def analyze_call_feedback_tolerant(input: CallFeedbackInput) -> CallFeedbackOutputTolerant: diff --git a/examples/08_pdf_agent.py b/examples/08_pdf_agent.py index b8b672b..f2e849d 100644 --- a/examples/08_pdf_agent.py +++ b/examples/08_pdf_agent.py @@ -1,6 +1,5 @@ import asyncio import os -from typing import List from dotenv import load_dotenv from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] @@ -18,7 +17,7 @@ class PDFQuestionInput(BaseModel): class PDFAnswerOutput(BaseModel): answer: str = Field(description="The answer to the question based on the PDF content") - quotes: List[str] = Field(description="Relevant quotes from the PDF that support the answer") + quotes: list[str] = Field(description="Relevant quotes from the PDF that support the answer") @workflowai.agent(id="pdf-answer", model=Model.CLAUDE_3_5_SONNET_LATEST) diff --git a/examples/11_ecommerce_chatbot.py b/examples/11_ecommerce_chatbot.py index b9aefec..b26bc7c 100644 --- a/examples/11_ecommerce_chatbot.py +++ b/examples/11_ecommerce_chatbot.py @@ -8,7 +8,7 @@ import asyncio from enum import Enum -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -64,7 +64,7 @@ class Message(BaseModel): "Based on your requirements, here are some great headphone options...", ], ) - recommended_products: Optional[List[Product]] = Field( + recommended_products: Optional[list[Product]] = Field( default=None, description="Product recommendations included with this message, if any", ) @@ -85,7 +85,7 @@ class ChatbotOutput(BaseModel): class ChatInput(BaseModel): """Input model containing the user's message and conversation history.""" - conversation_history: Optional[List[Message]] = Field( + conversation_history: Optional[list[Message]] = Field( default=None, description="Previous messages in the conversation, if any", ) diff --git a/examples/13_rag.py b/examples/13_rag.py index be5b4b7..ae2f851 100644 --- a/examples/13_rag.py +++ b/examples/13_rag.py @@ -12,7 +12,7 @@ import asyncio from enum import Enum -from typing import List, Optional +from typing import Optional from pydantic import BaseModel, Field @@ -22,12 +22,14 @@ class Role(str, Enum): """Enum representing possible message roles.""" + USER = "user" ASSISTANT = "assistant" class SearchResult(BaseModel): """Model representing a search result from the knowledge base.""" + content: str = Field( description="The content of the search result", ) @@ -38,7 +40,7 @@ class SearchResult(BaseModel): # Simulated knowledge base search tool # ruff: noqa: ARG001 -async def search_faq(query: str) -> List[SearchResult]: +async def search_faq(query: str) -> list[SearchResult]: """ Search the knowledge base for relevant information. @@ -79,6 +81,7 @@ async def search_faq(query: str) -> List[SearchResult]: class Message(BaseModel): """Model representing a chat message.""" + role: Role content: str = Field( description="The content of the message", @@ -87,20 +90,24 @@ class Message(BaseModel): class AssistantMessage(Message): """Model representing a message from the assistant.""" + role: Role = Role.ASSISTANT content: str = "" class ChatbotOutput(BaseModel): """Output model for the chatbot response.""" - assistant_message: AssistantMessage = Field(default_factory=AssistantMessage, + + assistant_message: AssistantMessage = Field( + default_factory=AssistantMessage, description="The chatbot's response message", ) class ChatInput(BaseModel): """Input model containing the user's message and conversation history.""" - conversation_history: Optional[List[Message]] = Field( + + conversation_history: Optional[list[Message]] = Field( default=None, description="Previous messages in the conversation, if any", ) diff --git a/examples/14_templated_instructions.py b/examples/14_templated_instructions.py index 335eaea..3e13eb8 100644 --- a/examples/14_templated_instructions.py +++ b/examples/14_templated_instructions.py @@ -19,7 +19,6 @@ """ import asyncio -from typing import List from pydantic import BaseModel, Field @@ -29,6 +28,7 @@ class CodeReviewInput(BaseModel): """Input model for the code review agent.""" + language: str = Field( description="The programming language of the code to review", examples=["python", "javascript", "typescript"], @@ -44,7 +44,7 @@ class CodeReviewInput(BaseModel): description="Whether this is a production code review", default=False, ) - required_checks: List[str] = Field( + required_checks: list[str] = Field( description="List of specific checks to perform", default=["code style", "performance", "maintainability"], ) @@ -57,17 +57,18 @@ class CodeReviewInput(BaseModel): class CodeReviewOutput(BaseModel): """Output model containing the code review results.""" + overall_assessment: str = Field( description="Overall assessment of the code quality", ) - style_violations: List[str] = Field( + style_violations: list[str] = Field( description="List of style guide violations", ) - security_issues: List[str] = Field( + security_issues: list[str] = Field( description="List of security concerns", default_factory=list, ) - suggested_improvements: List[str] = Field( + suggested_improvements: list[str] = Field( description="List of suggested improvements", ) diff --git a/examples/15_text_to_sql.py b/examples/15_text_to_sql.py index 92b59b9..908b61e 100644 --- a/examples/15_text_to_sql.py +++ b/examples/15_text_to_sql.py @@ -13,7 +13,6 @@ """ import asyncio -from typing import List from pydantic import BaseModel, Field @@ -41,7 +40,7 @@ class SQLGenerationOutput(BaseModel): explanation: str = Field( description="Explanation of what the query does and why certain choices were made", ) - tables_used: List[str] = Field( + tables_used: list[str] = Field( description="List of tables referenced in the query", ) diff --git a/examples/16_multi_model_consensus.py b/examples/16_multi_model_consensus.py index 6027782..da0fbbf 100644 --- a/examples/16_multi_model_consensus.py +++ b/examples/16_multi_model_consensus.py @@ -11,7 +11,6 @@ """ import asyncio -from typing import List from pydantic import BaseModel, Field @@ -21,6 +20,7 @@ class MultiModelInput(BaseModel): """Input model containing the question to ask all models.""" + question: str = Field( description="The question to ask all models", ) @@ -31,17 +31,20 @@ class MultiModelInput(BaseModel): 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") + + 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", ) diff --git a/examples/17_multi_model_consensus_with_tools.py b/examples/17_multi_model_consensus_with_tools.py index 56578fb..761e284 100644 --- a/examples/17_multi_model_consensus_with_tools.py +++ b/examples/17_multi_model_consensus_with_tools.py @@ -19,7 +19,6 @@ """ import asyncio -from typing import List from pydantic import BaseModel, Field @@ -29,14 +28,17 @@ 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 @@ -54,6 +56,7 @@ async def ask_model(query_input: AskModelInput) -> AskModelOutput: class MultiModelInput(BaseModel): """Input model containing the question to ask all models.""" + question: str = Field( description="The question to ask all models", ) @@ -61,23 +64,26 @@ class MultiModelInput(BaseModel): 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( + models_used: list[str] = Field( description="List of models whose responses were combined", ) diff --git a/examples/workflows/chain_of_agents.py b/examples/workflows/chain_of_agents.py index 56aec25..0e1f4d7 100644 --- a/examples/workflows/chain_of_agents.py +++ b/examples/workflows/chain_of_agents.py @@ -16,7 +16,6 @@ """ import asyncio -from typing import List from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] @@ -47,7 +46,7 @@ class WorkerOutput(BaseModel): findings: str = Field( description="Key findings and information extracted from this chunk.", ) - evidence: List[str] = Field( + evidence: list[str] = Field( default_factory=list, description="Supporting evidence or quotes from the chunk.", ) @@ -70,7 +69,7 @@ class ManagerOutput(BaseModel): answer: str = Field(description="The final answer to the query.") reasoning: str = Field(description="Explanation of how the answer was derived.") - supporting_evidence: List[str] = Field( + supporting_evidence: list[str] = Field( default_factory=list, description="Key evidence supporting the answer.", ) diff --git a/examples/workflows/evaluator_optimizer.py b/examples/workflows/evaluator_optimizer.py index 2867092..8f79f17 100644 --- a/examples/workflows/evaluator_optimizer.py +++ b/examples/workflows/evaluator_optimizer.py @@ -1,5 +1,5 @@ import asyncio -from typing import List, TypedDict +from typing import TypedDict from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] @@ -45,8 +45,8 @@ class TranslationEvaluationOutput(BaseModel): preserves_tone: bool = Field(description="Whether the translation preserves the original tone.") preserves_nuance: bool = Field(description="Whether the translation preserves subtle nuances.") culturally_accurate: bool = Field(description="Whether the translation is culturally appropriate.") - specific_issues: List[str] = Field(description="List of specific issues identified.") - improvement_suggestions: List[str] = Field(description="List of suggested improvements.") + specific_issues: list[str] = Field(description="List of specific issues identified.") + improvement_suggestions: list[str] = Field(description="List of suggested improvements.") # Uses O1 for its strong analytical and evaluation capabilities @@ -65,8 +65,8 @@ class TranslationImprovementInput(BaseModel): original_text: str = Field(description="The original text.") current_translation: str = Field(description="The current translation.") target_language: str = Field(description="The target language.") - specific_issues: List[str] = Field(description="Issues to address.") - improvement_suggestions: List[str] = Field(description="Suggestions for improvement.") + specific_issues: list[str] = Field(description="Issues to address.") + improvement_suggestions: list[str] = Field(description="Suggestions for improvement.") class TranslationImprovementOutput(BaseModel): diff --git a/examples/workflows/orchestrator_worker.py b/examples/workflows/orchestrator_worker.py index abe3fd5..b151c3e 100644 --- a/examples/workflows/orchestrator_worker.py +++ b/examples/workflows/orchestrator_worker.py @@ -1,6 +1,6 @@ import asyncio from enum import Enum -from typing import List, TypedDict +from typing import TypedDict from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] @@ -37,7 +37,7 @@ class ImplementationPlanInput(BaseModel): class ImplementationPlanOutput(BaseModel): """Output containing the implementation plan.""" - files: List[FileChange] = Field(description="List of files to be changed.") + files: list[FileChange] = Field(description="List of files to be changed.") estimated_complexity: ComplexityLevel = Field(description="Estimated complexity of the implementation.") @@ -86,7 +86,7 @@ class ImplementationChange(TypedDict): class FeatureImplementationResult(TypedDict): plan: ImplementationPlanOutput - changes: List[ImplementationChange] + changes: list[ImplementationChange] async def implement_feature(feature_request: str) -> FeatureImplementationResult: @@ -117,7 +117,7 @@ async def implement_feature(feature_request: str) -> FeatureImplementationResult ) # Combine results - changes: List[ImplementationChange] = [ + changes: list[ImplementationChange] = [ { "file": implementation_plan.files[i], "implementation": change, diff --git a/examples/workflows/parallel_processing.py b/examples/workflows/parallel_processing.py index 5b2f27c..86623c7 100644 --- a/examples/workflows/parallel_processing.py +++ b/examples/workflows/parallel_processing.py @@ -1,6 +1,6 @@ import asyncio from enum import Enum -from typing import List, TypedDict +from typing import TypedDict from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType] @@ -23,9 +23,9 @@ class SecurityReviewInput(BaseModel): class SecurityReviewOutput(BaseModel): """Output from security code review.""" - vulnerabilities: List[str] = Field(description="List of identified security vulnerabilities.") + vulnerabilities: list[str] = Field(description="List of identified security vulnerabilities.") risk_level: RiskLevel = Field(description="Overall security risk level.") - suggestions: List[str] = Field(description="Security improvement suggestions.") + suggestions: list[str] = Field(description="Security improvement suggestions.") # Uses Claude 3.5 Sonnet for its strong security analysis capabilities @@ -47,9 +47,9 @@ class PerformanceReviewInput(BaseModel): class PerformanceReviewOutput(BaseModel): """Output from performance code review.""" - issues: List[str] = Field(description="List of identified performance issues.") + issues: list[str] = Field(description="List of identified performance issues.") impact: RiskLevel = Field(description="Impact level of performance issues.") - optimizations: List[str] = Field(description="Performance optimization suggestions.") + optimizations: list[str] = Field(description="Performance optimization suggestions.") # Uses O1 Mini for its expertise in performance optimization @@ -71,9 +71,9 @@ class MaintainabilityReviewInput(BaseModel): class MaintainabilityReviewOutput(BaseModel): """Output from maintainability code review.""" - concerns: List[str] = Field(description="List of maintainability concerns.") + concerns: list[str] = Field(description="List of maintainability concerns.") quality_score: int = Field(description="Code quality score (1-10).", ge=1, le=10) - recommendations: List[str] = Field(description="Maintainability improvement recommendations.") + recommendations: list[str] = Field(description="Maintainability improvement recommendations.") # Uses Claude 3.5 Sonnet for its strong code quality and readability analysis diff --git a/tests/e2e/run_test.py b/tests/e2e/run_test.py index 4dc8a5a..0249683 100644 --- a/tests/e2e/run_test.py +++ b/tests/e2e/run_test.py @@ -1,5 +1,6 @@ +from collections.abc import AsyncIterator from enum import Enum -from typing import AsyncIterator, Optional +from typing import Optional import pytest from pydantic import BaseModel diff --git a/tests/e2e/tools_test.py b/tests/e2e/tools_test.py index 6b07240..6fa0d30 100644 --- a/tests/e2e/tools_test.py +++ b/tests/e2e/tools_test.py @@ -1,8 +1,8 @@ from datetime import datetime from typing import Annotated +from zoneinfo import ZoneInfo from pydantic import BaseModel -from zoneinfo import ZoneInfo from workflowai import Run, agent from workflowai.core.domain.model import Model diff --git a/tests/integration/run_test.py b/tests/integration/run_test.py index 60d657e..bbc32eb 100644 --- a/tests/integration/run_test.py +++ b/tests/integration/run_test.py @@ -1,5 +1,5 @@ import json -from typing import AsyncIterator +from collections.abc import AsyncIterator from pydantic import BaseModel diff --git a/tests/integration/stream_agent_builder_test.py b/tests/integration/stream_agent_builder_test.py index 9aaccad..1908f25 100644 --- a/tests/integration/stream_agent_builder_test.py +++ b/tests/integration/stream_agent_builder_test.py @@ -130,7 +130,7 @@ class InputArrayFieldConfig(BaseFieldConfig): class OutputGenericFieldConfig(BaseFieldConfig): - type: OutputSchemaFieldType = Field(default=None, description="The type of the field") + type: Optional[OutputSchemaFieldType] = Field(default=None, description="The type of the field") class OutputObjectFieldConfig(BaseFieldConfig): diff --git a/workflowai/core/client/_api.py b/workflowai/core/client/_api.py index d8d737d..ce6e913 100644 --- a/workflowai/core/client/_api.py +++ b/workflowai/core/client/_api.py @@ -1,6 +1,7 @@ import logging +from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from typing import Any, AsyncIterator, Literal, Optional, TypeVar, Union, overload +from typing import Any, Literal, Optional, TypeVar, Union, overload import httpx from pydantic import BaseModel, TypeAdapter, ValidationError diff --git a/workflowai/core/client/_fn_utils.py b/workflowai/core/client/_fn_utils.py index 514347d..c56a8db 100644 --- a/workflowai/core/client/_fn_utils.py +++ b/workflowai/core/client/_fn_utils.py @@ -1,14 +1,11 @@ import functools import inspect -from collections.abc import Callable, Iterable +from collections.abc import AsyncIterator, Callable, Iterable, Sequence from typing import ( Any, - AsyncIterator, Generic, NamedTuple, Optional, - Sequence, - Type, Union, get_args, get_origin, @@ -42,7 +39,7 @@ def get_generic_args(t: type[BaseModel]) -> Union[Sequence[type], None]: return t.__pydantic_generic_metadata__.get("args") -def check_return_type(return_type_hint: Type[Any]) -> tuple[bool, Type[BaseModel]]: +def check_return_type(return_type_hint: type[Any]) -> tuple[bool, type[BaseModel]]: if issubclass(return_type_hint, Run): args = get_generic_args(return_type_hint) # pyright: ignore [reportUnknownArgumentType] if not args: @@ -59,8 +56,8 @@ def check_return_type(return_type_hint: Type[Any]) -> tuple[bool, Type[BaseModel class RunFunctionSpec(NamedTuple): stream: bool output_only: bool - input_cls: Type[BaseModel] - output_cls: Type[BaseModel] + input_cls: type[BaseModel] + output_cls: type[BaseModel] def is_async_iterator(t: type[Any]) -> bool: diff --git a/workflowai/core/client/_fn_utils_test.py b/workflowai/core/client/_fn_utils_test.py index 58cfcac..90c4dbc 100644 --- a/workflowai/core/client/_fn_utils_test.py +++ b/workflowai/core/client/_fn_utils_test.py @@ -1,4 +1,5 @@ -from typing import AsyncIterator, Union +from collections.abc import AsyncIterator +from typing import Union from unittest.mock import Mock import pytest diff --git a/workflowai/core/client/_types.py b/workflowai/core/client/_types.py index 96cf75a..27334c5 100644 --- a/workflowai/core/client/_types.py +++ b/workflowai/core/client/_types.py @@ -1,6 +1,6 @@ +from collections.abc import AsyncIterator from typing import ( Any, - AsyncIterator, Generic, Optional, Protocol, diff --git a/workflowai/core/fields/local_date_time_test.py b/workflowai/core/fields/local_date_time_test.py index 1be43a5..5aec9a6 100644 --- a/workflowai/core/fields/local_date_time_test.py +++ b/workflowai/core/fields/local_date_time_test.py @@ -1,8 +1,8 @@ from datetime import date, datetime, time +from zoneinfo import ZoneInfo import pytest from pydantic import BaseModel -from zoneinfo import ZoneInfo from .local_date_time import DatetimeLocal diff --git a/workflowai/core/fields/zone_info.py b/workflowai/core/fields/zone_info.py index 0fef3c0..5092d99 100644 --- a/workflowai/core/fields/zone_info.py +++ b/workflowai/core/fields/zone_info.py @@ -1,9 +1,9 @@ from typing import Annotated, Any +from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler from pydantic.json_schema import JsonSchemaValue from pydantic_core import core_schema -from zoneinfo import ZoneInfo, ZoneInfoNotFoundError def _serialize(instance: ZoneInfo) -> str: diff --git a/workflowai/core/fields/zone_info_test.py b/workflowai/core/fields/zone_info_test.py index 6855941..19d44c7 100644 --- a/workflowai/core/fields/zone_info_test.py +++ b/workflowai/core/fields/zone_info_test.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel from zoneinfo import ZoneInfo +from pydantic import BaseModel + from .zone_info import TimezoneInfo diff --git a/workflowai/core/utils/_iter.py b/workflowai/core/utils/_iter.py index b35f4c9..b47fb29 100644 --- a/workflowai/core/utils/_iter.py +++ b/workflowai/core/utils/_iter.py @@ -1,5 +1,5 @@ -from collections.abc import Iterator -from typing import Callable, Iterable, Optional +from collections.abc import Iterable, Iterator +from typing import Callable, Optional from workflowai.core._logger import logger from workflowai.core.utils._vars import T, U diff --git a/workflowai/core/utils/_tools_test.py b/workflowai/core/utils/_tools_test.py index 2825374..c02d615 100644 --- a/workflowai/core/utils/_tools_test.py +++ b/workflowai/core/utils/_tools_test.py @@ -2,10 +2,10 @@ from datetime import datetime from enum import Enum from typing import Annotated, Any +from zoneinfo import ZoneInfo import pytest from pydantic import BaseModel -from zoneinfo import ZoneInfo from workflowai.core.utils._tools import _get_type_schema, tool_schema # pyright: ignore [reportPrivateUsage] From e05ed01d1a1d95efd1af0881a5695ba36f5d2b9b Mon Sep 17 00:00:00 2001 From: Pierre Date: Tue, 18 Feb 2025 09:11:23 -0700 Subject: [PATCH 13/13] chore: remove templated instructions example Remove examples/14_templated_instructions.py from PR --- examples/14_templated_instructions.py | 177 -------------------------- 1 file changed, 177 deletions(-) delete mode 100644 examples/14_templated_instructions.py diff --git a/examples/14_templated_instructions.py b/examples/14_templated_instructions.py deleted file mode 100644 index 3e13eb8..0000000 --- a/examples/14_templated_instructions.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -This example demonstrates how to use templated instructions that adapt based on input variables. -The template variables are automatically populated from the input model's fields. - -The templating uses Jinja2 syntax (server-side rendering): -- {{ variable }} for variable substitution -- {% if condition %} ... {% endif %} for conditionals -- {% for item in items %} ... {% endfor %} for loops -- {{ loop.index }} for loop indices - -For full Jinja2 template syntax documentation, see: -https://jinja.palletsprojects.com/en/stable/ - -It showcases: -1. Simple variable substitution -2. Conditional logic -3. Loops -4. Nested conditionals -""" - -import asyncio - -from pydantic import BaseModel, Field - -import workflowai -from workflowai import Model, Run - - -class CodeReviewInput(BaseModel): - """Input model for the code review agent.""" - - language: str = Field( - description="The programming language of the code to review", - examples=["python", "javascript", "typescript"], - ) - code: str = Field( - description="The code to review", - ) - style_guide: str = Field( - description="The style guide to follow", - examples=["PEP 8", "Google Style", "Airbnb"], - ) - is_production: bool = Field( - description="Whether this is a production code review", - default=False, - ) - required_checks: list[str] = Field( - description="List of specific checks to perform", - default=["code style", "performance", "maintainability"], - ) - security_level: str = Field( - description="Required security level", - default="standard", - examples=["standard", "high"], - ) - - -class CodeReviewOutput(BaseModel): - """Output model containing the code review results.""" - - overall_assessment: str = Field( - description="Overall assessment of the code quality", - ) - style_violations: list[str] = Field( - description="List of style guide violations", - ) - security_issues: list[str] = Field( - description="List of security concerns", - default_factory=list, - ) - suggested_improvements: list[str] = Field( - description="List of suggested improvements", - ) - - -@workflowai.agent( - id="templated-code-reviewer", - model=Model.CLAUDE_3_5_SONNET_LATEST, -) -async def review_code(review_input: CodeReviewInput) -> Run[CodeReviewOutput]: - """ - Review code based on specified parameters and guidelines. - - You are a code reviewer for {{ language }} code. - Please review the code according to the {{ style_guide }} style guide. - - {% if is_production %} - This is a PRODUCTION code review - please be extra thorough and strict about best practices. - {% else %} - This is a development code review - focus on maintainability and clarity. - {% endif %} - - Required checks to perform: - {% for check in required_checks %}{{ loop.index }}. {{ check }} - {% endfor %} - - {% if security_level == "high" %} - Additional security requirements: - - Must follow secure coding practices - - Check for potential security vulnerabilities - - Ensure all inputs are properly sanitized - {% endif %} - - Guidelines: - 1. Check for adherence to {{ style_guide }} conventions - 2. Look for potential bugs and performance issues - 3. Suggest improvements while keeping the {{ language }} best practices in mind - - {% if language == "python" %} - Python-specific checks: - - Type hints usage - - PEP 8 compliance - - Docstring format - {% elif language == "javascript" or language == "typescript" %} - JavaScript/TypeScript-specific checks: - - ESLint rules compliance - - Modern ES6+ features usage - - Browser compatibility - {% endif %} - - Please analyze the following code and provide: - 1. An overall assessment - 2. Style guide violations - 3. Security issues (if any) - 4. Suggested improvements - """ - ... - - -async def main(): - # Example 1: Python code review for development - print("\nExample 1: Python Development Code Review") - print("-" * 50) - python_code = """ -def calculate_sum(numbers): - result = 0 - for n in numbers: - result = result + n - return result - """ - - run = await review_code( - CodeReviewInput( - language="python", - code=python_code, - style_guide="PEP 8", - required_checks=["type hints", "docstring", "performance"], - ), - ) - print(run) - - # Example 2: TypeScript production code with high security - print("\nExample 2: TypeScript Production Code Review (High Security)") - print("-" * 50) - typescript_code = """ -function processUserData(data: any) { - const userId = data.id; - const query = `SELECT * FROM users WHERE id = ${userId}`; - return executeQuery(query); -} - """ - - run = await review_code( - CodeReviewInput( - language="typescript", - code=typescript_code, - style_guide="Airbnb", - is_production=True, - security_level="high", - required_checks=["security", "type safety", "SQL injection"], - ), - ) - print(run) - - -if __name__ == "__main__": - asyncio.run(main())