diff --git a/.gitignore b/.gitignore
index 74199cf1..d85db694 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ bin
examples/local/*
.logs
scratch_pad.py
+.*
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 2ba986f6..00000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- "type": "chrome",
- "request": "launch",
- "name": "Launch Chrome against localhost",
- "url": "http://localhost:8080",
- "webRoot": "${workspaceFolder}"
- }
- ]
-}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index d293ec5c..00000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "python.testing.unittestArgs": [
- "-v",
- "-s",
- "./flo_ai_tools",
- "-p",
- "*test.py"
- ],
- "python.testing.pytestEnabled": true,
- "python.testing.unittestEnabled": false,
- "python.testing.pytestArgs": [
- "flo_ai"
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index a1af69d1..393c32cd 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
**Please, star the project on github (see top-right corner) if you appreciate our contribution to the community!**
-Rootflo is an alternative to Langgraph, and CrewAI. It lets you easily build composable agentic workflows from using simple components to any size, unlocking the full potential of LLMs.
+Flo AI is a Python framework for building structured AI agents with support for multiple LLM providers, tool integration, and YAML-based configuration. Create production-ready AI agents with minimal code and maximum flexibility.
@@ -38,7 +38,7 @@ Rootflo is an alternative to Langgraph, and CrewAI. It lets you e
# Flo AI ๐
-> Build production-ready AI agents and teams with minimal code
+> Build production-ready AI agents with structured outputs, tool integration, and multi-LLM support
Flo AI is a Python framework that makes building production-ready AI agents and teams as easy as writing YAML. Think "Kubernetes for AI Agents" - compose complex AI architectures using pre-built components while maintaining the flexibility to create your own.
@@ -49,18 +49,10 @@ Flo AI is a Python framework that makes building production-ready AI agents and
- ๐ **YAML-First**: Define your entire agent architecture in simple YAML
- ๐ง **Flexible**: Use pre-built components or create your own
- ๐ค **Team-Oriented**: Create and manage teams of AI agents working together
-- ๐ **RAG Support**: Built-in support for Retrieval-Augmented Generation
- ๐ **Langchain Compatible**: Works with all your favorite Langchain tools
## ๐ Quick Start
-FloAI follows an agent team architecture, where agents are the basic building blocks, and teams can have multiple agents and teams themselves can be part of bigger teams.
-
-Building a working agent or team involves 3 steps:
-1. Create a session using `FloSession`, and register your tools and models
-2. Define you agent/team/team of teams using yaml or code
-3. Build and run using `Flo`
-
### Installation
```bash
@@ -69,422 +61,448 @@ pip install flo-ai
poetry add flo-ai
```
-### Create Your First AI Agent in 30 secs
+### Create Your First AI Agent in 30 seconds
```python
-from flo_ai import Flo, FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-
-# init your LLM
-llm = ChatOpenAI(temperature=0)
-
-# create a session and register your tools
-session = FloSession(llm).register_tool(name="TavilySearchResults", tool=TavilySearchResults())
-
-# define your agent yaml
-simple_weather_checking_agent = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- job: >
- Given the city name you are capable of answering the latest whether this time of the year by searching the internet
- tools:
- - name: InternetSearchTool
-"""
-flo = Flo.build(session, yaml=simple_weather_checking_agent)
-
-# Start streaming results
-for response in flo.stream("Write about recent AI developments"):
- print(response)
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.llm.openai_llm import OpenAI
+
+async def main():
+ # Create a simple conversational agent
+ agent = (
+ AgentBuilder()
+ .with_name('Math Tutor')
+ .with_prompt('You are a helpful math tutor.')
+ .with_llm(OpenAI(model='gpt-4o-mini'))
+ .build()
+ )
+
+ response = await agent.run('What is the formula for the area of a circle?')
+ print(f'Response: {response}')
+
+asyncio.run(main())
```
-## Lets create the same agent using code
+### Create a Tool-Using Agent
```python
-from flo_ai import FloAgent
-
-session = FloSession(llm)
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.tool.base_tool import Tool
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.anthropic_llm import Anthropic
+
+async def calculate(operation: str, x: float, y: float) -> float:
+ if operation == 'add':
+ return x + y
+ elif operation == 'multiply':
+ return x * y
+ raise ValueError(f'Unknown operation: {operation}')
+
+# Define a calculator tool
+calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add or multiply)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+)
-weather_agent = FloAgent.create(
- session=session,
- name="WeatherAssistant",
- job="Given the city name you are capable of answering the latest whether this time of the year by searching the internet",
- tools=[TavilySearchResults()]
+# Create a tool-using agent with Claude
+agent = (
+ AgentBuilder()
+ .with_name('Calculator Assistant')
+ .with_prompt('You are a math assistant that can perform calculations.')
+ .with_llm(Anthropic(model='claude-3-5-sonnet-20240620'))
+ .with_tools([calculator_tool])
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
)
-agent_flo: Flo = Flo.create(session, weather_agent)
-result = agent_flo.invoke("Whats the whether in New Delhi, India ?")
+response = await agent.run('Calculate 5 plus 3')
+print(f'Response: {response}')
```
-### Create Your First AI Team in 30 Seconds
+### Create an Agent with Structured Output
```python
-from flo_ai import Flo, FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.llm.openai_llm import OpenAI
+
+# Define output schema for structured responses
+math_schema = {
+ 'type': 'object',
+ 'properties': {
+ 'solution': {'type': 'string', 'description': 'The step-by-step solution'},
+ 'answer': {'type': 'string', 'description': 'The final answer'},
+ },
+ 'required': ['solution', 'answer'],
+}
+# Create an agent with structured output
+agent = (
+ AgentBuilder()
+ .with_name('Structured Math Solver')
+ .with_prompt('You are a math problem solver that provides structured solutions.')
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_output_schema(math_schema)
+ .build()
+)
-# Define your team in YAML
-yaml_config = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: research-team
-team:
- name: ResearchTeam
- router:
- name: TeamLead
- kind: supervisor
- agents:
- - name: Researcher
- role: Research Specialist
- job: Research latest information on given topics
- tools:
- - name: TavilySearchResults
- - name: Writer
- role: Content Creator
- job: Create engaging content from research
-"""
-
-# Set up and run
-llm = ChatOpenAI(temperature=0)
-session = FloSession(llm).register_tool(name="TavilySearchResults", tool=TavilySearchResults())
-flo = Flo.build(session, yaml=yaml_config)
-
-# Start streaming results
-for response in flo.stream("Write about recent AI developments"):
- print(response)
+response = await agent.run('Solve: 2x + 5 = 15')
+print(f'Structured Response: {response}')
```
-**Note:** You can make each of the above agents including the router to use different models, giving flexibility to combine the power of different LLMs.
-To know more, check multi-model integration in detailed [documentation](https://flo-ai.rootflo.ai/advanced/model-switching)
-
-### Lets Create a AI team using code
+## ๐ YAML Configuration
-```python
-from flo_ai import FloSupervisor, FloAgent, FloSession, FloTeam, FloLinear
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
-session = FloSession(llm).register_tool(
- name="TavilySearchResults",
- tool=TavilySearchResults()
-)
+Define your agents using YAML for easy configuration and deployment:
-researcher = FloAgent.create(
- session,
- name="Researcher",
- role="Internet Researcher", # optional
- job="Do a research on the internet and find articles of relevent to the topic asked by the user",
- tools=[TavilySearchResults()]
-)
+```yaml
+apiVersion: flo/alpha-v1
+kind: FloAgent
+metadata:
+ name: email-summary-flo
+ version: 1.0.0
+ description: "Agent for analyzing email threads"
+agent:
+ name: EmailSummaryAgent
+ kind: llm
+ role: Email communication expert
+ model:
+ provider: openai
+ name: gpt-4o-mini
+ settings:
+ temperature: 0
+ max_retries: 3
+ reasoning_pattern: DIRECT
+ job: >
+ You are given an email thread between a customer and a support agent.
+ Your job is to analyze the behavior, sentiment, and communication style.
+ parser:
+ name: EmailSummary
+ fields:
+ - name: sender_type
+ type: literal
+ description: "Who sent the latest email"
+ values:
+ - value: customer
+ description: "Latest email was sent by customer"
+ - value: agent
+ description: "Latest email was sent by support agent"
+ - name: summary
+ type: str
+ description: "A comprehensive summary of the email"
+ - name: resolution_status
+ type: literal
+ description: "Issue resolution status"
+ values:
+ - value: resolved
+ description: "Issue appears resolved"
+ - value: unresolved
+ description: "Issue requires attention"
+```
-blogger = FloAgent.create(
- session,
- name="BlogWriter",
- role="Thought Leader", # optional
- job="Able to write a blog using information provided",
- tools=[TavilySearchResults()]
-)
+```python
+from flo_ai.builder.agent_builder import AgentBuilder
-marketing_team = FloTeam.create(session, "Marketing", [researcher, blogger])
-head_of_marketing = FloSupervisor.create(session, "Head-of-Marketing", marketing_team)
-marketing_flo = Flo.create(session, routed_team=head_of_marketing)
+# Create agent from YAML
+builder = AgentBuilder.from_yaml(yaml_str=yaml_config)
+agent = builder.build()
+# Use the agent
+result = await agent.run(email_thread)
```
-## Tools
+## ๐ ๏ธ Tools
-FloAI supports all the tools built and available in `langchain_community` package. To know more these tools, go [here](https://python.langchain.com/docs/integrations/tools/).
-
-Along with that FloAI has a decorator `@flotool` which makes any function into a tool.
-
-Creating a simple tool using `@flotool`:
+Create custom tools easily with async support:
```python
-from flo_ai.tools import flotool
-from pydantic import BaseModel, Field
-
-# define argument schema
-class AdditionToolInput(BaseModel):
- numbers: List[int] = Field(..., description='List of numbers to add')
-
-@flotool(name='AdditionTool', description='Tool to add numbers')
-async def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The sum is {result}'
-
-# async tools can also be defined
-# when using async tool, while running the flo use async invoke
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
+from flo_ai.tool.base_tool import Tool
+
+async def weather_lookup(city: str) -> str:
+ # Your weather API call here
+ return f"Weather in {city}: Sunny, 25ยฐC"
+
+weather_tool = Tool(
+ name='weather_lookup',
+ description='Get current weather for a city',
+ function=weather_lookup,
+ parameters={
+ 'city': {
+ 'type': 'string',
+ 'description': 'City name to get weather for'
+ }
+ }
)
-async def mul_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The product is {result}'
-# register your tool or use directly in code impl
-session.register_tool(name='Adder', tool=addition_tool)
+# Add to your agent
+agent = (
+ AgentBuilder()
+ .with_name('Weather Assistant')
+ .with_llm(OpenAI(model='gpt-4o-mini'))
+ .with_tools([weather_tool])
+ .build()
+)
```
-**Note:** `@flotool` comes with inherent error handling capabilities to retry if an exception is thrown. Use `unsafe=True` to disable error handling
+### ๐ฏ @flo_tool Decorator
-## Output Parsing and formatting
+The `@flo_tool` decorator automatically converts any Python function into a `Tool` object with minimal boilerplate:
-FloAI now supports output parsing using JSON or YAML formatter. You can now defined your output formatter using `pydantic` and use the same in code or directly make it part of the Agent Definition Yaml (ADY)
+```python
+from flo_ai.tool import flo_tool
+
+@flo_tool(
+ description="Perform mathematical calculations",
+ parameter_descriptions={
+ "operation": "The operation to perform (add, subtract, multiply, divide)",
+ "x": "First number",
+ "y": "Second number"
+ }
+)
+async def calculate(operation: str, x: float, y: float) -> float:
+ """Calculate mathematical operations between two numbers."""
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+# Function can be called normally
+result = await calculate("add", 5, 3) # Returns 8
+
+# Tool object is automatically available
+agent = (
+ AgentBuilder()
+ .with_name('Calculator Agent')
+ .with_llm(OpenAI(model='gpt-4o-mini'))
+ .with_tools([calculate.tool]) # Access the tool via .tool attribute
+ .build()
+)
+```
-### Using Agent Defintion YAML
+**Key Benefits:**
+- โ
**Automatic parameter extraction** from type hints
+- โ
**Flexible descriptions** via docstrings or custom descriptions
+- โ
**Type conversion** from Python types to JSON schema
+- โ
**Dual functionality** - functions work normally AND as tools
+- โ
**Async support** for both sync and async functions
-We have added parser key to your agent schema, which gives you the output. The following is the schema of the parser
+**Simple Usage:**
+```python
+@flo_tool()
+async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ """Convert between different units (km/miles, kg/lbs, celsius/fahrenheit)."""
+ # Implementation here
+ return f"{value} {from_unit} = {result} {to_unit}"
-```yaml
-name: SchemaName
-fields:
- - name: field_name
- type: data_type
- description: field_description
- values:
- - value:
- description: value_description
+# Tool is automatically available as convert_units.tool
```
-### Supported Field Types
-
-#### Primitive Types
-
-- str: String values
-- int: Integer values
-- bool: Boolean values
-- float: Floating-point values
+**With Custom Metadata:**
+```python
+@flo_tool(
+ name="weather_checker",
+ description="Get current weather information for a city",
+ parameter_descriptions={
+ "city": "The city to get weather for",
+ "country": "The country (optional)",
+ }
+)
+async def get_weather(city: str, country: str = None) -> str:
+ """Get weather information for a specific city."""
+ return f"Weather in {city}: sunny"
+```
-##### Complex Types
+> ๐ **For detailed documentation on the `@flo_tool` decorator, see [README_flo_tool.md](flo_ai/README_flo_tool.md)**
-- array: Lists of items
-- object: Nested objects
-- literal: Enumerated values
+## ๐ง Reasoning Patterns
+Flo AI supports multiple reasoning patterns:
-Here an example of a simple summarization agent yaml that produces output a structured manner.
+- **DIRECT**: Simple question-answer without step-by-step reasoning
+- **COT (Chain of Thought)**: Step-by-step reasoning before providing the answer
+- **REACT**: Reasoning and action cycles for tool-using agents
-```yaml
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: SummarizationFlo
-agent:
- name: SummaryAgent
- kind: llm
- role: Book summarizer agent
- job: >
- You are an given a paragraph from a book
- and your job is to understand the information in it and extract summary
- parser:
- name: BookSummary
- fields:
- - name: long_summary
- type: str
- description: A comprehensive summary of the book, with all the major topics discussed
- - name: short_summary
- type: str
- description: A short summary of the book in less than 20 words
+```python
+from flo_ai.models.base_agent import ReasoningPattern
+
+agent = (
+ AgentBuilder()
+ .with_name('Reasoning Agent')
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_reasoning(ReasoningPattern.COT) # or REACT, DIRECT
+ .build()
+)
```
-As you can see here, the `parser` key makes sure that output of this agent will be the given key value format.
-
-### Using parser with code
-
-You can define parser as json in code and use it easily, here is an example:
+## ๐ง LLM Providers
+### OpenAI
```python
-format = {
- 'name': 'NameFormat',
- 'fields': [
- {
- 'type': 'str',
- 'description': 'The first name of the person',
- 'name': 'first_name',
- },
- {
- 'type': 'str',
- 'description': 'The middle name of the person',
- 'name': 'middle_name',
- },
- {
- 'type': 'literal',
- 'description': 'The last name of the person, the value can be either of Vishnu or Satis',
- 'name': 'last_name',
- 'values': [
- {'value': 'Vishnu', 'description': 'If the first_name starts with K'},
- {'value': 'Satis', 'description': 'If the first_name starts with M'},
- ],
- 'default_value_prompt': 'If none of the above value is suited, please use value other than the above in snake-case',
- },
- ],
-}
+from flo_ai.llm.openai_llm import OpenAI
-researcher = FloAgent.create(
- session,
- name='Researcher',
- role='Internet Researcher',
- job='What is the first name, last name and middle name of the the person user asks about',
- tools=[TavilySearchResults()],
- parser=FloJsonParser.create(json_dict=format)
+llm = OpenAI(
+ model='gpt-4o',
+ temperature=0.7,
+ api_key='your-api-key' # or set OPENAI_API_KEY env var
)
-
-
-Flo.set_log_level('DEBUG')
-flo: Flo = Flo.create(session, researcher)
-result = flo.invoke('Mahatma Gandhi')
-
```
-## Output Data Collector
-
-Output collector is an infrastructure that helps you collect outputs across multiple agents into single data structure. The most useful collector is a JSON output collector which when combined with output parser gives combined JSON outputs.
-
-Usage:
+### Anthropic Claude
```python
-from flo_ai.state import FloJsonOutputCollector
-
-dc = FloJsonOutputCollector()
+from flo_ai.llm.anthropic_llm import Anthropic
-# register your collector to the session
-session = FloSession(llm).register_tool(
- name='InternetSearchTool', tool=TavilySearchResults()
+llm = Anthropic(
+ model='claude-3-5-sonnet-20240620',
+ temperature=0.7,
+ api_key='your-api-key' # or set ANTHROPIC_API_KEY env var
)
+```
-simple_reseacher = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- kind: agentic
- job: >
- Given the person name, guess the first and last name
- tools:
- - name: InternetSearchTool
- parser:
- name: NameFormatter
- fields:
- - type: str
- description: The first name of the person
- name: first_name
- - type: str
- description: The first name of the person
- name: last_name
- - name: location
- type: object
- description: The details about birth location
- fields:
- - name: state
- type: str
- description: The Indian State in whihc the person was born
- data_collector: kv
-"""
-
-flo: Flo = Flo.build(session, simple_reseacher)
-result = flo.invoke('Gandhi')
-
-# This will output the output as JSON. The idea is that you can use the same collector across multiple agents and teams to still get a combined JSON output.
-print(dc.fetch())
+### Ollama (Local)
+```python
+from flo_ai.llm.ollama_llm import Ollama
+llm = Ollama(
+ model='llama2',
+ base_url='http://localhost:11434'
+)
```
-## ๐ Tool Logging and Data Collection
+## ๐ Output Formatting
-FloAI provides built-in capabilities for logging tool calls and collecting data through the `FloExecutionLogger` and `DataCollector` classes, facilitating the creation of valuable training data.
-You can customize `DataCollector` implementation according to your database. A sample implementation where logs are stored locally as JSON files is implemented in `JSONLFileCollector`.
-
-### Quick Setup
+Use Pydantic models or JSON schemas for structured outputs:
```python
-from flo_ai.callbacks import FloExecutionLogger
-from flo_ai.storage.data_collector import JSONLFileCollector
-
-# Initialize the file collector with a path for the JSONL log file to be stored
-file_collector = JSONLFileCollector("'.logs'")
-
-# Create a tool logger with the collector
-local_tracker = FloExecutionLogger(file_collector)
+from pydantic import BaseModel, Field
-# Register the logger with your session
-session.register_callback(local_tracker)
+class MathSolution(BaseModel):
+ solution: str = Field(description="Step-by-step solution")
+ answer: str = Field(description="Final answer")
+ confidence: float = Field(description="Confidence level (0-1)")
+
+agent = (
+ AgentBuilder()
+ .with_name('Math Solver')
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_output_schema(MathSolution)
+ .build()
+)
```
-### Features
+## ๐ Error Handling
-- ๐ Logs all tool calls, chain executions, and agent actions
-- ๐ Includes timestamps for start and end of operations
-- ๐ Tracks inputs, outputs, and errors
-- ๐พ Stores data in JSONL format for easy analysis
-- ๐ Facilitates the creation of training data from logged interactions
+Built-in retry mechanisms and error recovery:
-### Log Data Structure
+```python
+agent = (
+ AgentBuilder()
+ .with_name('Robust Agent')
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_retries(3) # Retry up to 3 times on failure
+ .build()
+)
+```
-The logger captures detailed information including:
-- Tool name and inputs
-- Execution timestamps
-- Operation status (completed/error)
-- Chain and agent activities
-- Parent-child relationship between operations
+## ๐ Examples
-### Training Data Generation
+Check out the `examples/` directory for comprehensive examples:
-The structured logs provide valuable training data that can be used to:
-- **Fine-tune LLMs** on your specific use cases
-- **Train new models** to replicate successful tool usage patterns
-- **Create supervised datasets** for tool selection and chain optimization
+- `agent_builder_usage.py` - Basic agent creation patterns
+- `yaml_agent_example.py` - YAML-based agent configuration
+- `output_formatter.py` - Structured output examples
+- `multi_tool_example.py` - Multi-tool agent examples
+- `cot_agent_example.py` - Chain of Thought reasoning
+- `usage.py` and `usage_claude.py` - Provider-specific examples
-We have created a script to convert your logs to training data:
+## ๐ Advanced Features
+### Custom Tool Creation
```python
-python generate_training_data.py --logger-path PATH --tool-path PATH [--output PATH]
+from flo_ai.tool.base_tool import Tool
+
+async def custom_function(param1: str, param2: int) -> dict:
+ # Your async logic here
+ return {"result": f"Processed {param1} with {param2}"}
+
+custom_tool = Tool(
+ name='custom_function',
+ description='A custom async tool',
+ function=custom_function,
+ parameters={
+ 'param1': {'type': 'string', 'description': 'First parameter'},
+ 'param2': {'type': 'integer', 'description': 'Second parameter'}
+ }
+)
```
-Arguments:
-- *logger-path*: Path to the logger file containing tool and chain entries, eg: .logs/logs/log.jsonl
-- *tool-path*: Path to the tool descriptions file eg: eg: .logs/tools/tools.jsonl
-- *output*: path to save the output eg: training-data.jsonl
-
+### YAML Parser Integration
+```python
+from flo_ai.formatter.yaml_format_parser import FloYamlParser
+
+# Create parser from YAML definition
+parser = FloYamlParser.create(yaml_dict=yaml_config)
+output_schema = parser.get_format()
+
+agent = (
+ AgentBuilder()
+ .with_name('YAML Configured Agent')
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_output_schema(output_schema)
+ .build()
+)
+```
## ๐ Documentation
Visit our [comprehensive documentation](https://flo-ai.rootflo.ai) for:
- Detailed tutorials
-- Architecture deep-dives
- API reference
- - Logging
- - Error handling
- - Observers
- - Dynamic model switching
- Best practices
- Advanced examples
+- Architecture deep-dives
+
+**Additional Resources:**
+- [@flo_tool Decorator Guide](flo_ai/README_flo_tool.md) - Complete guide to the `@flo_tool` decorator
+- [Examples Directory](examples/) - Ready-to-run code examples
+- [Contributing Guide](CONTRIBUTING.md) - How to contribute to Flo AI
## ๐ Why Flo AI?
-### For AI Engineers
-- **Faster Development**: Build complex AI systems in minutes, not days
-- **Production Focus**: Built-in optimizations and best practices
-- **Flexibility**: Use our components or build your own
+### For Developers
+- **Simple Setup**: Get started in minutes with minimal configuration
+- **Flexible**: Use YAML or code-based configuration
+- **Production Ready**: Built-in error handling and retry mechanisms
+- **Multi-LLM**: Switch between providers easily
### For Teams
-- **Maintainable**: YAML-first approach makes systems easy to understand and modify
-- **Scalable**: From single agents to complex team hierarchies
+- **Maintainable**: YAML-first approach makes configurations versionable
- **Testable**: Each component can be tested independently
+- **Scalable**: From simple agents to complex multi-tool systems
## ๐ฏ Use Cases
- ๐ค Customer Service Automation
-- ๐ Data Analysis Pipelines
-- ๐ Content Generation
-- ๐ Research Automation
-- ๐ฏ Task-Specific AI Teams
+- ๐ Data Analysis and Processing
+- ๐ Content Generation and Summarization
+- ๐ Research and Information Retrieval
+- ๐ฏ Task-Specific AI Assistants
+- ๐ง Email Analysis and Classification
## ๐ค Contributing
@@ -503,33 +521,10 @@ Flo AI is [MIT Licensed](LICENSE).
Built with โค๏ธ using:
- [LangChain](https://github.com/hwchase17/langchain)
-- [LangGraph](https://github.com/langchain-ai/langgraph)
-
-๐ Latest Blog Posts
-
-
-
-
- Mastering AI Interaction Logging and Data Collection with FloAI
Learn how to leverage FloAI's powerful logging system for debugging, training data generation, and system optimization
-
-
+- [Pydantic](https://github.com/pydantic/pydantic)
+- [OpenAI](https://openai.com/)
+- [Anthropic](https://www.anthropic.com/)
+
---
diff --git a/flo_ai/README.md b/flo_ai/README.md
index feffe7d3..19968686 100644
--- a/flo_ai/README.md
+++ b/flo_ai/README.md
@@ -1,533 +1,263 @@
-
-
-
-
-
Composable AI Agentic Workflow
-
-
-Rootflo is an alternative to Langgraph, and CrewAI. It lets you easily build composable agentic workflows from using simple components to any size, unlocking the full potential of LLMs.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Checkout the docs ยป
-
-
- Github
- โข
- Website
- โข
- Roadmap
-
-
-
-
-# Flo AI ๐
-
-> Build production-ready AI agents and teams with minimal code
-
-Flo AI is a Python framework that makes building production-ready AI agents and teams as easy as writing YAML. Think "Kubernetes for AI Agents" - compose complex AI architectures using pre-built components while maintaining the flexibility to create your own.
-
-## โจ Features
-
-- ๐ **Truly Composable**: Build complex AI systems by combining smaller, reusable components
-- ๐๏ธ **Production-Ready**: Built-in best practices and optimizations for production deployments
-- ๐ **YAML-First**: Define your entire agent architecture in simple YAML
-- ๐ง **Flexible**: Use pre-built components or create your own
-- ๐ค **Team-Oriented**: Create and manage teams of AI agents working together
-- ๐ **RAG Support**: Built-in support for Retrieval-Augmented Generation
-- ๐ **Langchain Compatible**: Works with all your favorite Langchain tools
-
-## ๐ Quick Start
-
-FloAI follows an agent team architecture, where agents are the basic building blocks, and teams can have multiple agents and teams themselves can be part of bigger teams.
-
-Building a working agent or team involves 3 steps:
-1. Create a session using `FloSession`, and register your tools and models
-2. Define you agent/team/team of teams using yaml or code
-3. Build and run using `Flo`
-
-### Installation
-
-```bash
-pip install flo-ai
-# or using poetry
-poetry add flo-ai
-```
+# @flo_tool Decorator
-### Create Your First AI Agent in 30 secs
+The `@flo_tool` decorator is a powerful utility that automatically converts any Python function into a `Tool` object for use with Flo AI agents. It extracts function parameters, type hints, and descriptions to create a fully functional tool with minimal boilerplate code.
-```python
-from flo_ai import Flo, FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-
-# init your LLM
-llm = ChatOpenAI(temperature=0)
-
-# create a session and register your tools
-session = FloSession(llm).register_tool(name="TavilySearchResults", tool=TavilySearchResults())
-
-# define your agent yaml
-simple_weather_checking_agent = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- job: >
- Given the city name you are capable of answering the latest whether this time of the year by searching the internet
- tools:
- - name: InternetSearchTool
-"""
-flo = Flo.build(session, yaml=simple_weather_checking_agent)
-
-# Start streaming results
-for response in flo.stream("Write about recent AI developments"):
- print(response)
-```
+## Features
-## Lets create the same agent using code
+- **Automatic parameter extraction**: Uses Python's `inspect` module to extract function parameters and type hints
+- **Flexible descriptions**: Supports custom descriptions, docstring extraction, and parameter-specific descriptions
+- **Type conversion**: Automatically converts Python types to JSON schema types
+- **Dual functionality**: Functions can be called normally AND used as tools
+- **Async support**: Works seamlessly with both sync and async functions
-```python
-from flo_ai import FloAgent
-
-session = FloSession(llm)
+## Basic Usage
-weather_agent = FloAgent.create(
- session=session,
- name="WeatherAssistant",
- job="Given the city name you are capable of answering the latest whether this time of the year by searching the internet",
- tools=[TavilySearchResults()]
-)
+### Simple Decorator
-agent_flo: Flo = Flo.create(session, weather_agent)
-result = agent_flo.invoke("Whats the whether in New Delhi, India ?")
+```python
+from flo_ai.tool import flo_tool
+
+@flo_tool()
+async def calculate(operation: str, x: float, y: float) -> float:
+ """Calculate mathematical operations between two numbers."""
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+# Function can be called normally
+result = await calculate("add", 5, 3) # Returns 8
+
+# Tool object is accessible via .tool attribute
+tool = calculate.tool
+print(tool.name) # "calculate"
+print(tool.description) # Uses function docstring
+print(tool.parameters) # Automatically extracted from type hints
```
-### Create Your First AI Team in 30 Seconds
+### With Custom Descriptions
```python
-from flo_ai import Flo, FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-
-
-# Define your team in YAML
-yaml_config = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: research-team
-team:
- name: ResearchTeam
- router:
- name: TeamLead
- kind: supervisor
- agents:
- - name: Researcher
- role: Research Specialist
- job: Research latest information on given topics
- tools:
- - name: TavilySearchResults
- - name: Writer
- role: Content Creator
- job: Create engaging content from research
-"""
-
-# Set up and run
-llm = ChatOpenAI(temperature=0)
-session = FloSession(llm).register_tool(name="TavilySearchResults", tool=TavilySearchResults())
-flo = Flo.build(session, yaml=yaml_config)
-
-# Start streaming results
-for response in flo.stream("Write about recent AI developments"):
- print(response)
+@flo_tool(
+ name="weather_checker",
+ description="Get current weather information for a city",
+ parameter_descriptions={
+ "city": "The city to get weather for",
+ "country": "The country (optional)",
+ }
+)
+async def get_weather(city: str, country: str = None) -> str:
+ """Get weather information for a specific city."""
+ # Implementation here
+ return f"Weather in {city}: sunny"
```
-**Note:** You can make each of the above agents including the router to use different models, giving flexibility to combine the power of different LLMs.
-To know more, check multi-model integration in detailed [documentation](https://flo-ai.rootflo.ai/advanced/model-switching)
-
-### Lets Create a AI team using code
+### Using Docstrings for Descriptions
```python
-from flo_ai import FloSupervisor, FloAgent, FloSession, FloTeam, FloLinear
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
-session = FloSession(llm).register_tool(
- name="TavilySearchResults",
- tool=TavilySearchResults()
-)
+@flo_tool()
+async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ """
+ Convert between different units (km/miles, kg/lbs, celsius/fahrenheit).
+
+ Args:
+ value: The value to convert
+ from_unit: The unit to convert from
+ to_unit: The unit to convert to
+ """
+ # Implementation here
+ return f"{value} {from_unit} = {result} {to_unit}"
+```
-researcher = FloAgent.create(
- session,
- name="Researcher",
- role="Internet Researcher", # optional
- job="Do a research on the internet and find articles of relevent to the topic asked by the user",
- tools=[TavilySearchResults()]
-)
+## Advanced Usage
-blogger = FloAgent.create(
- session,
- name="BlogWriter",
- role="Thought Leader", # optional
- job="Able to write a blog using information provided",
- tools=[TavilySearchResults()]
-)
+### Creating Tools from Existing Functions
-marketing_team = FloTeam.create(session, "Marketing", [researcher, blogger])
-head_of_marketing = FloSupervisor.create(session, "Head-of-Marketing", marketing_team)
-marketing_flo = Flo.create(session, routed_team=head_of_marketing)
+If you have existing functions that you want to convert to tools without modifying them:
+```python
+from flo_ai.tool import create_tool_from_function
+
+async def existing_function(text: str, style: str = "normal") -> str:
+ """Format text in different styles."""
+ styles = {
+ "uppercase": text.upper(),
+ "lowercase": text.lower(),
+ "title": text.title(),
+ "normal": text
+ }
+ return styles.get(style, text)
+
+# Convert to tool
+format_tool = create_tool_from_function(
+ existing_function,
+ name="text_formatter",
+ description="Format text in different styles",
+ parameter_descriptions={
+ "text": "The text to format",
+ "style": "The formatting style (uppercase, lowercase, title, normal)"
+ }
+)
```
-## Tools
-
-FloAI supports all the tools built and available in `langchain_community` package. To know more these tools, go [here](https://python.langchain.com/docs/integrations/tools/).
-
-Along with that FloAI has a decorator `@flotool` which makes any function into a tool.
-
-Creating a simple tool using `@flotool`:
+### Using with Agents
```python
-from flo_ai.tools import flotool
-from pydantic import BaseModel, Field
-
-# define argument schema
-class AdditionToolInput(BaseModel):
- numbers: List[int] = Field(..., description='List of numbers to add')
-
-@flotool(name='AdditionTool', description='Tool to add numbers')
-async def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The sum is {result}'
-
-# async tools can also be defined
-# when using async tool, while running the flo use async invoke
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.models.base_agent import ReasoningPattern
+
+# Create tools from decorated functions
+tools = [
+ calculate.tool,
+ get_weather.tool,
+ convert_units.tool,
+ format_tool # From create_tool_from_function
+]
+
+# Build agent with tools
+agent = (
+ AgentBuilder()
+ .with_name("Multi-Tool Agent")
+ .with_prompt("You are a helpful assistant with access to various tools.")
+ .with_llm(llm)
+ .with_tools(tools)
+ .with_reasoning(ReasoningPattern.REACT)
+ .build()
)
-async def mul_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The product is {result}'
-# register your tool or use directly in code impl
-session.register_tool(name='Adder', tool=addition_tool)
+# Use the agent
+response = await agent.run("Calculate 5 + 3 and then convert 10 km to miles")
```
-**Note:** `@flotool` comes with inherent error handling capabilities to retry if an exception is thrown. Use `unsafe=True` to disable error handling
+## Parameter Types
-## Output Parsing and formatting
+The decorator automatically converts Python types to JSON schema types:
-FloAI now supports output parsing using JSON or YAML formatter. You can now defined your output formatter using `pydantic` and use the same in code or directly make it part of the Agent Definition Yaml (ADY)
+| Python Type | JSON Schema Type |
+|-------------|------------------|
+| `str` | `string` |
+| `int` | `integer` |
+| `float` | `number` |
+| `bool` | `boolean` |
+| `list` | `array` |
+| `dict` | `object` |
+| `Optional[T]` | `T` (required: false) |
+| No annotation | `string` (default) |
-### Using Agent Defintion YAML
+## Examples
-We have added parser key to your agent schema, which gives you the output. The following is the schema of the parser
+### Complete Example
-```yaml
-name: SchemaName
-fields:
- - name: field_name
- type: data_type
- description: field_description
- values:
- - value:
- description: value_description
-```
+```python
+import asyncio
+from flo_ai.tool import flo_tool
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+
+# Define tools with decorator
+@flo_tool(
+ description="Perform basic calculations",
+ parameter_descriptions={
+ "operation": "The operation to perform (add, subtract, multiply, divide)",
+ "x": "First number",
+ "y": "Second number"
+ }
+)
+async def calculate(operation: str, x: float, y: float) -> float:
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+@flo_tool()
+async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ """Convert between different units."""
+ # Implementation here
+ return f"{value} {from_unit} = {result} {to_unit}"
+
+async def main():
+ # Create agent with tools
+ llm = OpenAI(model='gpt-4-turbo-preview')
+
+ agent = (
+ AgentBuilder()
+ .with_name("Calculator Agent")
+ .with_prompt("You can perform calculations and unit conversions.")
+ .with_llm(llm)
+ .with_tools([calculate.tool, convert_units.tool])
+ .with_reasoning(ReasoningPattern.REACT)
+ .build()
+ )
+
+ # Test the agent
+ response = await agent.run("Calculate 10 + 5 and convert 20 km to miles")
+ print(response)
-### Supported Field Types
-
-#### Primitive Types
-
-- str: String values
-- int: Integer values
-- bool: Boolean values
-- float: Floating-point values
-
-##### Complex Types
-
-- array: Lists of items
-- object: Nested objects
-- literal: Enumerated values
-
-
-Here an example of a simple summarization agent yaml that produces output a structured manner.
-
-```yaml
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: SummarizationFlo
-agent:
- name: SummaryAgent
- kind: llm
- role: Book summarizer agent
- job: >
- You are an given a paragraph from a book
- and your job is to understand the information in it and extract summary
- parser:
- name: BookSummary
- fields:
- - name: long_summary
- type: str
- description: A comprehensive summary of the book, with all the major topics discussed
- - name: short_summary
- type: str
- description: A short summary of the book in less than 20 words
+if __name__ == '__main__':
+ asyncio.run(main())
```
-As you can see here, the `parser` key makes sure that output of this agent will be the given key value format.
+## Benefits
-### Using parser with code
+1. **Reduced Boilerplate**: No need to manually create Tool objects with parameter definitions
+2. **Type Safety**: Leverages Python's type hints for automatic parameter type detection
+3. **Documentation**: Uses docstrings and parameter descriptions for better tool documentation
+4. **Flexibility**: Functions can be used both as regular functions and as tools
+5. **Maintainability**: Changes to function signatures automatically update the tool definition
-You can define parser as json in code and use it easily, here is an example:
+## Migration from Manual Tool Creation
+### Before (Manual)
```python
-format = {
- 'name': 'NameFormat',
- 'fields': [
- {
- 'type': 'str',
- 'description': 'The first name of the person',
- 'name': 'first_name',
+from flo_ai.tool.base_tool import Tool
+
+async def calculate(operation: str, x: float, y: float) -> float:
+ # Implementation
+ pass
+
+calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform',
},
- {
- 'type': 'str',
- 'description': 'The middle name of the person',
- 'name': 'middle_name',
- },
- {
- 'type': 'literal',
- 'description': 'The last name of the person, the value can be either of Vishnu or Satis',
- 'name': 'last_name',
- 'values': [
- {'value': 'Vishnu', 'description': 'If the first_name starts with K'},
- {'value': 'Satis', 'description': 'If the first_name starts with M'},
- ],
- 'default_value_prompt': 'If none of the above value is suited, please use value other than the above in snake-case',
- },
- ],
-}
-
-researcher = FloAgent.create(
- session,
- name='Researcher',
- role='Internet Researcher',
- job='What is the first name, last name and middle name of the the person user asks about',
- tools=[TavilySearchResults()],
- parser=FloJsonParser.create(json_dict=format)
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
)
-
-
-Flo.set_log_level('DEBUG')
-flo: Flo = Flo.create(session, researcher)
-result = flo.invoke('Mahatma Gandhi')
-
```
-## Output Data Collector
-
-Output collector is an infrastructure that helps you collect outputs across multiple agents into single data structure. The most useful collector is a JSON output collector which when combined with output parser gives combined JSON outputs.
-
-Usage:
+### After (With @flo_tool)
```python
-from flo_ai.state import FloJsonOutputCollector
-
-dc = FloJsonOutputCollector()
-
-# register your collector to the session
-session = FloSession(llm).register_tool(
- name='InternetSearchTool', tool=TavilySearchResults()
+from flo_ai.tool import flo_tool
+
+@flo_tool(
+ description="Perform basic calculations",
+ parameter_descriptions={
+ "operation": "The operation to perform",
+ "x": "First number",
+ "y": "Second number"
+ }
)
+async def calculate(operation: str, x: float, y: float) -> float:
+ # Implementation
+ pass
-simple_reseacher = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- kind: agentic
- job: >
- Given the person name, guess the first and last name
- tools:
- - name: InternetSearchTool
- parser:
- name: NameFormatter
- fields:
- - type: str
- description: The first name of the person
- name: first_name
- - type: str
- description: The first name of the person
- name: last_name
- - name: location
- type: object
- description: The details about birth location
- fields:
- - name: state
- type: str
- description: The Indian State in whihc the person was born
- data_collector: kv
-"""
-
-flo: Flo = Flo.build(session, simple_reseacher)
-result = flo.invoke('Gandhi')
-
-# This will output the output as JSON. The idea is that you can use the same collector across multiple agents and teams to still get a combined JSON output.
-print(dc.fetch())
-
-```
-
-## ๐ Tool Logging and Data Collection
-
-FloAI provides built-in capabilities for logging tool calls and collecting data through the `FloExecutionLogger` and `DataCollector` classes, facilitating the creation of valuable training data.
-You can customize `DataCollector` implementation according to your database. A sample implementation where logs are stored locally as JSON files is implemented in `JSONLFileCollector`.
-
-### Quick Setup
-
-```python
-from flo_ai.callbacks import FloExecutionLogger
-from flo_ai.storage.data_collector import JSONLFileCollector
-
-# Initialize the file collector with a path for the JSONL log file to be stored
-file_collector = JSONLFileCollector("'.logs'")
-
-# Create a tool logger with the collector
-local_tracker = FloExecutionLogger(file_collector)
-
-# Register the logger with your session
-session.register_callback(local_tracker)
-```
-
-### Features
-
-- ๐ Logs all tool calls, chain executions, and agent actions
-- ๐ Includes timestamps for start and end of operations
-- ๐ Tracks inputs, outputs, and errors
-- ๐พ Stores data in JSONL format for easy analysis
-- ๐ Facilitates the creation of training data from logged interactions
-
-### Log Data Structure
-
-The logger captures detailed information including:
-- Tool name and inputs
-- Execution timestamps
-- Operation status (completed/error)
-- Chain and agent activities
-- Parent-child relationship between operations
-
-### Training Data Generation
-
-The structured logs provide valuable training data that can be used to:
-- **Fine-tune LLMs** on your specific use cases
-- **Train new models** to replicate successful tool usage patterns
-- **Create supervised datasets** for tool selection and chain optimization
-
-We have created a script to convert your logs to training data:
-
-```python
-python generate_training_data.py --logger-path PATH --tool-path PATH [--output PATH]
+# Tool is automatically available as calculate.tool
```
-Arguments:
-- *logger-path*: Path to the logger file containing tool and chain entries, eg: .logs/logs/log.jsonl
-- *tool-path*: Path to the tool descriptions file eg: eg: .logs/tools/tools.jsonl
-- *output*: path to save the output eg: training-data.jsonl
-
-
-## ๐ Documentation
-
-Visit our [comprehensive documentation](https://flo-ai.rootflo.ai) for:
-- Detailed tutorials
-- Architecture deep-dives
-- API reference
- - Logging
- - Error handling
- - Observers
- - Dynamic model switching
-- Best practices
-- Advanced examples
-
-## ๐ Why Flo AI?
-
-### For AI Engineers
-- **Faster Development**: Build complex AI systems in minutes, not days
-- **Production Focus**: Built-in optimizations and best practices
-- **Flexibility**: Use our components or build your own
-
-### For Teams
-- **Maintainable**: YAML-first approach makes systems easy to understand and modify
-- **Scalable**: From single agents to complex team hierarchies
-- **Testable**: Each component can be tested independently
-
-## ๐ฏ Use Cases
-
-- ๐ค Customer Service Automation
-- ๐ Data Analysis Pipelines
-- ๐ Content Generation
-- ๐ Research Automation
-- ๐ฏ Task-Specific AI Teams
-
-## ๐ค Contributing
-
-We love your input! Check out our [Contributing Guide](CONTRIBUTING.md) to get started. Ways to contribute:
-
-- ๐ Report bugs
-- ๐ก Propose new features
-- ๐ Improve documentation
-- ๐ง Submit PRs
-
-## ๐ License
-
-Flo AI is [MIT Licensed](LICENSE).
-
-## ๐ Acknowledgments
-
-Built with โค๏ธ using:
-- [LangChain](https://github.com/hwchase17/langchain)
-- [LangGraph](https://github.com/langchain-ai/langgraph)
-
-๐ Latest Blog Posts
-
-
-
-
- Mastering AI Interaction Logging and Data Collection with FloAI
Learn how to leverage FloAI's powerful logging system for debugging, training data generation, and system optimization
-
-
----
-
-
+The `@flo_tool` decorator significantly reduces the amount of code needed to create tools while maintaining all the functionality and flexibility of the original Tool class.
\ No newline at end of file
diff --git a/flo_ai/examples/agent_builder_usage.py b/flo_ai/examples/agent_builder_usage.py
new file mode 100644
index 00000000..2fa610d3
--- /dev/null
+++ b/flo_ai/examples/agent_builder_usage.py
@@ -0,0 +1,115 @@
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.tool.base_tool import Tool
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.llm.anthropic_llm import Anthropic
+
+
+async def example_simple_agent():
+ # Create a simple conversational agent with OpenAI
+ agent = (
+ AgentBuilder()
+ .with_name('Math Tutor')
+ .with_prompt('You are a helpful math tutor.')
+ .with_llm(OpenAI(model='gpt-4o-mini'))
+ .build()
+ )
+
+ response = await agent.run('What is the formula for the area of a circle?')
+ print(f'Simple Agent Response: {response}')
+
+
+async def example_tool_agent():
+ # Define a calculator tool
+ async def calculate(operation: str, x: float, y: float) -> float:
+ if operation == 'add':
+ return x + y
+ elif operation == 'multiply':
+ return x * y
+ raise ValueError(f'Unknown operation: {operation}')
+
+ calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add or multiply)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+ # Create a tool-using agent with Claude
+ agent_openai = (
+ AgentBuilder()
+ .with_name('Calculator Assistant')
+ .with_prompt('You are a math assistant that can perform calculations.')
+ .with_llm(OpenAI(model='gpt-4o', temperature=0.7))
+ .with_tools([calculator_tool])
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
+ )
+
+ agent_claude = (
+ AgentBuilder()
+ .with_name('Calculator Assistant')
+ .with_prompt('You are a math assistant that can perform calculations.')
+ .with_llm(Anthropic(model='claude-3-5-sonnet-20240620', temperature=0.7))
+ .with_tools([calculator_tool])
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
+ )
+
+ response = await agent_openai.run('Calculate 5 plus 3')
+ print(f'OpenAI Tool Agent Response: {response}')
+
+ response = await agent_claude.run('Calculate 5 plus 3')
+ print(f'Claude Tool Agent Response: {response}')
+
+
+async def example_structured_output():
+ # Define output schema for structured responses
+ math_schema = {
+ 'type': 'object',
+ 'properties': {
+ 'solution': {'type': 'string', 'description': 'The step-by-step solution'},
+ 'answer': {'type': 'string', 'description': 'The final answer'},
+ },
+ 'required': ['solution', 'answer'],
+ }
+
+ # Create an agent with structured output
+ agent = (
+ AgentBuilder()
+ .with_name('Structured Math Solver')
+ .with_prompt(
+ 'You are a math problem solver that provides structured solutions.'
+ )
+ .with_llm(OpenAI(model='gpt-4o'))
+ .with_output_schema(math_schema)
+ .build()
+ )
+
+ response = await agent.run('Solve: 2x + 5 = 15')
+ print(f'Structured Output Response: {response}')
+
+
+async def main():
+ print('\n=== Simple Conversational Agent ===')
+ await example_simple_agent()
+
+ print('\n=== Tool-using Agent ===')
+ await example_tool_agent()
+
+ print('\n=== Structured Output Agent ===')
+ await example_structured_output()
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/agentic_rag.ipynb b/flo_ai/examples/agentic_rag.ipynb
deleted file mode 100644
index cc3e97ce..00000000
--- a/flo_ai/examples/agentic_rag.ipynb
+++ /dev/null
@@ -1,251 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Agentic RAG implemented using FloAI\n",
- "\n",
- "FloAI has just made implementing agentic RAG simple and easy to manage. The Diagram shows what we are going to implement:\n",
- "\n",
- "\n",
- "

\n",
- "
\n",
- "\n",
- "To implement this we use the following components, already available in flo-ai:\n",
- "\n",
- "1. `tool` agent: This will be a tool agent to retrieve the records from vector store\n",
- "2. `delegator` agent: The agent will check if the retrieved records are relevent, else it will re-write the query and send it back for re-retrieval\n",
- "3. `llm` agent: This will generate the output from the relevant documents"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from flo_ai import Flo\n",
- "from flo_ai import FloSession\n",
- "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
- "from langchain_chroma import Chroma\n",
- "from langchain_community.document_loaders import TextLoader\n",
- "from langchain_community.embeddings.sentence_transformer import (\n",
- " SentenceTransformerEmbeddings,\n",
- ")\n",
- "from langchain_text_splitters import CharacterTextSplitter\n",
- "\n",
- "from dotenv import load_dotenv\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Setting up Vector Store with Sample Data\n",
- "\n",
- "Using Chroma is this example"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "/Users/vizsatiz/Documents/hub/flo/.venv/lib/python3.11/site-packages/sentence_transformers/cross_encoder/CrossEncoder.py:11: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",
- " from tqdm.autonotebook import tqdm, trange\n",
- "/Users/vizsatiz/Documents/hub/flo/.venv/lib/python3.11/site-packages/transformers/tokenization_utils_base.py:1601: FutureWarning: `clean_up_tokenization_spaces` was not set. It will be set to `True` by default. This behavior will be depracted in transformers v4.45, and will be then set to `False` by default. For more details check this issue: https://github.com/huggingface/transformers/issues/31884\n",
- " warnings.warn(\n"
- ]
- }
- ],
- "source": [
- "# load the document and split it into chunks\n",
- "loader = TextLoader(\"./data/rag_document.txt\")\n",
- "documents = loader.load()\n",
- "\n",
- "# split it into chunks\n",
- "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n",
- "docs = text_splitter.split_documents(documents)\n",
- "\n",
- "# create the open-source embedding function\n",
- "embedding_function = SentenceTransformerEmbeddings(model_name=\"all-MiniLM-L6-v2\")\n",
- "\n",
- "# load it into Chroma\n",
- "db = Chroma.from_documents(docs, embedding_function)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Creating the retrival tool\n",
- "\n",
- "This tool will retrive the records from vector db"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "2024-09-23 14:42:29,261 - SESSION - INFO - New FloSession created with ID: fb386a6f-07b9-4cf4-a83a-21f322c7289c\n",
- "2024-09-23 14:42:29,359 - SESSION - INFO - Tool 'HousingLoanTool' registered for session fb386a6f-07b9-4cf4-a83a-21f322c7289c\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 3,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai.retrievers.flo_retriever import FloRagBuilder\n",
- "from flo_ai.retrievers.flo_compression_pipeline import FloCompressionPipeline\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "session = FloSession(llm)\n",
- "builder = FloRagBuilder(session, db.as_retriever())\n",
- "compression_pipeline = FloCompressionPipeline(OpenAIEmbeddings(model=\"text-embedding-3-small\"))\n",
- "compression_pipeline.add_embedding_reduntant_filter()\n",
- "compression_pipeline.add_embedding_relevant_filter()\n",
- "\n",
- "retriever_tool = builder.with_compression(compression_pipeline).build_retriever_tool(name=\"HousingLoanRetreiver\",\n",
- " description=\"Tool to fetch data around housing loans\")\n",
- "session.register_tool(name=\"HousingLoanTool\", tool=retriever_tool)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Creating the flo\n",
- "\n",
- "This follow as you can see consist of the following components:\n",
- "\n",
- "1. HousingLoanRetriver: This is a `tool agent`, with ability to retrieve from vector store\n",
- "2. RelevancyChecker: This is a `delegator agent` with ability to delegate based on relevancy\n",
- "3. ResponseGenerator: This is a `llm agent` with ability to response based on documents"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "2024-09-23 14:45:06,389 - BUILDER - INFO - Building Flo instance from YAML\n",
- "2024-09-23 14:45:06,398 - COMMON - INFO - Flo instance created for session fb386a6f-07b9-4cf4-a83a-21f322c7289c\n"
- ]
- },
- {
- "data": {
- "image/jpeg": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "agent_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: agentic-rag\n",
- "team:\n",
- " name: AgenticRAGTeam\n",
- " router:\n",
- " name: SupportSupervisor\n",
- " kind: linear\n",
- " agents:\n",
- " - name: HousingLoanRetriver\n",
- " kind: tool\n",
- " job: Fetching the loan information from the loan tool\n",
- " tools:\n",
- " - name: HousingLoanTool\n",
- " - name: RelevancyChecker\n",
- " kind: delegator\n",
- " to:\n",
- " - name: HousingLoanRetriver\n",
- " - name: ResponseGenerator\n",
- " retry: 1\n",
- " job: >\n",
- " Your job is to check if the records fetched by the retriever are relevent to the question.\n",
- " If its not relevant re-write the query send to HousingLoanRetriver for re-retrieval.\n",
- " If its relevant then send to ResponseGenerator for response generation\n",
- " - name: ResponseGenerator\n",
- " kind: llm\n",
- " job: Based on the documents given answer the user question that was asked\n",
- "\"\"\"\n",
- "\n",
- "flo = Flo.build(session, agent_yaml)\n",
- "\n",
- "flo.draw()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [
- {
- "name": "stderr",
- "output_type": "stream",
- "text": [
- "2024-09-23 14:45:11,703 - COMMON - INFO - Invoking query for session fb386a6f-07b9-4cf4-a83a-21f322c7289c: Whats the interest rate for housing loan ?\n"
- ]
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'messages': [HumanMessage(content='Whats the interest rate for housing loan ?'), HumanMessage(content='You can use any one of the ways to repay the loan:\\n \\n Cheques\\n Standing instructions at your branch\\n FedNet - Internet Banking\\n Automated Payment through ECS\\n Mobile Banking\\n\\n Housing loan interest rates will change subject to the changes made by Bank/RBI from time to time. \\n\\nPresent Repo Rate\\t 6.50% (p.a)\\n \\n\\nLoan scheme\\n\\nInterest Rate (%) * \\n\\nHome Loan \\n\\n8.80 (Repo Rate+ 2.30) Onwards \\n\\n*T&C Apply\\nHousing Loan\\n\\nYour dream home is never far away! Get hassle free home loans from Federal Bank to turn your dream home into reality. We assist you to realize your dream home. Avail your Housing Loan from us at competitive interest rates. The loan scheme assists borrowers for construction of house, acquisition of land & construction of house, repairs / renovation / remodeling / extension of house, reimbursement of debt incurred for construction / purchase / furnishing / beautification / purchase of flat / villa / house plots / takeover of housing loans / supplementary housing loan to employees of well-run companies / purchase of house plot for subsequent construction of house etc.', name='HousingLoanRetriver'), HumanMessage(content='The interest rate for housing loans currently starts at 8.80% per annum, which is based on the present repo rate of 6.50% plus an additional margin of 2.30%. Please note that interest rates may change based on adjustments made by the bank or the Reserve Bank of India (RBI) over time.', name='ResponseGenerator')], 'next': 'ResponseGenerator', 'loop_tracker': {'RelevancyChecker': 1}}\n"
- ]
- }
- ],
- "source": [
- "print(flo.invoke(\"Whats the interest rate for housing loan ?\"))"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.19"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/agents_of_flo_ai.ipynb b/flo_ai/examples/agents_of_flo_ai.ipynb
deleted file mode 100644
index 8d45d1a2..00000000
--- a/flo_ai/examples/agents_of_flo_ai.ipynb
+++ /dev/null
@@ -1,635 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# The Agents of FloAI\n",
- "\n",
- "This notebook shows different kinds of agents that can be built using flo-ai. These agents have their own specialities, and this notebook tries to explain them.\n",
- "\n",
- "Here are the major types of agents:\n",
- "\n",
- "1. Agentic Agents (`kind: agentic`): Any agent created in flo-ai by default tries to be an agentic agent. This agent always needs a tool, meaning an agent without a tool if specified as agentic agent whill throw an exception\n",
- "\n",
- "2. LLM Agents (`kind: llm`): These agents are simply an LLM which can answer any questions asked to it. The agents dont except tools. If tool is passed to an agent of type llm, they are ignored.\n",
- "\n",
- "3. Tool Agents (`kind: tool`): These agents are just tools or functions that can be executed on the current state. Within the tool will be given the current state as the input, meaning the history of what happened in the flo until now\n",
- "\n",
- "4. Reflection Agents (`kind: reflection`): These agents can help in reflecting on the current answer and retrying an exisitng flow. This is useful when you want to re-evaluate an answer with better model for example.\n",
- "\n",
- "5. Delegator Agent (`kind: delegator`): These agents can delegate to any other agent with the workflow based on a prompt. For example you want to delegate to different agents based on the user question"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import Flo\n",
- "from flo_ai import FloSession\n",
- "from flo_ai.common.flo_logger import get_logger\n",
- "from flo_ai.common.flo_langchain_logger import FloLangchainLogger\n",
- "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
- "\n",
- "from dotenv import load_dotenv\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Setup\n",
- "Create the Flo session, setup tools"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 2,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from langchain_community.tools.tavily_search.tool import TavilySearchResults\n",
- "from flo_ai.common.flo_langchain_logger import FloLangchainLogger\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "\n",
- "session = FloSession(\n",
- " llm\n",
- ")\n",
- "\n",
- "session.register_tool(\n",
- " name=\"InternetSearchTool\", \n",
- " tool=TavilySearchResults()\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Agentic Agent (agentic)\n",
- "\n",
- "Here we are gonna create a simple weather assitant flo agent that can check the whether by an internet searching tool.\n",
- "\n",
- "As you can see the kind is `agentic`"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "simple_weather_checking_agent = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloAgent\n",
- "name: weather-assistant\n",
- "agent:\n",
- " name: WeatherAssistant\n",
- " kind: agentic\n",
- " job: >\n",
- " Given the city name you are capable of answering the latest whether this time of the year by searching the internet\n",
- " tools:\n",
- " - name: InternetSearchTool\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from IPython.display import Image, display\n",
- "flo = Flo.build(session, simple_weather_checking_agent)\n",
- "\n",
- "flo.draw(xray=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'messages': [HumanMessage(content='Whats the whether in New Delhi, India ?', additional_kwargs={}, response_metadata={}), HumanMessage(content='As of now, the weather in New Delhi, India is as follows:\\n\\n- **Temperature**: 32.2ยฐC (90ยฐF)\\n- **Condition**: Mist\\n- **Humidity**: 46%\\n- **Wind**: 9.2 mph (14.8 kph) from the WSW\\n- **Visibility**: 4 km\\n- **Feels Like**: 30ยฐC (86ยฐF)\\n\\nFor more detailed forecasts and updates, you can check [Weather API](https://www.weatherapi.com/) or [World Weather Info](https://world-weather.info/forecast/india/delhi/october-2023/).', additional_kwargs={}, response_metadata={}, name='WeatherAssistant')]}\n"
- ]
- }
- ],
- "source": [
- "result = flo.invoke(\"Whats the whether in New Delhi, India ?\")\n",
- "print(result)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Lets create the same agent but using code"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "data": {
- "text/plain": [
- "{'messages': [HumanMessage(content='Whats the whether in New Delhi, India ?', additional_kwargs={}, response_metadata={}),\n",
- " HumanMessage(content='As of now, the weather in New Delhi, India is as follows:\\n\\n- **Temperature**: 32.2ยฐC (90ยฐF)\\n- **Condition**: Mist\\n- **Wind**: 9.2 mph (14.8 kph) from the WSW\\n- **Humidity**: 46%\\n- **Visibility**: 4 km\\n- **Feels Like**: 30ยฐC (86ยฐF)\\n\\nFor more detailed forecasts and updates, you can check [Weather API](https://www.weatherapi.com/) or [World Weather Info](https://world-weather.info/forecast/india/delhi/october-2023/).', additional_kwargs={}, response_metadata={}, name='WeatherAssistant')]}"
- ]
- },
- "execution_count": 5,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import FloAgent\n",
- "\n",
- "weather_agent = FloAgent.create(\n",
- " session=session,\n",
- " name=\"WeatherAssistant\",\n",
- " job=\"Given the city name you are capable of answering the latest whether this time of the year by searching the internet\",\n",
- " tools=[TavilySearchResults()]\n",
- ")\n",
- "\n",
- "agent_flo: Flo = Flo.create(session, weather_agent)\n",
- "agent_flo.draw(xray=False)\n",
- "\n",
- "agent_flo.invoke(\"Whats the whether in New Delhi, India ?\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## LLM Agent (llm)\n",
- "\n",
- "Here we are gonna create a simple llm math assitant flo agent that can check answer any math question\n",
- "\n",
- "As you can see the kind is `llm`"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "simple_llm_agent = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloAgent\n",
- "name: llm-assistant\n",
- "agent:\n",
- " name: ask-llm-anything\n",
- " kind: llm\n",
- " job: >\n",
- " You are a high school maths teacher. Answer any questions the students ask \n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "flo = Flo.build(session, simple_llm_agent)\n",
- "\n",
- "flo.draw(xray=True)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{}\n",
- "---------g------\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "{'messages': [HumanMessage(content='What is pythagorus theorum, just give me the formula', additional_kwargs={}, response_metadata={}),\n",
- " HumanMessage(content='The Pythagorean theorem states that in a right triangle, the square of the length of the hypotenuse (the side opposite the right angle) is equal to the sum of the squares of the lengths of the other two sides. The formula is:\\n\\n\\\\( a^2 + b^2 = c^2 \\\\)\\n\\nwhere \\\\( c \\\\) is the length of the hypotenuse, and \\\\( a \\\\) and \\\\( b \\\\) are the lengths of the other two sides.', additional_kwargs={}, response_metadata={}, name='ask-llm-anything')]}"
- ]
- },
- "execution_count": 11,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "flo.invoke(\"What is pythagorus theorum, just give me the formula\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Code implementation"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{}\n",
- "---------g------\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "{'messages': [HumanMessage(content='What is pythagorus theorum, just give me the formula', additional_kwargs={}, response_metadata={}),\n",
- " HumanMessage(content='The Pythagorean theorem states that in a right triangle, the square of the length of the hypotenuse (the side opposite the right angle) is equal to the sum of the squares of the lengths of the other two sides. The formula is:\\n\\n\\\\[ c^2 = a^2 + b^2 \\\\]\\n\\nwhere \\\\( c \\\\) is the length of the hypotenuse, and \\\\( a \\\\) and \\\\( b \\\\) are the lengths of the other two sides.', additional_kwargs={}, response_metadata={}, name='WeatherAssistant')]}"
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import FloLLMAgent\n",
- "\n",
- "simple_llm_agent = FloLLMAgent.create(\n",
- " session=session,\n",
- " name=\"WeatherAssistant\",\n",
- " job=\"You are a high school maths teacher. Answer any questions the students ask \",\n",
- ")\n",
- "\n",
- "agent_flo: Flo = Flo.create(session, simple_llm_agent)\n",
- "agent_flo.draw()\n",
- "\n",
- "agent_flo.invoke(\"What is pythagorus theorum, just give me the formula\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Tool Agent (tool)\n",
- "\n",
- "Lets create a simple tool agent, which has just a tool and nothing else. The tool agent just executes a tool and nothing else. The tool can invoke llms within if in needs to"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "#"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "from flo_ai.tools import flotool\n",
- "from langchain.tools import BaseTool\n",
- "\n",
- "@flotool(\"print_tool\", \"To print the state\")\n",
- "def print_state(**kwargs):\n",
- " print(kwargs)\n",
- " return \"Print tool call success\"\n",
- " \n",
- "session.register_tool(\n",
- " name=\"printStateTool\", \n",
- " tool=print_state\n",
- ")\n",
- " \n",
- "simple_tool_agent = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloAgent\n",
- "name: llm-tool\n",
- "agent:\n",
- " name: tool-to-print-state\n",
- " kind: tool\n",
- " tools:\n",
- " - name: printStateTool\n",
- "\"\"\"\n",
- "\n",
- "flo = Flo.build(session, simple_tool_agent)\n",
- "\n",
- "flo.draw()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{}\n",
- "{'messages': [HumanMessage(content='Print what I am saying', additional_kwargs={}, response_metadata={}), HumanMessage(content='Print tool call success', additional_kwargs={}, response_metadata={}, name='tool-to-print-state')]}\n"
- ]
- }
- ],
- "source": [
- "print(flo.invoke(\"Print what I am saying\"))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Building with code"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- },
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{}\n",
- "Print tool call success\n"
- ]
- }
- ],
- "source": [
- "from flo_ai import FloToolAgent\n",
- "\n",
- "simple_tool_agent = FloToolAgent.create(\n",
- " session=session,\n",
- " name=\"llm-tool\",\n",
- " tool=print_state\n",
- ")\n",
- "\n",
- "agent_flo: Flo = Flo.create(session, simple_tool_agent)\n",
- "agent_flo.draw()\n",
- "\n",
- "print(flo.invoke(\"Print what I am saying\"))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Reflection Agent (reflection)\n",
- "These agents help evaluate a work based on certain criteria. In the graph you can see flo has automatically added reflection manager to handle retries and its count."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAAAX4AAAGwCAIAAAAhWplvAAAAAXNSR0IArs4c6QAAIABJREFUeJzt3XdYU+ffBvAnOyGBsDcIyBZkKiouFAfuWXFvrVr3qFZ/1lFr3YpadyvuPepoAbVqHagoqCgyBEX2HgkJZL1/nL4ppSwx4UkO38/Vqxc5yTm5DcnNc0bOoSgUCgQAAM2LijsAAKAlguoBAGAA1QMAwACqBwCAAVQPAAADqB4AAAZ03AGAamR/EInKZRVlMplUUSmW447TMBabSqVTdHRpOno0CzsO7jiguVHguB7tpVAo3j0rT4sXpsYLW7npUKkUHT2agSmzSqQF1cPkUEvyqirKZVKJIv1dhV0bHQdPnlt7XQqFgjsaaA5QPdoq7l5JTFSRnTvX3oPr4MGlULX7E5sWL0x9LfiYUOETpO8TZIA7DlA7qB7tk/W+4vejuc6+vE4DjWl07W6cGhRyxcNrhe+elfWdaG7tpIM7DlAjqB4t8/pBacrL8j4TzHV0SbudTiSQRZ7IsXPjenXTx50FqAtUjzZJjCnP/iDqPsIUd5DmcP9yvpEFs00HPu4gQC2gerTG4xuFonJpj1Az3EGaz90LeXQGtfNgY9xBgOrBcT3aIelFeVmhpEX1DkKo+whTkUD27lkZ7iBA9aB6tEBBVmVavLDPBHPcQTDoNdYs/V1FfmYl7iBAxaB6tMDDqwVuAXq4U2DTpiP/weUC3CmAikH1aLqM5Aq5HNm6tNw9zVaOHBqD8jFBiDsIUCWoHk2X8LQ8cLAR7hSYBQ42SoAtPuQC1aPRhKXST0kVptZs3EEwMzJn5X6sLCuU4A4CVAaqR6OlxgsdPLjN/KTnzp1bs2ZNE2b89ttvr127poZECCFk78FNi4d1LvKA6tFoOR/Ejj68Zn7ShISEZp6xMVp7cXPTxepbPmhmcEihRgtf/2HobCs9I4Y6Fh4bG7t3796UlBSZTObs7DxnzhxfX98ZM2a8ePGCeMDJkyddXFz++OOP48ePp6enM5nMtm3bLl682NramhjjUCgUOzu7EydObNy4ceHChcRcPB7v7t27Kk9bXiy5GJY56Xs7lS8ZYAGjHo1WUSbT0aOpY8kikWjBggUODg6//vpreHi4k5PTvHnzysrKtm/f7urq2rt371u3bjk6Or5582bVqlWBgYHHjx8PCwsTiURLly4llsBgMFJSUt69excWFubp6Xnz5k2E0NKlS69evaqOwFw9urBMqo4lAyxI+xVEEhBXyOgMCp2hlj8POTk5QqGwX79+9vb2CKElS5b06tWLyWSy2Ww6nc5kMvX19RFCrVq1On78uJOTE51ORwiNGTNm0aJFRUVFhoaGCKGMjIwjR47w+XyEUGVlJUJIR0eHuKlyVBqFxaGKBDIOTy1dDJoZVI/mkskUHF11fcxsbW1btWq1atWqESNGdOjQwcXFxc/P778P4/F4mZmZe/bs+fTpk1gslkgkCKGysjKielq1aqWmoqmVji5dJpUjBNVDBrDCpbm4uvTSfIlcrpaNcTQa7fDhw8HBwZcvXx43btzAgQNv3Ljx34dFRkYuX77cw8MjLCzs1KlTK1eurH4vj9esm8CLcqp4+mrZ7AWaH1SPRtPRo1WUydS0cAMDgwULFly9evXcuXPt27f//vvv/7uL6vLly/7+/rNmzbKzszM2NhaLse1jEpZJddQ2BgTND6pHo9k46ahp22pmZqZyP5SDg8N3331HpVLfv39PTFHu96yqqiI2+hD++OOP6vf+l/p2mFaUyayd4ezx5AHVo9EMzJnvXwrUseScnJxly5adOHHiw4cPHz9+PHz4MJVK9fT0RAjp6uomJiYmJiaWlJR4eHhER0fHx8dnZ2dv3LjR2NgYIfT27dv/Dn9YLBaLxXrx4kViYqJUqvq6fP9KYGDKVPliAS60ph23CpoHh0t7GlHUtovqzxNqaWlpaWl58eLFo0ePXr16taKiYvny5W3btkUI8fn8GzduXLp0ycfHp3fv3snJyQcPHrx586afn9/ChQtfvXp19uxZOzu79PR0gUAwePBg5TLlcvnly5cjIiJGjBjBYrFUG/ivKwV+PQ109GDHCEnAIYWa7sYv2Z0HGfGNW/QffEGJ5M/z+QOnW+IOAlQGVrg0nZM3L/pmEe4UmEXfLHL0au4vlAC1guGrpnP21Y25VVyYVWlkWfsqzMSJEz9+/Pjf6TKZjNiJXutcV69eVdMhOXFxcQsWLKj1LplMVlcehNDt27drvbc4tyrnozh4TMs6OSzpwQqXFkh/J0x7I+w2vPYLUQiFQrm8lsuNEtt6iaOQ/4vH46npOp9SqVQkEtV1F41Gq+t5dXV1a51+/1K+rauOnXtzf4MfqBVUj3Z4Flkkkyo69Gtx5wxrsf9w0oNtPdqhXW/DknzJy/vFuIM0q/iHJXmfKqF3SAlGPdrk4bUCHp/u1bVFXJPz9aPSouzKulYzgbaD6tEyd8/nUaiUbsNNcAdRr78u50sqFT1CoXdIC6pH+8Q/LI3+vbDTAGP3DiS8Qk7C07JH1wrb9TFo27lFDO5aLKgerSQSyB5dL8j7VOnqr2vvwdU30foDDksLJWmvhclx5QamzE4DjXR04bAPkoPq0WIl+VXxj8rS4oVUKrJ102GyqFw9uq4hXaau77qrEp1OKSuSCEulVWL5pySRtEpu78l176BnZK7ib2AAzQTVQwZFuVU5H0SCEpmwTEqjUcqLVfztzefPn/v6+qr2OCBdA7pMquDy6Tx9upkty8gCGqdlgeoBDQsICHj48GFdRycC0ARwXA8AAAOoHgAABlA9oGEeHh5q+sIXaLGgekDD4uPjYZsgUC2oHtAwAwMDGPUA1YLqAQ0rLi6GUQ9QLage0DAbGxsY9QDVguoBDfv06ROMeoBqQfWAhvn4+MCoB6gWVA9oWGxsLIx6gGpB9QAAMIDqAQ0zNTWFFS6gWlA9oGF5eXmwwgVUC6oHNMzc3BxGPUC1oHpAw3JycmDUA1QLqgcAgAFUD2iYq6srrHAB1YLqAQ179+4drHAB1YLqAQBgANUDGubl5QUrXEC1oHpAw16+fAkrXEC1oHoAABhA9YCGwTfXgcpB9YCGwTfXgcpB9QAAMIDqAQ2Di+EAlYPqAQ2Di+EAlYPqAQBgANUDGgbX4QIqB9UDGgbX4QIqB9UDGubm5gajHqBaUD2gYQkJCTDqAaoF1QMAwACqBzTMysoKVriAakH1gIZlZmbCChdQLage0DD4+ihQOage0DD4+ihQOage0DAY9QCVg+oBDYNRD1A5qB7QMHt7exj1ANWiwF8zUJeQkBAGg6FQKAoKCoyMjKhUqkwms7Oz27t3L+5oQOvRcQcAmisvL0852MnJyUEI6enpjR8/HncuQAawwgXq1KlTpxpTnJ2dO3TogCkOIBWoHlCniRMn6unpKW/q6upOnDgRayJAHlA9oE7+/v4uLi7EzwqFwt3dvWPHjrhDAZKA6gH1mTx5spGREUKIz+ePGzcOdxxAHlA9oD7t27d3c3NTKBQuLi4w5AEqBHu48CsvlhTlVMlkuHPUYUCPqUUZjMG9xqTGC3FnqR2NRjEwY+gZMnAHAZ8BjuvBKS9DHH2zqDCrytadKyyW4o6jrXgG9PQEoYE5s0OIoZktG3cc0ChQPdgU51VdP5Tda6IVVxfGniogEkojjmb2n2JuaM7CnQU0DLb14CESyC7syhjyTSvoHVXhcOlD5rS6vDdLWAbjRy0A1YPH04iiToNMcacgoY6DTJ5GFOFOARoG1YNHRrJIFzaLqoGeETMjSYQ7BWgYVA8eFArSNWDiTkFCeoZMCpWikMMWTE0H1YNHeZFEIccdgpQUqDS/ikKFU3xoOqgeAAAGUD0AAAygegAAGED1AAAwgOoBAGAA1QMAwACqBwCAAVQPAAADqB4AAAZQPQAADKB6AAAYwMlitENySuKMmWNrvevIoTMODo7qDjBr9gQWm71z+0HllGcx0cu+/WbB/OWDB41QTty46ftnzx5fuhBZY/bv1ywTCMq3bd2n7pxAW0D1aJNJE2e2adO2xkQLC6tmeGo/v4Cz546LxWI2++8zkMbFxRD/r149cXExfn4B/519wIBhUomE+HnN2m87dOjct8/AZogNNBZUjzZp3drJv7YPdjPw8ws4eerX1/Fx7fz/vvroi9hn7fw7xMbFKBQK4vrIGRnpeXm5/r61JFTOhRBKSkro0KFzM2YHmgiqhyRyc3P2H9gZ9/J5RYXQ3NxyxPAxAwcMq2e6TCY7dvzQ7dt/5Bfk6enxAzt1mzljPofDmbdgGovJ2rJ5r3LJ/1u9pLCoYNeOQ2w2+8WLp0SJCASCpKSEdWu2rFq9ODU1pXVrJ4RQbFwMUVIIoSHDgseNnfIsJjo29tmlC1Fbtq4jVriCevojhDZtXrv3523Xrt5FCN2+E3H+/ImP6Wkcjk6PoD7Tps4hBlZr1n5LoVBsbe3OnT+xb+8xOzsHnK8vUDWoHm0ilUorKyurT6HRaHQ6HSG0ecvaKknVjxt26unxY2Kid+76ydzcsp1/h7qmX7h46tTpoyuWr3N2cs3Oydq8ZS2NTp87Z0n/kCGbtqwtKMg3NjZBCIlEomcxj2fPWsRgMNq29X3x4inxvK9evWAymQEBgTY2rWLjnv1dPbHP7OwciBnpdPq165c6dew6Ydw05ToaQujcmZtfhfab+83Snj37IoQePLj7w4aVY0ZPWrXqx4yM9O07NpSWlaxcsR4hxGAwkpLfiSvFP/0YZm5u2ewvNlAvqB5tsnbd8hpTOnTovHHDToRQalrK0CGj3FzbIISsBo1wdnI1M7OoZ3pwz5B2/h2J7dPW1rZB3Xs/efoQIdStW/Cen7fevvPHqK/GI4QeR/+lUCh6BPVBCPn7Bezbv7OsvExPV+9F7LM27m3pdLpXW98Xsc9GDB+DEIp7+Zx4JEKIQqGwWeyZM+bVCKynx0cI6ejo8PX4CKFTZ456eflOn/YNQsjaymb6tLk/bvzf9KnfmJqaKRDKysoI23WEeCQgGagebTJj+lyvtr7Vp/B4usQPnTp2PX3mqEBQHhAQ2NbTx83No/7pfL5+ZNSNrdt/KCjIk0qlIlEFh6ODEGKz2T2C+kRG3SCq5/792106B/F4PISQn2+AQqF4+fJ5l85BsXHPgrr3Rgh5efntCvtJJpOlp38oLi6qvinqv1vEa5DL5UlJCZMmzlRO8fbyQwilpiabmpohhGxsWkHvkBVUjzaxsWnl7u5Z610LF6xwsHeMunXz/IWTXC530MARUybPotPpdU3fvWdL1K2bC+evaOPhxWKyTp8Jv/NnBLGofv2G/HbtYkpKkrW17ZOnD9et3UpMd3BwNDIyfvHiaVtPn9TUlIXzVyCEfLz9BQJBckri27ev6XS6l5efMhKXy6v/nyMWi2Uy2dHwA8eOH6o+vbCooJFLANoLqock6HT68OGjhw8fXVRUGBl148gvP+vrG3w1clyt04cPG33z96vjx03r1asfMbtQKFAuysXZzcnR5e69KCcnVz09vp9ve+Vdvr7tX8fHvX4dx2azXV3bIISMjIytrW1fv459+/Z1mzZtORxO4zOz2Ww6nT5saGj/fkOqT9c3MFTFSwI0GhzNTAYCgSDq1u9SqRQhZGhoFDpqgru7Z2pqSl3T5XK5TCbT+/91GaFQ+Ojx/erXoQ0JGfzn3ai7d6N69+pPpf7zJvH3DUhLex/7MsbTw5vYvE2sJSUkxL9NeO1X2271WhHPRaVSnZxcc3OzbW3tiP8sLKxodLqerp5KXx6giWDUo02Sk98xGDUvoWNtbavPNwjbven58yfDh4/mcnlv375OSkoI6TuIQqHUOp3BYDg5ukREXm/XrqNYJArbszkgIPDOnYj09A+WltZ0Oj04OOTAwV0P8nJ+/eV89efy8wuQy+VRkTdCQycqJ3p5+YWFbSoXlNd6MGENLBaLxWK9fPXC0dHF3q516KgJa9Z+e+r00S6dg8SV4lOnfn31OvbY0UtcLld1LxvQRFA92uTY8cP/nTh50tcTxk/b9NOew4f3LFo8s6qqytzccvKkr4nDheuavnTJ6i1b102Z+pW5ueWUybPcXD3exL+cNWfC4UNnLMwtdXm63t7+FRVCayub6s9lZGRsb986Le29d7VtOj7e/uWCch6X5+Ls1ph/xejQSWfOhj9+/NeJ41e6dunx3Yr1p88c/fXofi6X5+HhtWPbAeidloBSfZgNms2Bb9+PXOzAYGno5aJKSorHjBu0bOn33bsF487yeRRydHx9ypztav9SG/hCMOoB/1JaVpqV+WnPz9tatXLo2qUH7jiAtGAzM/iXiIhr8xZM47A5a1Zvqr6BGQDVglEP+JevRo77auQ43CkA+cGfNQAABlA9AAAMoHoAABhA9QAAMIDqAQBgANUDAMAAqgcAgAFUDwAAA6geAAAGUD0AAAygevAwsWXL4ZwBaiCXK8ztPuNMiQAXqB48KApUlC3GnYKECrLEcB4YrQDVg4eDFzc/A6pH9fI/iR294GTyWgCqBw+vLvqFmeLEmBLcQUgl+UVpzocK7+76uIOAhsFZCpvD2bNnORwOk8lksVg6Ojo6OjpsNltHR+fJZWRmy9YzYRlbsojLloMmURRkVZYVSrLfC4fPs8YdBjQKVI/aBQUFiUQiOp1OoVCoVCqNRqNSqQqFgsVi3bx58+3j0g8JFXI5KsisbMTCAKqoqGAwGAwGQznF2IpNoShauel4dILrBWoNqJ7m4OfnV2NQY2hoOH78+PHjx+MLpa2EQmFYWNiDBw+++eabkJAQ3HFAE8G2nuagr/+vrQ9MJrNHjx7QO03D5XJXrFhx5MiRhw8fjhw58t69e7gTgaaAUY96SaXSM2fOnD59OicnRznw6dix4+7du3FHI4PU1NQ9e/YUFhbOnTvX398fdxzwGaB61OXjx4+nT5++fPlyaGjo6NGjR4wYIRaLEUL29vbnz59vxAJAY8XHx+/evZvD4Xz99deurq6444BGgepRvejo6Nu3bz9//nz06NEjR44kJg4ZMiQjI8PKymrPnj02NjYNLQN8tidPnoSFhVlbW8+fP9/S0hJ3HNAAqB5VunLlysmTJ01NTSdOnNi+ffsa93bv3n3dunVdu3bFlK5FuHXr1o0bN0xMTObNm8fjwbGFmguqRwWqqqqOHTsWGxtrbm4+duxYBwcH3IlauosXL4aFhY0aNWr27Nm4s4DaQfV8kby8vKNHj16+fHnChAkTJ07U0dHBnQj848iRI1FRUcOHD1eu9gLNAdXTRFlZWQcOHMjIyOjdu/eoUaNwxwG1k0qlW7duffz48aJFi7p164Y7DvgHVM9ny8zMPHDgQGxs7MyZMwcMGIA7DmhYRkbG9u3bDQ0Nx40bZ2dnhzsOQFA9n6e4uPj48eO3bt2aOXNm//79cccBnyc2NvaHH37o0KHD0qVLcWcBcDRzo+3Zs2fkyJHu7u6//fYb9I428vHxuXjxoo2NTUBAwPXr13HHaemgehp24cKFjh07crncW7duBQcH444DvkhoaOjDhw8TExMnTpyYlpaGO07LBStc9YmLiztx4oSRkdHixYuZTCbuOECV4uPj16xZ07lz5wULFuDO0hJB9dRp/fr1Hz58WLdunZWVFe4sQF2OHz/+66+/btiwoWPHjriztCxQPbWIiIhYvXr1ihUrhgwZgjsLULvS0tKwsDCFQrF69WrcWVoQqJ6aNmzYIBQK161bR6fTcWcBzefq1avbt2/funVru3btcGdpEaB6/vH8+fOvv/563759cPqFlkkgECxZssTX13fGjBm4s5AfVM/f9u/f/+LFi/3791OpsNevRbt+/Xp4ePjBgwcNDAxwZyEz+JghuVw+depUGo128OBB6B0wYMCATZs2jRw58tatW7izkFlLH/Xk5eWtWrVq9uzZ3t7euLMAzfLtt986OjpOnz4ddxByatHV8+bNmyVLlvz++++4gwANdfXq1fv372/btg13EBJquesX9+7d27RpE/QOqMfgwYMHDhw4duxY3EFIqIWOem7fvn3jxo3t27fjDgK0wNu3bw8cOLBr1y7cQUilJY56nj17FhUVBb0DGsnd3X3+/PlwghTVanGjnujo6OPHj+/duxd3EKBlEhISNmzYcOLECdxBSKJlVU9GRsacOXOuXr2KOwjQSjExMVFRUStWrMAdhAxa1grX0KFDL1++jDsF0Fb+/v4sFuvkyZO4g5BBC6qeefPmXblyBQ4aBF9i0aJFly5d+vDhA+4gWq+lfA737Nnj4+MDp78AX27Hjh07d+7EnULrtYjq+fjx4507dyZPnow7CCADW1tbfX39a9eu4Q6i3VrEZubJkycvXLiwbdu2uIMAkigtLZ02bdr58+dxB9Fi5B/1XLp0ydHREXoHqBCfz3dxcYFD4b8EyatHKpVGRUWtXLkSdxBANoMHD4ajNL4Eyavn9OnTLi4uuFMAEmrXrh2TyczPz8cdRFuRv3pGjx6NOwUgJx6P9+LFC9wptBWZq+fOnTtt2rQxMzPDHQSQU+fOnd+/f487hbYic/XAkAeolbm5eWxsLO4U2oq01ZOcnGxqaurr64s7CCAtOzs7DoeDO4W2Im31/Pnnn7a2trhTADLT1dV9+vQp7hTairTV8+DBg86dO+NOAciMwWB4e3tXVlbiDqKVyHmVu+Li4qysrDZt2uAOAshp1KhRLBaLQqEkJiZOnDiRyWQihLhc7r59+3BH0xrkrB4Y8gC1qr5jKyUlBSFEpVKXLVuGNZSWIecK119//dWlSxfcKQBp+fn51Zhia2s7YsQITHG0EjmrJz4+/r9vDgBUZdy4cXw+X3mTyWSOGjUKayLtQ8LqycvLUygU+vr6uIMA0urSpUvr1q2VN62trUeOHIk1kfYhYfUkJibC97aAuo0fP54Y+LBYrDFjxuCOo31IWD1JSUnOzs64UwCS69Kli6Ojo0KhsLS0HDJkCO442oeEe7jevXsXEhKCO0WLI62Si4Ry3Cma1cihEzI+FI4eOaW8WIo7S7NisqkszpeOWkhYPZWVlW5ubrhTtCDxj0pf3i8VCWR0BgV3lmZmNbLT1rIEdDEhA3eSZkVnUmUSuUdnvl8PgyYvhIQnSPX393/27BmF0tI+Bng8vllYVijz7Gyga8jAnQU0n/JiSUpsaZVIFjymiWeGINu2nvz8fGNjY+id5vHgaoFYqOg0yBR6p6XRNWD49DDm6jMiT+Q2bQlkq568vDxTU1PcKVqEvE/i8mKpf29j3EEANh6BhjQ65WOCsAnzQvWAJirIqqJQYXTZ0tGZtPyMpnyBFqoHNFFFmdTYio07BcDMyJIlqmjKnk2y7eESiUQODg64U7QIlSI5jUG2P13gc0kliooyWRNmJNtbJzMzE66qDoDmI9unVCgUcrlc3CkAAA0gW/UIBAIej4c7BQCgAWSrHhj1AKAVyFY9MOoBQCuQrXosLCz09PRwpwAANIBs1ZOQkAB7uADQfGT7lEqlUjqdbAcrAUA+UD0AAAygegAAGED1AAAwIFv1ODg4QPVorJyc7FlzJvbu2/HCxVPElMtXzk2dHooQGji4e1BPf+V/If07T50eeuXqebm84a8mymSyteuWh/Tv/L/VS1JTU4J6+r9+Hfflab98UatWLw7q6X/6THiN6cXFRcG9A4J6+kulLevMqtWR7VOanJyMOwKo0+9/XP34MXXLpr02Nq2IKS9ePPXzDSB+7tqlx5AhXxE/VwiFz2Ie7wrbVFZWOmH8tPoX++p17N17txYuWOHv30EsEn1JwrS09ytWzj9z6jpCyNjEdMH85ZaW1l+yQDabHRl1Y3ToxOoT79yJoNFoMllTvnVJGqSqHrlcDucn1GTl5WVmZhZeXr7ETZlMFvcypn//ocRNYxNTH29/5YMDA7tViCpOnzk6ZvSk+keyZWWlCKFuXXvy+fqpqSlfkjApKUH5s56u3uBBX3pBUY82XjHPnyQlv3N2clVOjLp108XFXSVDM+1FqupRKBRQPRpr7vyp8fEvEUJBPf2nT/tmzOhJ7969EYvFXm1965rF3c0zKupmaWmJkZExQigp+d3hw3sSkxKkUomvT/s5sxebm1sc+eXnEyd/QQgNGRbczr/D1zMXVF/C7TsR58+f+JiexuHo9AjqM23qHDb773MMRURcP302PDs709zcMnTUhJC+g46GHwg/dohIOGf2Il+f9lOnh4btPOzp6Y0QunHzyrnzJ7KyMjgcnYD2nWZ9vdDQ0AghNHR4r/Fjp+bm5dz5M0IkqvD09FmyaBURGCFkaGTcurVTROR1ZfWkp39ITEqYPOlrZfXIZLJjxw/dvv1HfkGenh4/sFO3mTPmczgchNDadcsRQu3bdzp1+mhhYb6Ndav58751d/esf66CgvxtOzbExj7j8XRHDB8jFAru/3Un/NcLxMbQEyeP3PkzMjc328TEbOSIsUS9pqW9nzJt1Ib12w8e3q2nxw/beVhtb4S/kWpbD1SPJtu4YVe/kMG2tnZXLt0aNjQUIfT8xVN3d0/i01KrTxkfWSyWvr4BQig3N2fR4pkUKnXHtgPbtu4vKy9dvHRWVVXV2DFTli1djRA6dvTi6v/9VH32Bw/u/rBhpZ9fwKGDp5ct/f7+X7e37dhA3HXv/u3NW9f17TMwbNeRAf2Hbt6y7u69W6GjJg4bFmpqanbl0q2BA4ZXX1Rk5I2t237o3av/L4fPrluzJSn53Yrv5hOXVKDT6afPhtvZOZw+ee2Xw+eSk98dP/HP51Ymk3Xv1uvOnQjlZp2oWzcdHBxtbe2Uj7lw8dSp00enTJl95NCZZUu/f/jo3uFf9hJ30ej01/FxCQnxB/efvHQhis/X37RlbYNzbd3+Q3Lyu/Xrtm3auPvlqxd3/oxUHme7/8Cus+eOjx09+cjhsyNHjN2zd+uNm1cQQgwGAyEUfuzgqK/GL5y/4st+1Y0C1QOaCY/HYzKZVCqVz9cnhh4xz6OVG3oI0v/4qgkhAAAgAElEQVRXVl4WFXXz+vVLffsMpNFoCKHfrl2gUCirVm5wcHB0dXH/bvn67OzMe/dvs9lsDkcHIaSnx6/x9b1TZ456eflOn/aNtZVNh4DA6dPm3rr1e15eLkLo/IWTnQO7h46a4OLsNnLE2NBREwoL8tlsNovJolAofL4+i8WqvqjzF04GBnYbO2ayjU0rb2+/ud8sTUp+RwziEEKtbO1D+g6i0+mmpmbt23VKTHxbfd6ePfuWlBQ/e/aYeIvevv1Hj6A+1R8Q3DPkwL4TPYJ6W1vbtvPvENS9d0xMtPJesVg0e9YiDofDZrODe4akp38Qi8X1zFVUVPj06aNxY6e28+/QurXTqu82lJWWEIsSCARXfzs/6qvxffoMsLayGTxoRJ/eA06dPooQQhQKQsjb2z+k7yB7+9ZI/Ui1wkVchRZ3BNAoFRUVCQnxM6fPU065dOnMpUtnlDfZbHb/fkNmfb2QuJmQEO/q0kaXp0vcNDMzt7CwSklJ7BVc++Ue5XJ5UlLCpIkzlVO8vfwQQqmpyaamZjXumjljXq0LIUil0vepyUFBvZVTXFzcEUIp75OIdTEHByflXbq6emXlZdVntzC3bNOmbWTUjY4du7x+HZedkxUU1Lv6RiU+Xz8y6sbW7T8UFORJpVKRqIIoU4KVpY1yJVFXV4/YZMZms+uaKzPzk0Kh8GjjRczC5XL9/AI+pqchhN6/T5JKpf5+HZQL9/Lyu3HzSkVFBXGTWJVrHqSqHoVCUVVVhTsFaJRXr16wWCziM0zo2aPPiBFjiZ+3bF1noG84b+4y5b1CoSA5JbF3347KKRKJpLCooK7li8VimUx2NPzAseOHqk8vLCoQi8USiYTNrnNFrwaRWKRQKHR0/jkZiw5HByEkEv39ia3xB++/A++ePfruP7BTIBDcuv27m5uHpYVV9erZvWdL1K2bC+evaOPhxWKyTp8Jv/NnhPJe5n/+mhIrenXNVVpaghDi6PxTXnp6fOKHigohQmjh4pnKlQNiUUXFhcRNLrf5zvpAquoBWuTZ82gvL7/qu674+gau/99Ec+csXbh4ZtSt35WDGi6X5+npvXjhyuoLqT46qIHNZtPp9GFDQ/v3+9cF0fUNDNlsNpvNJj6HjcFhc6hUavXHCyuEn/VBDerea+/P2/56cOfe/dsTx0+vfpdMJrv5+9Xx46b16tXv74ULBQ0usJ65iKqqFIuVDy7//1EYEXjldz842DtWX5qpiVlefhMvp9VkpNrWQ6GQ8GKqZPX8+RNfn/Z13evt7dezR5+9P28rLSslpri5eWRmfrK0tLa1tSP+o1Aoyh1J/0WlUp2cXHNzs5WPt7CwotHperp6CCFHR5dXr14oH7x779bde7fWtSg6ne7Y2vl1/D/7wt++eaVc7WoMfX0DP7+A02fCy8vLunULrn6XXC6XyWTKgYlQKHz0+H6Db+N65rKyskEIvUt8o7zr+fMnxM8ODk4MBqO4uEj5mujp8fl8fSaT2ch/iApB9QAMCgsLPn5M8/cLqOcxX89cIJFUHTiwi7g5cMBwkahi0+Y1ySmJGRnpx44fnjz1q3fv3tSzhNBRE+7/defU6aOfPn1MTkn8ceP/5s2fKhQKEUIjho95FhP969H97xLfXrx05sqVc26uHgghHk+3sLDg1avYnJzs6osaOXJcdPSDc+dP5ORkx8bF7N671cvL17XR1YMQCu7R99Onjz7e/jXqksFgODm6RERez8zKeP8++btVCwICAsvLy9LTP9RzrHM9c5mZmjs7uZ48+cubN6/S0z9s3LTawNCImIvH4w0YMOxo+IE7f0ZmZWfGxsUsWTb7p81rGv+vUCFY4QIYxMREGxkZt2plX89jjI1NJoyfvv/Arl69+vl4+5ubW2zfduDgwbB586fSaDQ7u9Y/rN9e/2bRrl16fLdi/ekzR389up/L5Xl4eO3YdoA4f263rj0XzF9+7vyJ02fCzcws5s1dFtyzL7FRJiLy+uKls8aMntSt6z/Dk+CefSsrxefOnzh0eA+Xy+sc2H3mzPmf9U8ODOzOZrN79Ojz37uWLlm9Zeu6KVO/Mje3nDJ5lpurx5v4l7PmTDh86ExtS2p4rlUrN2zZtn7h4pnGRiZjx04xMjRWdvTsrxfq8nQPHgorLCwwNDTq1LHr1ClzPusfoiqkGiZIpdLAwMAnT57gDtIiPPytgMagt+mkjzsIqEksFkukEuXewEWLv9bT46/5fpM6nuv9q/LcDxV9xpt97oykGvVQqVQLCwvcKQDA7LuVC4qKCxcvXGlgYPg4+q/YuJiNG3biDlUTqapHoVBkZWXhTgEAZqtWbvh53/b/fb+kslJsaWm9fNmaDh064w5VE6mqBwCAEDI0NFq1cgPuFA0g1R4uAIC2gOoBAGAA1QMAwIBU1UOhUNzc3HCnAAA0jFTVgxB6+/ZtIx4FAMCMVNUDX6QAQFuQrXpwRwAANAqpqocAAx8ANB/ZqgfWuQDQCmSrnrZt20L1AKD5yFY9b9++beFXVms2LA6NziTb+wd8LhqdwtWjNWFGsr11qFRqYy6VC74cT5+W/+mLLvUJSKAgU6yjC9UD1dOMTGxYCjms27Z00iq5mW1TLgMD1QOayMicZWrNfHi1uU8nDjRHTFQBk0mxcqzz5Pz1gOoBTeff29DSnnX3XFZ+plgugxFQC1KYJX5yI5fNoXQbYdK0JZDtfD2tW7eG6mlObbvo6+jRY2/lF+ZIkBrKR65QyGQyBl0L3qgKhORyGY3alA0f2oXJpnF4VM9AvnsHvSYvRAt+o58lIyNDIpHgTtGyOHrxHL14CKFKkSpLv7Cw0MjI6Mcff+wzoI+fn58Kl6w+06dPX7ZsmZOTUyMeq8WYbOqXf3GAbNVDp9PruYQIUCsWRzXr72lpaStWrFi2bJmltcna9atUsszmMXX6BApNqqrXgdzI9hrRaDQ4rkd7PXr0CCGUnp6+fv16X19f3HE+W7du3dq0aYM7hXYgW/XAqEdLKRSKKVOmpKWlER9gLV1nyc/P/+2333Cn0A5kqx4ulwvVo12OHDly9epVhUKxffv2sWPH4o7zRUxMTNatW4c7hXYgW/XI5XLYzKwVKisrEUJ37typrKzs378/lUrV1yfD1QTDw8OJayuD+pFtMzOTyayqqsKdAtRHKpXu2LEjMTHx8OHDPXr06NGjB+5EqgTbehqJbKMeBoMBox6N9enTJ7lcnpqaamNjc/jwYdxx1OL58+fXrl3DnUILQPWAZhIeHj537lwKheLs7BwaGoo7jrqw2ezz58/jTqEFoHqAesXHx1+/fh0h5OXldeXKFdKfxNbd3X3u3Lm4U2gBslWPjY0N7gjgH/Hx8Vu2bPHw8EAIeXt7447THCgUSrt27XCn0AJkq57S0tKysjLcKVq6hw8frlq1CiFkbW0dHh5uZ2eHO1GzOnToUExMDO4Umo5s1cPhcEQiOH8VNgKBoKSk5OzZsxMnTkQIkWN/+eei0WhPnjzBnULTQfUA1UhMTBw+fHhZWRmPxwsLC9PSw5FVYtiwYb169cKdQtNB9YAvRfyF//jx47Zt2ywtLenacIILtdLX13d2dsadQtORrXqMjIxYrKacrhE0gUQi6dOnD/HFq969e7e0bTr1GD9+PBzaWj+yVQ9CKCsrC3cE8jt16lRmZqZMJjt58iSJD9JpMuLISdwpNBrZxsY8Hk8gEOBOQXJr1qzR1dW1sLCgUqlsNht3HE20a9cuDoeDO4VGI1v1cLlc+PKemhw5ckQqlc6cOfO7775jMpm442g0Y2Nj3BE0HdlWuKB6VI64muuDBw8qKyunT59OfEcXdyhN9/Dhwx07duBOodHIVj26urpmZma4U5DHoUOHhg8fjhDq3Lnz7NmzqVSyvWHUhM/nx8XF4U6h0cj2TuLz+c+fP8edQutJJJK8vDziO3GXLl3CHUf7uLm5rV27FncKjUa26jEwMCguLsadQrvdvn27S5cuxABn0qRJuONoJRqNBoca1I9s1UOn09lsNuzkagKJRPLw4UOEEIvFio6Ohg2lX2j16tVwnEc9yFY9xFcWS0pKcKfQMunp6f369SP2lHfu3Bl3HDIoKCj49OkT7hSai4TVw2KxCgsLcafQDhKJ5JdffiFWEKKiorTlSntaYdGiRfb29rhTaC4SVo+xsXFBQQHuFJqO2GU+dOhQLpeLELKyssKdiGwcHR1NTU1xp9BcZDukEKqnMX7++WcvL6/AwEDi/IFAHZ48eVJeXh4cHIw7iIYi4ajHxMQkPz8fdwrNFR4ezmKxAgMDcQchuby8vAcPHuBOoblIOOoxNzd//Pgx7hQa59ixY/fu3Tty5AhxEi+gbj4+Pnw+H3cKzUXO6oGdmtWVlJTo6+tXVFQcPHgQd5YWxNra2traGncKzUXCFS4LC4vs7GzcKTRCdHR09+7diStBf/311zQaDXeiFiQvL+/IkSO4U2guElaPubk5HA4XHx+PEMrPz7927Rq8GlhUVlbCtQDrQcLqIa5L0WKP5iopKenfv39ubi5CaODAgbq6urgTtVBmZmYrVqzAnUJzkbN6bG1t09PTcadobrdv3y4vLxcIBEeOHOnZsyfuOC0dk8kMCAjAnUJzkbN6rK2tMzIycKdoVj///HNERISOjo61tbW5uTnuOABJpdJp06bhTqG5SLiHCyFkZ2f38eNH3Cmaw4sXL4qKioKDg4cOHWphYYE7DvgHnU6HU/bUg5yjHjs7u5ZwUu5Hjx7t27ePuKAw9I4G2rdvH/GFFfBf5Kwee3t7Eu9fz8jI2LJlC/EtoUOHDsEOLI3Vrl07CoWCO4WGImf1mJiYkPLi68R5iH766aeuXbsihODbiRpuxowZEokEdwoNRc7qIUYEKSkpuFOoTFVV1Y8//njjxg2E0J49e2DXiVZ48+aNTCbDnUJDkbZ6AgICMjMzcadQmVOnTrm4uIwaNQp3EPAZtm/fzmAwcKfQUOTcw0Wsc8XGxg4cOBB3kC8SHh7+5s2bzZs3wzmStYifn59CoaBSqXK5nPg/nU6fOXPm1KlTcUfTIKQd9bi5uYnFYtwpGkUkEg0ZMoTYfKOUlJSEEBIKhZs3b8YXDTSFg4MDcVJ95f9tbW3h8tA1kLZ6nJ2dIyMjcadolFWrVmVkZCivXJiTkxMSElJeXo4Qmj17Nu504LONHTuWxWIpb9Lp9IEDBxJngwRKpK0ehFCfPn2IsYMmO3ny5LNnzxBCFAqlQ4cOCKHCwsLw8HA4TbL2GjJkSPXTZVhbWw8ZMgRrIk1E5urhcDhv3rzBnaI+qampJ06cqKioIG5KpdKBAwe2adMG9ppruzFjxhCXh6bRaAMGDIBzhv0XmavH3d397du3uFPUZ9WqVTXO5Upc8xNou8GDBxOXo7CxsSGuHA1qIHP1uLm5JSQk4E5Rp59++qnGkUcKhUIqlQ4YMABfKKAyo0aNYrPZAwYMgPOW1IpS/3dM8jMrY++U5KaLRQKtPDJKKpPSaRp6AEGVpAohhNA/B9orj7ln0Os7GIRKQ2wdmpkd27+ngYEZU80xv1Ruujj2z5LC7CphqRR3luYmkUrpdHpL+yaFvglTR4/Wtgvf1kWnnofVVz0f3gofXSts281Q34TJ4WnoB7gFolCQsExaWlAVe7sw6CtTaycO7kR1ev9a8Cyi2CNQ38CcxdaBt1CLUFUpL8wSp8SWtW7L9QyscyNXndXz7lnZ26flvcbBleE0WsTRDJ8g/dZtebiD1OLNo9Lkl8KeYyxxBwF4PLica2TBaN/HsNZ7a9/WI66QvX0CvaMF+kyyjv2zRCbTuDMzCEulyXHQOy1a56Fm+RmVBZmVtd5be/Vkp4ppLW4VVVtRKJTsVBHuFDVlp4npTHgLtXQsLj3zfe1vztqrp6xQYtaqvk1EQHNYtNYpztW4MzOUF8NbCCAzW46gpPbdC7Vv+asUy6VVag4FVEQillexGvG45iUWyjVuJRA0O5lMISitfec4mY/rAQBoLKgeAAAGUD0AAAygegAAGED1AAAwgOoBAGAA1QMAwACqBwCAAVQPAAADqB4AAAZQPQAADKB6AAAYqOzEcatWL3748J7yJo1GMzOz6NSx68QJM3i8hk9kder00XPnT8hlst+u/jl4aM/hw0ZPGD/tCyOlpqZMnR4atvOwp6f3Fy7q6m8Xdu76qWePPqtWbvjCRYG61HgLKR0/drmqsvILf5XV31QDB3cXCAT/fczcb5YOG9ocl5a+cPHU3p+3K29yOBwb61aDBo3oFzKYQvmiM43sCtsU9/L5r0fOqSKmeqnynJVWltaLF68ifpZIJElJCWfOhqelpWzZvLf+F1Qikfzy676+fQYOHfKlv/i0tPcrVs4/c+o6QsjYxHTB/OWWltaNmK8Bf0Rcc3BwfPDwrkAgaEyTNtmQYcH7fj5mYd5Cz7BlZWm9YMGKGhONjUyqJFWq+lUSunQOGjRoRI2JtjZ2qlp+Y/z4ww42h4MQEgoF0dEPtm77QSgUfDVyXHNmwEiV1cPmcHy8/ZU327fraGhotGXr+vj4l/X/saqoEMpkMn//Dq1bO31hhqSkfy5BoaerN/g/b68mSE//8O7dm107Dn27Yu69+7f691PX5dxyc3NKS0vUtHCtwOZw/P0CapnOZqvkV6lkYmpW6xM1Jw9Pb13e3xer6BzYvby87PyFk1A9quHu5okQysvPJW4mJb87fHhPYlKCVCrx9Wk/Z/Zic3OLmOdPli6bgxBau275jwxG5B+Pqy+h1lmIuyIirp8+G56dnWlubhk6akJI30FHww+EHzuEEArq6T9n9iJfn/bVR+k3bl45d/5EVlYGh6MT0L7TrK8XGhoaEc+LEGrfvtOp00cLC/NtrFvNn/etu7unMsPvf/xma2vXtq1Ply49IqNuVK+e16/jwnZv/pieZmlpPevrhSdOHmnt4LRg/nKEUElJ8c/7d7x8+by0tMTBwWn6tG+IXr7624Vfj+7fuGFn2J4tnz590NPljxs3tV/I4Ni4mEWLv0YIjRk7KDCw2w/rtqn1V6Ndqq871/P7kslkx44fun37j/yCPD09fmCnbjNnzOdwPu+0+bm5OfsP7Ix7+byiQmhubjli+JiBA4bVM72uJ/3l132XLp+5cC6CzWYTS7548fTBw7svnI+o9XldXdvc/+uOWCxms9lDhgWPGzvlWUx0bOyzSxeieDxeXe/egoL8LdvWx8XFcLm8QQP/dcGvkP6dJ02cOeqr8cTNLVvXp6QkHth/gljPOBp+IDLqhkBQ7ujoMnP6PA8PL+IilCdOHrnzZ2RubraJidnIEWOJxk9Lez9l2qgN67cfPLxbV1dv964jTf1N/kO9m5kzMtMRQmam5sRvbtHimRQqdce2A9u27i8rL128dFZVVZW3l9+xoxcRQsuWrj5/9vfqs9c1C0Lo3v3bm7eu69tnYNiuIwP6D928Zd3de7dCR00cNizU1NTsyqVbAwf869cQGXlj67Yfevfq/8vhs+vWbElKfrfiu/nEKfFpdPrr+LiEhPiD+09euhDF5+tv2rJWOaNMJou6dbNP7wEIoT69B7x6FZuVnUncVVlZuWr1Yh0ud++eowvmLT98eE92diaxaimXy79dPvfNm1ffLltzYN8JVxf35SvmpaamEFfgFgoFx04cXvv95mtX7/bu3X/Hzo35+XmeHt6r/7cRIXRg/4kV365T6+9Fq9Xz+7pw8dSp00enTJl95NCZZUu/f/jo3uFf9ta6EIVcXvlvxPsKIbR5y9qCwvwfN+z85ci5YUNDd+766VlMdD3T63rSkJDBQqHw0eP7yie999ftzoHdlSOdGrKzM/X0+ERP0en0a9cvOdg77th2gM1m1/Pu3fjT6g8f3m/8cdeObQdKS0vu/3WnMa/hvv07bty8MnvWop07DllZ2Sxb/g3xrt5/YNfZc8fHjp585PDZkSPG7tm79cbNKwghBoOBEAo/dnDUV+MXzq+5Rtw0Kh71SKVS5Q+JiW/37dthb9+6TZu2CKHfrl2gUCirVm4gXvrvlq8fPXbgvfu3ewWH6OnxEUIcjg6fr199afXMcv7Cyc6B3UNHTUAIuTi7FRUVFhbks9lsFpNFoVBqLAchdP7CycDAbmPHTEYI2di0mvvN0qXL5ijXBMVi0exZi4jfenDPkI2bvif++CCEnsVEFxUV9gruhxDy9WlnZmYeFXVz4oTpCKHH0X+VlZUunL/Czs4BITRv7rJ5C/7eNB7z/ElS8rvt2/YTI51v5iyJef7k0uUzSxavIl6cMaGTTE3NEEIhfQeHHzv0/n1Shw6ddXS4CCFdXT0ul6va34u2UCgUystAEygUyn+HLXX9voJ7hrTz7+jg4IgQsra2Dere+8nTh7U+0eUr5y5f+de2WDab/fuNBwih1LSUoUNGubm2QQhZDRrh7ORqZmZRz/S6ntTC3NLPt33UrZs9gnojhAoLC+LjX276abfyGeUyGfF5EVYInz599EfENeXaFoVCYbPYM2fMI27W9e41N7d8Efts/rxvfX3aEe/AmOdPGnyRhULhjZtXZs6YH9S9F0Jo8cKVooqKzMxPerr8q7+dHztmcp8+AxBC1lY2ycnvTp0+2r/fEOIScd7e/iF9BzX6l9kAVVbP+/fJvfp0UN6kUCjt23davHAlMRBISIh3dWmjrHwzM3MLC6uUlMRewSF1LbCeWZKSEiZNnKl8pPKXVCupVPo+NTkoqLdyiouLO0Io5X0SUT1WljbKUbGurh5CqLy8jJgSEXHN16edgYEh8S4J7hkSGXWDqJ709A88Lo/oHYSQp6e3svISEuIZDIa3lx9xk0qltvX0SUlJVAZwcHD619MJyhv9MpNZampK/4Fdq09RNkJ1df2++Hz9yKgbW7f/UFCQJ5VKRaIKDqf280MHde81YviY6lMo1L/XADp17Hr6zFGBoDwgILCtp4+bm0f90+t50n79hvy48X/FxUUGBob3/7pjbGzi59te+YxDhgUrf6bRaEOHjJowfrpyCvEHu/53b2VVJbGm9vc/gUJxdW1T/W1Wqw8f3ldVVbn9/1wMBmPtms0IoZcvX0ilUn+/fz7CXl5+N25eUf4xqL4V4supsnqsrW1XfvcD8fOVK+eePH343Yr1erp6xBShUJCckti7b0fl4yUSSWFRQT0LrGsWsVgskUjY7Mauw4vEIoVCQYwpCDocHYSQSPT3a8pk1Ty5MTGaLReUP3p8v6qqqnqlEpt4PD29y8pKdf49PCGGb8SGc4lE0iekk/IumUxGrJwTWDWesd5rwLYclpbWy5etqT6FSq1lm0Bdv6/de7ZE3bq5cP6KNh5eLCbr9JnwO3/WvmHFwNCorg/SwgUrHOwdo27dPH/hJJfLHTRwxJTJs+h0el3T63nSLp2DeDzdO3cihg8fff/+7d69+lf/52zftp8oKTqNbm5uWWPPKZf798163r3EG5jFZNW4q37l5WUIIRaLXWN6RYUQIbRw8Uzl/mjiVS0qLqwRSSVUWT0sFsvVxZ34edashY+j/zp4MGzJ/+9u53J5np7eixeurD5LXX+U6p+FzWaz2WzilWoMDptDpVKrP15YIWzMS3nnTgSVSv15z1FK9XfM9g2RUTc8Pb1ZLJZYLK7++LKyUmVyJpN56MCp6vfW+ikC1XE4nCYfuSOTyW7+fnX8uGm9evUjpgiFtRy80yA6nT58+Ojhw0cXFRVGRt048svP+voGX40cV+v04cNG1/OkDAYjuGfIn/eievTo8+p17OJF/3onOzq61LXdp7p63r3EX9/qzyioNnyucURLVdXf18Pi6xsoi6Y64uOw8rsfHOwdq083NTFT7ilSIXV9GPh6/GlT59y4eeXVq1hiipubR2bmJ0tLa1tbO+I/CoViZGRcz0LqmcXR0eXVqxfKR+7eu3X33q11LYdOpzu2dn4dH6ec8vbNK+XAtR5/RFzr2KGLm5uHq4u78r+goN5370ZVVlZaWdmUlZVmZmUQD379Ok65a9zVtU1VVZVMJlMmZzJZxsamjXnp6rkUNaiHXC6XyWTKgSexifdzX0yBQBB163di5drQ0Ch01AR3d8/U1JS6pjf4pP37DXnz5tWFi6fc3T2trW2b8O+q591rY92KWPMipkul0riXz5UP09HhVm+i96nJxA821q3YbPbL///4yOXy+QunR0Rcd3BwYjAYxcVFyjetnh6fz9dnMplNiN0gNf4d7t9viIuz27YdGyQSCUJo4IDhIlHFps1rklMSMzLSjx0/PHnqV+/evalnCfXMMmL4mGcx0b8e3f8u8e3FS2euXDnn5uqBEOLxdAsLC169is3Jya6+qJEjx0VHPzh3/kROTnZsXMzuvVu9vHxd660e4nCebt2Ca0zv3r2XQCh49Ph+h4DOLBZrz96t6ekfXr+O23dgp7JJ/XzbOzm6/Ljxf3Fxz7Nzsm7d/mPGzDFXfztf/ytGrJxGRz/48CG1oVcX1MRgMJwcXSIir2dmZbx/n/zdqgUBAYHl5WXp6R+Uez+U8nJznjx9VOO/d4lvKRRK2O5NW7f9kJySmJWdeev2H0lJCd7efnVNb/BJ7e1bu7l5nD13vG+fgU3+p9X17jU3t3B39zx1+tdnMdHJKYlbt/1A7IoiODu7PXh4t7S0RCKRnDz1q3JIzuPxQvoOOnnql8jIG4lJCdt3/JiUlODh6c3j8QYMGHY0/MCdPyOzsjNj42KWLJv90+Y1dcb6Mmo8rodKpc6b9+2cbyadPPXrpIkzzM0ttm87cPBg2Lz5U2k0mp1d6x/Wb69/w1U9s3Tr2nPB/OXnzp84fSbczMxi3txlwT37IoR69ugbEXl98dJZY0ZP6tb1n9YI7tm3slJ87vyJQ4f3cLm8zoHdZ86cX3/+PyKusdnsgPaBNaZbmFu6OLtFRt0I6t7r+//9tHff9mkzRjvYO34zZ8mWbeuZTBax1XDTT7v3Hdj5/RBGYGYAAAyHSURBVNplYrHI3Nxy/PhpI0eMrf8ZnZ3d2rfvtG//Dk8P7+3b9jfiNQb/snTJ6i1b102Z+pW5ueWUybPcXD3exL+cNWfC4UNnajzywcO7Dx7erTHR16fdtq37Nv205/DhPYsWz6yqqjI3t5w86WuiNeqaXs+TEkeld+3SIy0tpfq78XPV8+5dtXLD1q3rV65aSBzX0yu4n3L/+uxZizZvWRs6ZoCurl6/kCF9eg949uzvg+ZmzphPoVL3H9wlElXY2ztu3LDLytIaITT764W6PN2Dh8IKCwsMDY06dew6dcqcJseuH6XWEenTiKIqMfLqXvt12oFSaVkpm8UmthlXVVUNHtpjxvR5Q4d81ZwZXtwq5PGpfsEGzfmkDXp8vVCBqJ5dNCtV81MoFHPmTnZ2ciUONG1p3r8qz/1Q0We82X/vUu/RzOQmEAjGjR/s69N+wvjpFArl7PnjVCq1a5ceuHMBjSAWi7OyMi5dPpOenrb2+82442gcqJ6m4/F4m37ac+jQ7nkLplIp1NaOzls27a1/wzloOT58TJ09Z2KrVvYb1u8wMWnUHoYWBarni7i7eezYfgB3CqCJXF3c79x6hjuF5oIjTQAAGED1AAAwgOoBAGAA1QMAwACqBwCAAVQPAAADqB4AAAZQPQAADKB6AAAY1H40M51BlcNZY7QEg0WlMb7ounHqQGdS0JddzQ6QAI1BYevUPr6pfSqXTyvKrlRzKqAahVliXQON+0IMV48ObyFQnFPJ4dFqvav26jEyZyrkMOrRDnKFwshCLeeR+xKGFgw5vIVavCqRzNS65om0CbVXj7EVi6dPf3m/SM3BwJd6HlVgYsnSN9G46jFvxWGyqW8eFeMOArBJfV0uEkjt2tR+ZafaTxVGuHMun0qjeHUzpDNga7TGkVTJY28XMtmULkM09zQdUSdz2Vy6ZxcDGh3eQi2IXK5Iel6alVIxaKYFpY5NfvVVD0LoWWRR/KNSOoPK0dW4rQktFoWKhCVSChV5dNTzCdL00wA++b3w1YMSHV0GS6f2dX4Sk8lkNFqL+1dTKZTsDxVtA/ldhpnU87AGqocosNICSUWZTNUJQZMpuHy6niGDStOOXUgKuaIkX1JR3uLeQvPmzduyZUvNa66RHUuHamzZ8D+54bEMlUoxMGUawFnWQFNRqBQDM6ZBLefnJbm88nfm9kwdncZeq7JFgTVwAAAGUD0AqIu+vn5dG1kBVA8A6lJSUgLXkq0LVA8A6uLm5oY7guaC6gFAXRISEnBH0FxQPQCoi6OjI+4ImguqBwB1SUlJwR1Bc0H1AAAwgOoBQF0MDAxg53pdoHoAUJfi4mLYuV4XqB4A1MXV1RV3BM0F1QOAurx79w53BM0F1QMAwACqBwB1adu2LWxmrgtUDwDq8urVK9jMXBeoHgAABlA9AKiLs7Mz7giaC6oHAHVJSkrCHUFzQfUAADCA6gFAXaysrHBH0FxQPQCoS2ZmJu4ImguqBwCAAVQPAOqip6cHhxTWBaoHAHUpKyuDQwrrAtUDgLrAxXDqAdUDgLrAxXDqAdUDAMAAqgcAdYHrcNUDqgcAdYHrcNUDqgcAgAFUDwDqApcArAdUDwDqApcArAdUDwDq4uTkhDuC5oLqAUBdkpOTcUfQXFA9AKgLlUqFo5nrAtUDgLrI5XI4mrkuUD0AAAygegAAGED1AKAu9vb2uCNoLqgeANQlLS0NdwTNRYHNYAColq+vL0JIuW9LoVBQKJTBgwevXr0adzQNAqMeAFTMzs6O2K1OoFKplpaW06ZNw51Ls0D1AKBi/fr1q344j0Kh6Natm6WlJdZQGgeqBwAVGz16tLW1tfKmtbV1aGgo1kSaCKoHABXjcrn9+vVT3uzUqVP1JgIEqB4AVC80NNTGxoa4AOnYsWNxx9FEUD0AqJ6uru7gwYMpFEpgYCAMeWoFO9dBS1eUU5WVKirOlQhKZQghQYlUJYuVK+SfPn2ysrKi0+gqWaCuIUMuk3P5dAMTuoU9x8SapZLF4gLVA1ooYZk07l5p0vNyhYKia8ZDFMRg0hlsOtLYr5orkKRSKq2UyeUKYYFQJpW19uT5BPH1TZi4kzUFVA9ocSRV8r+uFKbECozs+TwjHZYOA3eippCIpeX5FUWfSm2cOF2HGXF4qhlbNRuoHtCyJMUJH10r5JnwjFvxcWdRjaKM8pLMUr+ehl5ddHFn+QxQPaAFeRpRnBhbYeNljjuI6mW9zbNoxQgaYYw7SGPBHi7QUrx8UJb6roqUvYMQsnQ3LchDD68X4Q7SWDDqAS1C9O9FH5Il5k5aMyhomoK0Yn0DefBoU9xBGgajHkB+718J3seLSd87CCFje4PCPMWrB6W4gzQMqgeQXEW5LOZWqZWHGe4gzcTM2TjxRUV+ViXuIA2A6gEkd/9yIVufhztFs2Ib8B5cKcSdogFQPYDMSvIlmSkivkXLqh5dYx1BiTzzvQh3kPpA9QAye3671MTBAHeKOl26tmXL7tHqWLKhnX7cPY3e4gPVA8gs+UUp14iDOwUGXAPOx7dCaZUcd5A6QfUA0sp8L+LwmTR6C32T88110t4Icaeok5Z97wOAxstOE+mZq3ErT+yryHsPT+Xmp7FYOj6evUOCZzGZbITQmp/69uw2uaQ0N/ZVZFVVhX0r75GDv9PTM0YIlZbln7+yISXtOZvN69humPqyIYR4Rtzc9EonHw39dkUL/YMAWoKCzCoKRV3v8Pi3906e/5+zY/vFc06MGvq/V2/uXPhtI3EXlUr/86/jZqb2KxdfWTL3dGZ24q17vxB3nb64Jicvder4HbMm/ywUlrx++6ea4iGEaAxqzkfN3cUO1QNIS1Aio7Noalr4nb+OOdj59us129jIxs25U//ec168/KOkNJe418zUrr3vQBqNrs83c3Hq+CkzASFUUpqXkhoT1GWCk4O/man90AFL2CyumuIhhOgsWkWZas49pA5QPYC0ZDIFg62W6pHL5RlZCc6O7ZVTHOx8EULZOSnETQszJ+VdOhy9ClEZQigv/wNCyNbanZhOoVBs/v9ndWCw6Zr8JSnY1gNIS1Ipl0vV8umTSMRyuSzyzqGoP49Un15WXkD8wGDUcgrByqoKhBCd/s9dLKaOOuIR5DJ5lUhz93BB9QDS4urRpVUydSyZwWDTaPTOHUYF+A2qPp3HNaxnLiaTgxASiwXKKSJxuTriEaSVMg5PXeubXw5WuABp8fRpErFaqodKpVpZuBaXZJua2BH/GRpYUal0HR29euYyMbJFCGXlJBM3ZTLp+7QX6ohHkFbKeHzNHVtA9QDSMrNlyaXq2s7avfO412//vHM/PC//Y2ZW4qkL3+89PEMsru84GkMDi1Y2nnfuhyemPMnMSjx/5Uc6XY3nZpWIJWatNPfU8VA9gLRsXXVKcwSNeGBTtG0TNHr42thXkdv2jDkYPk8mk8ya8jOb3cAeq7Ej15kY2/5yYvGhY/P19c19vUIUcnVtjhEUVLRyVeO2pC8EpwoDZBa+7qO5uymLq5XXbPgSMoks5WHGzE0OuIPUCUY9gMzadOALCsW4U2BQXiBy61jfhifsNHcrFABfzr+3/pNFKYZWuhRa7ZfXehh9/vfb+2u9SyqppNe2jxwhFDrsew+3rqoKmfYx7siJxbVnkFbRaQxEqSX8qKH/83TvXtcycxIL+65upaqE6gArXIDknkWWpCVVmbY2qvVekVggEpXVeleFqFyHU/sXoHhcQ+LrWiohkVSWC2o/s5dYLGAydajUWtZOuFwDFrP2L+UXfCgxNVN0HabRJ4SF6gHkd3lvFtfCkMnRykv9fS65TJ6bkDt6qaZf6B229QDyGzDdPOVxJu4UzST1SWa/KVpwImqoHkB+DCZ1xDyrDzFZuIOoXXpsds9QE76RFozvYIULtBQl+ZILYZkOAVZUGjn/4n6Iyeoz3tTCTnMPI6wOqge0IKUFkpOb0m3amuoaa+6xdk0gLBZ/fJEzbI6Vub3KNn6rG1QPaHFu/pqbn1ll7GDINdCaD2pdROWVBe+LeXzKwOnmdIY2jeagekBLlJ0munuhUKagsHhsXRMdNk/LDneuqpCU5VeISkQKmazbMKNWbmo85ZiaQPWAlis7TZQcJ0x9LWTqMCorZHQWjanDlMs09Bw3dDpNLKyUVckYLGqlUGLnznX25Vo7aeuaI1QPAKisUFJRLhOWSStF8iqxhlYPk0VlcqhcPRpHl6ZvrGXDtP+C6gEAYKBN26UAAKQB1QMAwACqBwCAAVQPAAADqB4AAAZQPQAADP4PfGyWUbxzt6sAAAAASUVORK5CYII=",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "yaml_data = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: adding-team\n",
- "team:\n",
- " name: EssayTeam\n",
- " agents:\n",
- " - name: EssayWriter\n",
- " kind: llm\n",
- " job: >\n",
- " You are an essay assistant tasked with writing excellent 300-words essays. Generate the best essay possible for the user's request. \n",
- " If the you are provided critique view, respond with a revised version of your previous attempts. A maximum of total 100 words\n",
- " - name: ReflectionAgent\n",
- " kind: reflection\n",
- " retry: 1\n",
- " to: \n",
- " - name: EssayWriter\n",
- " job: >\n",
- " You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.\n",
- " Provide detailed recommendations, including requests for length, depth, style, etc.\n",
- " - name: FinalEssayProducer\n",
- " kind: llm\n",
- " job: >\n",
- " Generate the final assay to be returned to the user\n",
- " router:\n",
- " name: router\n",
- " kind: linear\n",
- "\"\"\"\n",
- "\n",
- "input_prompt = \"\"\"\n",
- "Question: Write me an interesting blog about latest advancements in agentic AI by reasearching the internet\n",
- "\"\"\"\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "session = FloSession(llm).register_tool(\n",
- " name=\"TavilySearchResults\", \n",
- " tool=TavilySearchResults()\n",
- ")\n",
- "\n",
- "flo: Flo = Flo.build(session, yaml=yaml_data)\n",
- "flo.draw()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Delegator Agent (delegator)\n",
- "\n",
- "Using these agents you can do redirects, this can help redirecting calls as well as retrying previous steps"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "yaml_data = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: adding-team\n",
- "team:\n",
- " name: EssayTeam\n",
- " agents:\n",
- " - name: EssayWriter\n",
- " kind: llm\n",
- " job: >\n",
- " You are an essay assistant tasked with writing excellent 300 words essay. Generate the best essay possible for the user's request. \n",
- " If the you are provided critique view, respond with a revised version of your previous attempts. A maximum of total 100 words\n",
- " - name: DelegatorAgent\n",
- " kind: delegator\n",
- " retry: 1\n",
- " to: \n",
- " - name: EssayWriter\n",
- " job: >\n",
- " You are a teacher grading an essay submission. Score the essay between 1 to 10, with 10 being perfect\n",
- " If the score is greater than 7 sent it to FinalEssayProducer\n",
- " else if its less than or equal to 7 sent it to EssayWriter with suggestions to change\n",
- " - name: FinalEssayProducer\n",
- " kind: llm\n",
- " job: >\n",
- " Generate the final assay to be returned to the user\n",
- " router:\n",
- " name: router\n",
- " kind: linear\n",
- "\"\"\"\n",
- "\n",
- "input_prompt = \"\"\"\n",
- "Question: Write me an interesting blog about latest advancements in agentic AI by reasearching the internet\n",
- "\"\"\"\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "session = FloSession(llm)\n",
- "\n",
- "flo: Flo = Flo.build(session, yaml=yaml_data)\n",
- "flo.draw()\n",
- "# data = flo.invoke(input_prompt)\n",
- "# print((data['messages'][-1]).content)"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.10"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/app/streamlit_chat.py b/flo_ai/examples/app/streamlit_chat.py
deleted file mode 100644
index a8240070..00000000
--- a/flo_ai/examples/app/streamlit_chat.py
+++ /dev/null
@@ -1,249 +0,0 @@
-import os
-import boto3
-import yaml
-import json
-import time
-import streamlit as st
-from typing import Dict, Any
-from pydantic import Field, BaseModel
-from langchain_aws import ChatBedrock
-
-from flo_ai.tools import flotool
-from flo_ai import FloSession, Flo
-from flo_ai_tools import RedshiftConnector, RedshiftConfig
-
-redshift = RedshiftConnector(
- RedshiftConfig(
- username=os.getenv('REDSHIFT_USERNAME'),
- password=os.getenv('REDSHIFT_PASSWORD'),
- host=os.getenv('REDSHIFT_HOST'),
- port=os.getenv('REDSHIFT_PORT'),
- db_name=os.getenv('REDSHIFT_DB'),
- )
-)
-
-AWS_REGION = os.getenv('AWS_REGION')
-AWS_BEDROCK_MODEL_ID = os.getenv('AWS_BEDROCK_MODEL_ID')
-bedrock_client = boto3.client(service_name='bedrock-runtime', region_name=AWS_REGION)
-
-with open('./examples/data/schema.yaml') as f:
- schema = yaml.safe_load(f)
-
-columns = schema['columns']
-columns_with_desc = [col for col in columns if col['description'] is not None]
-schema['columns'] = columns_with_desc
-
-bedrock_chat = ChatBedrock(
- client=bedrock_client,
- provider='anthropic',
- model_id=AWS_BEDROCK_MODEL_ID,
- region_name=AWS_REGION,
- model_kwargs={'temperature': 0.2, 'max_tokens': 4000},
-)
-
-
-class RedshiftQueryToolInput(BaseModel):
- query: str = Field(
- ...,
- description='The query to be run on reshift db. All queries should use proper column projections to use only the minimum required columns',
- )
-
-
-@flotool(
- name='RedshiftQueryTool',
- description='This tool has the ability to run queries on Redshift DB',
- argument_contract=RedshiftQueryToolInput,
-)
-def redshift_execution_tool(query: str):
- results, column_names = redshift.execute_query(query=query)
- output = []
- for result in results:
- row = []
- for i, column in enumerate(column_names):
- row.append(f'{column}: {result[i]}')
- output.append('\n'.join(row))
- full_text = '\n ---- \n'.join(output)
- print(f'Here is the response fro the db: {full_text}')
- return f'Here is the response fro the db: {full_text}'
-
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: analytics-flo
-team:
- name: AnalyticsTeam
- agents:
- - name: AnalyticsDelegator
- kind: delegator
- role: analytics team manager
- to:
- - name: Analyst
- - name: AnalyticsPresenter
- job: >
- Your job is to understand the users question and delegate to the right agent
- If the question is very generic, ask the AnalyticsPresenter to ask the user about more specific details,
- to clarify the question.
- If the question can be answered from the database, ask the Analyst
-
- eg:
- "How did we perform this week compared to last week" - then the you should confirm the understanding of "performance" before going forward - "By performance, do you mean First Call Resolution, Average Handler Time or something else?"
-
- - name: Analyst
- kind: agentic
- role: expert in writing and executing Redshift Queries
- job: >
- Your job is to understand the human question, and answer the question.
- You can use the given tools to query data from the redshift
- tools:
- - name: RedshiftQueryTool
-
- - name: AnalyticsPresenter
- kind: llm
- role: expert product manager
- job: >
- If the assistant has given an answer, summarize it and return the answer as if you are talking to a product manager
- If you needs more information, ask for the same. Always produce a good answer, this output will be show on the UI
- router:
- name: router
- kind: linear
-"""
-
-session = FloSession(bedrock_chat).register_tool(
- name='RedshiftQueryTool', tool=redshift_execution_tool
-)
-
-flo: Flo = Flo.build(session, yaml=yaml_data)
-
-# Initialize session state for messages
-if 'messages' not in st.session_state:
- st.session_state.messages = []
-
-
-def parse_stream_response(response) -> Dict[str, Any]:
- """Parse the stream response and extract relevant content."""
- try:
- # Extract the message content based on the response structure
- if 'AnalyticsDelegator' in response:
- return {
- 'role': 'assistant',
- 'content': response['AnalyticsDelegator']['messages'][-1],
- 'type': 'query',
- }
- elif 'Analyst' in response:
- # Extract the text content from the HumanMessage
- message = response['Analyst']['messages'][-1].content[0]['text']
- return {'role': 'assistant', 'content': message, 'type': 'analysis'}
- elif 'AnalyticsPresenter' in response:
- # Extract the presenter's message
- message = response['AnalyticsPresenter']['messages'][-1].content
- return {'role': 'assistant', 'content': message, 'type': 'summary'}
- return None
- except json.JSONDecodeError:
- # If it's not JSON, it might be the raw data response
- if 'product:' in response:
- return {'role': 'assistant', 'content': response, 'type': 'data'}
- return None
-
-
-def process_analytics_query(query: str, placeholder):
- """Process the analytics query using flo.stream."""
- loading_messages = {
- 'query': '๐ Analyzing your question...',
- 'data': '๐ Fetching data...',
- 'analysis': '๐งฎ Processing analysis...',
- 'summary': '๐ Preparing summary...',
- }
- try:
- prompt = f"""
- {query}
-
- Below is the schema of the table:
- {json.dumps(schema)}
- """
- # Stream the responses
- for response in flo.stream(prompt):
- # Show loading message
- with placeholder:
- st.write(loading_messages.get('query', 'Processing...'))
- with st.spinner(''):
- time.sleep(1) # Add slight delay for visual feedback
- st.empty() # Clear the loading message
- if '__end__' not in response:
- parsed_response = parse_stream_response(response)
- if parsed_response:
- yield parsed_response
-
- # Show next stage loading message if not the last response
- next_type = None
- if parsed_response['type'] == 'query':
- next_type = 'data'
- elif parsed_response['type'] == 'data':
- next_type = 'analysis'
- elif parsed_response['type'] == 'analysis':
- next_type = 'summary'
-
- if next_type:
- with placeholder:
- st.write(loading_messages.get(next_type, 'Processing...'))
- with st.spinner(''):
- time.sleep(1) # Add slight delay for visual feedback
- st.empty() # Clear the loading message
- except Exception as e:
- yield {
- 'role': 'assistant',
- 'content': f'Error processing query: {str(e)}',
- 'type': 'error',
- }
-
-
-# Streamlit UI
-st.title('Analytics Chat Interface')
-
-# Sidebar with information
-with st.sidebar:
- st.markdown("""
- ### Analytics Assistant
- Ask questions about:
- - Product escalation rates
- - Customer complaints
- - Service metrics
-
- The assistant will provide:
- 1. Data analysis
- 2. Key insights
- 3. Recommendations
- """)
-
-# Display chat history
-for message in st.session_state.messages:
- with st.chat_message(message['role']):
- st.markdown(message['content'])
-
-# Create a placeholder for loading messages
-loading_placeholder = st.empty()
-
-# Chat input
-if prompt := st.chat_input('What would you like to analyze?'):
- # Add user message to chat
- st.session_state.messages.append({'role': 'user', 'content': prompt})
- with st.chat_message('user'):
- st.markdown(prompt)
-
- # Process the query and display streaming responses
- for response in process_analytics_query(prompt, loading_placeholder):
- st.session_state.messages.append(response)
- with st.chat_message('assistant'):
- if response['type'] == 'data':
- # Format raw data in a more readable way
- formatted_data = response['content'].replace(
- '\n', ' \n'
- ) # Add markdown line breaks
- st.markdown(f'```\n{formatted_data}\n```')
- else:
- st.markdown(response['content'])
-
-# Add clear chat button
-if st.sidebar.button('Clear Chat'):
- st.session_state.messages = []
- st.rerun()
diff --git a/flo_ai/examples/bedrock_example.ipynb b/flo_ai/examples/bedrock_example.ipynb
deleted file mode 100644
index d5d001b3..00000000
--- a/flo_ai/examples/bedrock_example.ipynb
+++ /dev/null
@@ -1,325 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [],
- "source": [
- "import os\n",
- "import boto3\n",
- "import yaml\n",
- "import json\n",
- "from pydantic import Field, BaseModel\n",
- "from langchain_aws import ChatBedrock\n",
- "from langchain_openai import ChatOpenAI"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from flo_ai.tools import flotool\n",
- "from flo_ai import FloSession, Flo\n",
- "from flo_ai_tools import RedshiftConnector, RedshiftConfig"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "redshift = RedshiftConnector(RedshiftConfig(\n",
- " username=os.getenv(\"REDSHIFT_USERNAME\"),\n",
- " password=os.getenv(\"REDSHIFT_PASSWORD\"),\n",
- " host=os.getenv(\"REDSHIFT_HOST\"),\n",
- " port=os.getenv(\"REDSHIFT_PORT\"),\n",
- " db_name=os.getenv(\"REDSHIFT_DB\"),\n",
- "))"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [],
- "source": [
- "AWS_REGION = os.getenv(\"AWS_REGION\")\n",
- "AWS_BEDROCK_MODEL_ID = os.getenv(\"AWS_BEDROCK_MODEL_ID\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [],
- "source": [
- "\n",
- "bedrock_client = boto3.client(\n",
- " service_name=\"bedrock-runtime\",\n",
- " region_name=AWS_REGION\n",
- ")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [],
- "source": [
- "with open('./data/schema.yaml') as f:\n",
- " schema = yaml.safe_load(f)\n",
- "\n",
- "columns = schema['columns']\n",
- "columns_with_desc = [col for col in columns if col['description'] is not None]\n",
- "schema['columns'] = columns_with_desc"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [],
- "source": [
- "bedrock_chat = ChatBedrock(\n",
- " client=bedrock_client,\n",
- " provider=\"anthropic\",\n",
- " model_id=AWS_BEDROCK_MODEL_ID,\n",
- " region_name=AWS_REGION,\n",
- " \n",
- " model_kwargs= {\n",
- " \"temperature\": 0.2,\n",
- " \"max_tokens\": 4000\n",
- " }\n",
- ")\n",
- "\n",
- "gpt = ChatOpenAI(temperature=0.2)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "class RedshiftQueryToolInput(BaseModel):\n",
- " query: str = Field(\n",
- " ..., \n",
- " description='The query to be run on reshift db. All queries should use proper column projections to use only the minimum required columns'\n",
- " )\n",
- "\n",
- "@flotool(\n",
- " name='RedshiftQueryTool', \n",
- " description='This tool has the ability to run queries on Redshift DB', \n",
- " argument_contract=RedshiftQueryToolInput\n",
- ")\n",
- "def redshift_execution_tool(query: str):\n",
- " results, column_names = redshift.execute_query(query=query)\n",
- " output = []\n",
- " for result in results:\n",
- " row = []\n",
- " for i, column in enumerate(column_names):\n",
- " row.append(f\"{column}: {result[i]}\")\n",
- " output.append(\"\\n\".join(row))\n",
- " full_text = \"\\n ---- \\n\".join(output)\n",
- " print(f\"Here is the response fro the db: {full_text}\")\n",
- " return f\"Here is the response fro the db: {full_text}\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "yaml_data = f\"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: analytics-flo\n",
- "team:\n",
- " name: AnalyticsTeam\n",
- " agents:\n",
- " - name: AnalyticsDelegator\n",
- " kind: delegator\n",
- " role: analytics team manager\n",
- " to:\n",
- " - name: Analyst\n",
- " - name: __end__\n",
- " job: >\n",
- " Your job is to understand the users question and delegate to the right agent\n",
- " If the question is very generic, ask the AnalyticsPresenter to ask the user about more specific details, \n",
- " to clarify the question.\n",
- " If the question can be answered from the database, ask the Analyst\n",
- "\n",
- " eg:\n",
- " \"How did we perform this week compared to last week\" - then the you should confirm the understanding of \"performance\" before going forward - \"By performance, do you mean First Call Resolution, Average Handler Time or something else?\"\n",
- "\n",
- " - name: Analyst\n",
- " kind: agentic\n",
- " role: expert in writing and executing Redshift Queries\n",
- " job: >\n",
- " Your job is to understand the human question, and answer the question.\n",
- " You can use the given tools to query data from the redshift.\n",
- " Always use aggregration queries, never fetch row queries and optimize the query for minimal output\n",
- " tools:\n",
- " - name: RedshiftQueryTool\n",
- "\n",
- " - name: AnalyticsPresenter\n",
- " kind: llm\n",
- " role: expert product manager\n",
- " job: >\n",
- " If the assistant has given an answer, summarize it and return the answer as if you are talking to a product manager\n",
- " If you needs more information, ask for the same. Always produce a good answer, this output will be show on the UI\n",
- " router:\n",
- " name: router\n",
- " kind: linear\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "session = FloSession(bedrock_chat).register_tool(\n",
- " name=\"RedshiftQueryTool\", \n",
- " tool=redshift_execution_tool\n",
- ")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "flo: Flo = Flo.build(session, yaml=yaml_data)\n",
- "flo.draw()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 13,
- "metadata": {},
- "outputs": [],
- "source": [
- "input_prompt = f\"\"\"\n",
- "Find me products which has high escalations percentages:\n",
- "\n",
- "Below is the schema of the table:\n",
- "{json.dumps(schema)}\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'AnalyticsDelegator': {'messages': ['Please find the products with high escalation percentages by calculating the percentage of escalated calls for each product and sorting them in descending order.'], 'next': 'Analyst'}}\n",
- "----\n",
- "Here is the response fro the db: product: branch_complaint\n",
- "total_calls: 1\n",
- "escalated_calls: 1\n",
- "escalation_percentage: 100.00\n",
- " ---- \n",
- "product: home_loan\n",
- "total_calls: 10\n",
- "escalated_calls: 1\n",
- "escalation_percentage: 10.00\n",
- " ---- \n",
- "product: savings_account\n",
- "total_calls: 20\n",
- "escalated_calls: 1\n",
- "escalation_percentage: 5.00\n",
- " ---- \n",
- "product: personal_loan\n",
- "total_calls: 216\n",
- "escalated_calls: 7\n",
- "escalation_percentage: 3.24\n",
- " ---- \n",
- "product: gold_loan\n",
- "total_calls: 1278\n",
- "escalated_calls: 9\n",
- "escalation_percentage: 0.70\n",
- " ---- \n",
- "product: transaction\n",
- "total_calls: 30\n",
- "escalated_calls: 0\n",
- "escalation_percentage: 0.00\n",
- " ---- \n",
- "product: insurance\n",
- "total_calls: 3\n",
- "escalated_calls: 0\n",
- "escalation_percentage: 0.00\n",
- " ---- \n",
- "product: top_up_request\n",
- "total_calls: 1\n",
- "escalated_calls: 0\n",
- "escalation_percentage: 0.00\n",
- " ---- \n",
- "product: fixed_deposits\n",
- "total_calls: 3\n",
- "escalated_calls: 0\n",
- "escalation_percentage: 0.00\n",
- " ---- \n",
- "product: balance_transfer\n",
- "total_calls: 1\n",
- "escalated_calls: 0\n",
- "escalation_percentage: 0.00\n",
- "{'Analyst': {'messages': [HumanMessage(content=[{'type': 'text', 'text': '\\n\\nLet me break down the results for you:\\n\\n1. Branch Complaint: 100% escalation rate (1 out of 1 call escalated)\\n2. Home Loan: 10% escalation rate (1 out of 10 calls escalated)\\n3. Savings Account: 5% escalation rate (1 out of 20 calls escalated)\\n4. Personal Loan: 3.24% escalation rate (7 out of 216 calls escalated)\\n5. Gold Loan: 0.70% escalation rate (9 out of 1,278 calls escalated)\\n\\nSome observations:\\n- Branch Complaint has the highest escalation percentage, but this is based on a very small sample size (only 1 call)\\n- Home Loan and Savings Account have relatively high escalation percentages\\n- Personal Loan has a moderate escalation percentage\\n- Gold Loan, despite having the most calls, has a low escalation percentage\\n\\nA few caveats:\\n1. The results are heavily influenced by the sample size\\n2. For products with very few calls (like branch_complaint), the percentage might not be statistically significant\\n3. I recommend focusing on products with a larger number of total calls for more reliable insights\\n\\nWould you like me to refine the analysis further, such as filtering for products with a minimum number of calls?', 'index': 0}], additional_kwargs={}, response_metadata={}, name='Analyst')]}}\n",
- "----\n",
- "{'AnalyticsPresenter': {'messages': [HumanMessage(content='Product Escalation Analysis Summary for Leadership:\\n\\n๐ Key Insights:\\n\\nProduct Escalation Breakdown:\\n1. Branch Complaint: 100% (Caution: Very small sample size)\\n2. Home Loan: 10% Escalation Rate\\n3. Savings Account: 5% Escalation Rate\\n4. Personal Loan: 3.24% Escalation Rate\\n5. Gold Loan: 0.70% Escalation Rate\\n\\nStrategic Recommendations:\\n- Prioritize investigation into Home Loan and Savings Account processes\\n- Conduct deeper root cause analysis for high escalation products\\n- Develop targeted agent training for these specific product lines\\n- Implement process improvements based on escalation patterns\\n\\nRisk Mitigation:\\n- For products with high escalation rates, create specialized support protocols\\n- Review current customer interaction workflows\\n- Enhance first-call resolution strategies\\n\\nLimitations:\\n- Some results might be statistically insignificant due to small sample sizes\\n- Recommend continuous monitoring and larger data collection\\n\\nNext Steps:\\n1. Validate findings with additional data points\\n2. Develop targeted intervention strategies\\n3. Monitor escalation trends quarterly\\n\\nWould you like a more detailed breakdown or specific recommendations for reducing escalations?', additional_kwargs={}, response_metadata={}, name='AnalyticsPresenter')]}}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "for s in flo.stream(input_prompt):\n",
- " if '__end__' not in s:\n",
- " print(s)\n",
- " print('----')"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.10"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/build_agents_by_code.ipynb b/flo_ai/examples/build_agents_by_code.ipynb
deleted file mode 100644
index 37b2d06d..00000000
--- a/flo_ai/examples/build_agents_by_code.ipynb
+++ /dev/null
@@ -1,265 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Code to create agents and teams\n",
- "\n",
- "This notebook shows the code flow to create agents and teams using flo-ai"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import FloSupervisor, FloAgent, FloSession, FloTeam, FloLinear, Flo, FloLLMAgent\n",
- "from langchain_openai import ChatOpenAI\n",
- "from flo_ai.models.flo_reflection_agent import FloReflectionAgent\n",
- "from flo_ai.models.delegate import Delegate\n",
- "from langchain_community.tools.tavily_search.tool import TavilySearchResults\n",
- "from dotenv import load_dotenv\n",
- "\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Code to create a simple tea, with 2 agents, each agent having one tool of itself"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o')\n",
- "session = FloSession(llm).register_tool(\n",
- " name=\"TavilySearchResults\",\n",
- " tool=TavilySearchResults()\n",
- ")\n",
- "\n",
- "researcher = FloAgent.create(\n",
- " session,\n",
- " name=\"Researcher\", \n",
- " role=\"Internet Researcher\", # optional\n",
- " job=\"Do a research on the internet and find articles of relevent to the topic asked by the user\", \n",
- " tools=[TavilySearchResults()]\n",
- ")\n",
- "\n",
- "blogger = FloAgent.create(\n",
- " session, \n",
- " name=\"BlogWriter\", \n",
- " role=\"Thought Leader\", # optional\n",
- " job=\"Able to write a blog using information provided\", \n",
- " tools=[TavilySearchResults()]\n",
- ")\n",
- "\n",
- "marketing_team = FloTeam.create(session, \"Marketing\", [researcher, blogger])\n",
- "head_of_marketing = FloSupervisor.create(session, \"Head-of-Marketing\", marketing_team)\n",
- "marketing_flo = Flo.create(session, routed_team=head_of_marketing)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "marketing_flo.draw()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "A second team created for doing editorial suggestions"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 12,
- "metadata": {},
- "outputs": [],
- "source": [
- "chief_editorial = FloLLMAgent.create(\n",
- " session, \n",
- " name=\"Senior-Editor\", \n",
- " job=\"Have a look at the article created and give editorial suggestions\"\n",
- ")\n",
- "\n",
- "edit_team = FloTeam.create(\n",
- " session, \n",
- " name=\"Editorial-Team\", \n",
- " members=[chief_editorial]\n",
- ")\n",
- "\n",
- "editor = FloSupervisor.create(\n",
- " session, \n",
- " name=\"Editor-Team-Routing\", \n",
- " team=edit_team\n",
- ")\n",
- "\n",
- "editorial_flo = Flo.create(session, routed_team=editor)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "editorial_flo.draw(xray=False)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 16,
- "metadata": {},
- "outputs": [],
- "source": [
- "journal_company = FloTeam.create(session, \"Newspaper\", [marketing_flo, editorial_flo])\n",
- "\n",
- "r3 = FloLinear.create(\n",
- " session,\n",
- " \"linear-router\",\n",
- " journal_company\n",
- ")\n",
- "\n",
- "master_flo = Flo.create(session, routed_team=r3)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "master_flo.draw()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "# master_flo.invoke(\"Write an article about CR7\")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "metadata": {},
- "outputs": [],
- "source": [
- "reflection_agent = FloReflectionAgent.create(\n",
- " session,\n",
- " \"journal-reflection\",\n",
- " \"You are critic who looks are the article and create a list of improvements that can be done.\",\n",
- " Delegate(to=[\"Marketing\"], retry=1)\n",
- ")\n",
- "\n",
- "journal_company_with_reflection = FloTeam.create(session, \"Newspaper\", [marketing_flo, editorial_flo, reflection_agent])\n",
- "\n",
- "company = FloLinear.create(\n",
- " session,\n",
- " \"linear-router\",\n",
- " journal_company_with_reflection\n",
- ")\n",
- "\n",
- "company_flo = Flo.create(session, routed_team=company)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 15,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "image/png": "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",
- "text/plain": [
- ""
- ]
- },
- "metadata": {},
- "output_type": "display_data"
- }
- ],
- "source": [
- "company_flo.draw()"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.10.10"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/cot_agent_example.py b/flo_ai/examples/cot_agent_example.py
new file mode 100644
index 00000000..13375e66
--- /dev/null
+++ b/flo_ai/examples/cot_agent_example.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+"""
+Example demonstrating Chain of Thought (CoT) reasoning pattern in the Agent class.
+"""
+
+import asyncio
+from flo_ai.models.agent import Agent
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.tool.base_tool import Tool
+import os
+
+
+class CalculatorTool(Tool):
+ """Simple calculator tool for demonstration"""
+
+ def __init__(self):
+ # Define the calculator function
+ async def calculator_function(operation: str, a: float, b: float) -> str:
+ if operation == 'add':
+ result = a + b
+ elif operation == 'subtract':
+ result = a - b
+ elif operation == 'multiply':
+ result = a * b
+ elif operation == 'divide':
+ if b == 0:
+ raise ValueError('Cannot divide by zero')
+ result = a / b
+ else:
+ raise ValueError(f'Unknown operation: {operation}')
+
+ return f'{a} {operation} {b} = {result}'
+
+ super().__init__(
+ name='calculator',
+ description='Performs basic arithmetic operations (add, subtract, multiply, divide)',
+ function=calculator_function,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'enum': ['add', 'subtract', 'multiply', 'divide'],
+ 'description': 'The arithmetic operation to perform',
+ },
+ 'a': {'type': 'number', 'description': 'First number'},
+ 'b': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+
+async def main():
+ """Main function demonstrating CoT reasoning"""
+
+ # Initialize LLM (you'll need to set OPENAI_API_KEY environment variable)
+ api_key = os.getenv('OPENAI_API_KEY')
+ if not api_key:
+ print('Please set OPENAI_API_KEY environment variable')
+ return
+
+ llm = OpenAI(model='gpt-4o-mini', api_key=api_key)
+
+ # Create tools
+ tools = [CalculatorTool()]
+
+ # Create agent with CoT reasoning pattern
+ agent = Agent(
+ name='CoT Calculator Agent',
+ system_prompt='You are a helpful math assistant that solves problems step by step.',
+ llm=llm,
+ tools=tools,
+ reasoning_pattern=ReasoningPattern.COT,
+ role='Math Assistant',
+ )
+
+ # Test questions
+ questions = [
+ 'What is 15 + 27?',
+ 'If I have 100 apples and I give away 23, then buy 15 more, how many do I have?',
+ 'Calculate 8 * 7 and then add 12 to the result.',
+ ]
+
+ print('=== Chain of Thought (CoT) Reasoning Demo ===\n')
+
+ for i, question in enumerate(questions, 1):
+ print(f'Question {i}: {question}')
+ print('-' * 50)
+
+ try:
+ response = await agent.run(question)
+ print(f'Answer: {response}')
+ except Exception as e:
+ print(f'Error: {e}')
+
+ print('\n' + '=' * 60 + '\n')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/cot_conversational_example.py b/flo_ai/examples/cot_conversational_example.py
new file mode 100644
index 00000000..875955ff
--- /dev/null
+++ b/flo_ai/examples/cot_conversational_example.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""
+Example demonstrating Chain of Thought (CoT) reasoning pattern in conversational mode.
+"""
+
+import asyncio
+from flo_ai.models.agent import Agent
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+import os
+
+
+async def main():
+ """Main function demonstrating conversational CoT reasoning"""
+
+ # Initialize LLM (you'll need to set OPENAI_API_KEY environment variable)
+ api_key = os.getenv('OPENAI_API_KEY')
+ if not api_key:
+ print('Please set OPENAI_API_KEY environment variable')
+ return
+
+ llm = OpenAI(model='gpt-3.5-turbo')
+
+ # Create agent with CoT reasoning pattern (no tools)
+ agent = Agent(
+ name='CoT Reasoning Agent',
+ system_prompt='You are a helpful assistant that thinks through problems step by step.',
+ llm=llm,
+ tools=None, # No tools for conversational mode
+ reasoning_pattern=ReasoningPattern.COT,
+ role='Problem Solver',
+ )
+
+ # Test questions that require step-by-step reasoning
+ questions = [
+ 'If a train leaves station A at 2 PM traveling 60 mph and another train leaves station B at 3 PM traveling 80 mph, and the stations are 300 miles apart, when will they meet?',
+ 'A store has a 20% discount on all items. If a customer buys 3 items that originally cost $50, $30, and $20, what is the final total after the discount?',
+ 'Explain why the sky appears blue during the day but red during sunset.',
+ ]
+
+ print('=== Conversational Chain of Thought (CoT) Reasoning Demo ===\n')
+
+ for i, question in enumerate(questions, 1):
+ print(f'Question {i}: {question}')
+ print('-' * 50)
+
+ try:
+ response = await agent.run(question)
+ print(f'Answer: {response}')
+ except Exception as e:
+ print(f'Error: {e}')
+
+ print('\n' + '=' * 60 + '\n')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/data/rag_document.txt b/flo_ai/examples/data/rag_document.txt
deleted file mode 100644
index 893a656e..00000000
--- a/flo_ai/examples/data/rag_document.txt
+++ /dev/null
@@ -1,61 +0,0 @@
-Housing Loan
-
-Your dream home is never far away! Get hassle free home loans from Federal Bank to turn your dream home into reality. We assist you to realize your dream home. Avail your Housing Loan from us at competitive interest rates. The loan scheme assists borrowers for construction of house, acquisition of land & construction of house, repairs / renovation / remodeling / extension of house, reimbursement of debt incurred for construction / purchase / furnishing / beautification / purchase of flat / villa / house plots / takeover of housing loans / supplementary housing loan to employees of well-run companies / purchase of house plot for subsequent construction of house etc.
-
-
-
-Maximum funding for your dream house -85% of the project cost.
-Loan amount up to Rs. 1500 Lakhs
-Longer repayment period up to 360 months
-Repayment holiday up to 36 months.
-Loan can be availed in overdraft format
-Minimal paperwork
-Speedy loan approval
-No pre closure charges
-Low processing fees
-Optional funding for life insurance
-Easy top up loans in future.
-Repayment of the loan is made as Equated Monthly Installments (EMI).
-
-
-Eligibility
-For Residents:
-
-
-
-For NRIs:
-
-NRI individuals including salaried people, self-employed and business persons are eligible for Housing Loan.
-Persons of Indian Origin (PIO) are also eligible for Housing Loan subject to following conditions:
-The loan is covered by primary / collateral security of immovable property in India in the name of PIO.
-The PIO should hold a valid PIO Card accompanied by a valid foreign passport.
-A close relative (as defined in Companies Act) residing in India should join as co-obligant to the loan.
-Age of the borrower should not exceed 55 years at the end of loan tenure.
-Monthly income should be not less than Rs. 50000/-.
-To avail the loan, you can apply online in simple steps. After submitting the application, bank will process your application and will be informing further details. You can track the progress of application too.
-
-
-Repayment of the loan is made as Equated Monthly Installments (EMI).
-
-You can use any one of the ways to repay the loan:
-
- Cheques
- Standing instructions at your branch
- FedNet - Internet Banking
- Automated Payment through ECS
- Mobile Banking
-
- Housing loan interest rates will change subject to the changes made by Bank/RBI from time to time.
-
-Present Repo Rate 6.50% (p.a)
-
-
-Loan scheme
-
-Interest Rate (%) *
-
-Home Loan
-
-8.80 (Repo Rate+ 2.30) Onwards
-
-*T&C Apply
\ No newline at end of file
diff --git a/flo_ai/examples/email_reply_agent.ipynb b/flo_ai/examples/email_reply_agent.ipynb
deleted file mode 100644
index caa32675..00000000
--- a/flo_ai/examples/email_reply_agent.ipynb
+++ /dev/null
@@ -1,423 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import Flo\n",
- "from flo_ai import FloSession\n",
- "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
- "\n",
- "from dotenv import load_dotenv\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setting up the send email tool"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Optional, Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "from langchain.callbacks.manager import (\n",
- " AsyncCallbackManagerForToolRun,\n",
- " CallbackManagerForToolRun,\n",
- ")\n",
- "\n",
- "class SendEmailInput(BaseModel):\n",
- " email: str = Field(description=\"The email text to be sent\")\n",
- "\n",
- "class SendEmailTool(BaseTool):\n",
- " name: str = \"email_triage\"\n",
- " description: str = \"useful for when you need to send an email to someone\"\n",
- " args_schema: Type[BaseModel] = SendEmailInput\n",
- "\n",
- " def _run(\n",
- " self, email: str, run_manager: Optional[CallbackManagerForToolRun] = None\n",
- " ) -> str:\n",
- " print(email)\n",
- " return \"Email sent successfully\"\n",
- "\n",
- " async def _arun(\n",
- " self, email: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None\n",
- " ) -> str:\n",
- " print(email)\n",
- " return \"Email sent successfully\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setting up the fetch transaction tool"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Optional, Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool, StructuredTool, tool\n",
- "from langchain.callbacks.manager import (\n",
- " AsyncCallbackManagerForToolRun,\n",
- " CallbackManagerForToolRun,\n",
- ")\n",
- "\n",
- "class FetchTrxInput(BaseModel):\n",
- " reference_number: str = Field(description=\"The transaction reference number\")\n",
- "\n",
- "class FetchTransactionTool(BaseTool):\n",
- " name: str = \"fetch_transactions\"\n",
- " description: str = \"useful for when you want to fetch the transaction details given reference number\"\n",
- " args_schema: Type[BaseModel] = FetchTrxInput\n",
- "\n",
- " def _run(\n",
- " self, reference_number: str, run_manager: Optional[CallbackManagerForToolRun] = None\n",
- " ) -> str:\n",
- " return \"The transaction happened on 23/07/2024 IST and it failed because there was not enough balance in the account\"\n",
- "\n",
- " async def _arun(\n",
- " self, reference_number: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None\n",
- " ) -> str:\n",
- " return \"The transaction happened on 23/07/2024 IST and it failed because there was not enough balance in the account\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setup a retriver db"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# import\n",
- "from langchain_chroma import Chroma\n",
- "from langchain_openai import OpenAIEmbeddings\n",
- "from langchain_community.document_loaders import TextLoader\n",
- "from langchain_text_splitters import CharacterTextSplitter\n",
- "\n",
- "# load the document and split it into chunks\n",
- "loader = TextLoader(\"./data/rag_document.txt\")\n",
- "documents = loader.load()\n",
- "\n",
- "# split it into chunks\n",
- "text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)\n",
- "docs = text_splitter.split_documents(documents)\n",
- "\n",
- "# create the open-source embedding function\n",
- "embedding_function = OpenAIEmbeddings()\n",
- "\n",
- "# load it into Chroma\n",
- "db = Chroma.from_documents(docs, embedding_function)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 5,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 5,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai.retrievers.flo_retriever import FloRagBuilder\n",
- "from flo_ai.retrievers.flo_compression_pipeline import FloCompressionPipeline\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "session = FloSession(llm)\n",
- "builder = FloRagBuilder(session, db.as_retriever())\n",
- "compression_pipeline = FloCompressionPipeline(OpenAIEmbeddings(model=\"text-embedding-3-small\"))\n",
- "compression_pipeline.add_embedding_reduntant_filter()\n",
- "compression_pipeline.add_embedding_relevant_filter()\n",
- "# Reranking\n",
- "\n",
- "retriever_tool = builder.with_multi_query().with_compression(compression_pipeline).build_retriever_tool(name=\"HousingLoanRetreiver\",\n",
- " description=\"Tool to fetch data around housing loans\")\n",
- "session.register_tool(name=\"HousingLoanTool\", tool=retriever_tool)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Register the tools"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 6,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "session.register_tool(\n",
- " name=\"SendEmailTool\", \n",
- " tool=SendEmailTool()\n",
- ").register_tool(\n",
- " name=\"FetchTransactionTool\",\n",
- " tool=FetchTransactionTool()\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setting up the agentic flo"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 7,
- "metadata": {},
- "outputs": [],
- "source": [
- "agent_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: support-email-handler\n",
- "team:\n",
- " name: SupportTicketHandler\n",
- " router:\n",
- " name: SupportSupervisor\n",
- " kind: supervisor\n",
- " agents:\n",
- " - name: EmailSender\n",
- " job: You are capable of sending the reply email but constructing a apt response\n",
- " tools:\n",
- " - name: SendEmailTool\n",
- " - name: TransactionFetcher\n",
- " job: You are capable of fetching any kind of transactions from the database given transaction reference id\n",
- " tools:\n",
- " - name: FetchTransactionTool\n",
- " - name: HousingLoanTeamLead\n",
- " job: Fetch the housing loan information from the db and answer the question\n",
- " tools:\n",
- " - name: HousingLoanTool\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Setup input prompt\n",
- "\n",
- "1. This works if the transaction number is passed\n",
- "2. Asks for transaction number if its missing"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 8,
- "metadata": {},
- "outputs": [],
- "source": [
- "email_input_for_transaction_without_id = \"\"\"\n",
- "Hi Tom,\n",
- "\n",
- "We have a failed transaction. Can you tell me more about why this failed.\n",
- "\n",
- "Thanks\n",
- "Vishnu\n",
- "\"\"\"\n",
- "\n",
- "email_input_for_transaction_with_id = \"\"\"\n",
- "Hi Tom,\n",
- "\n",
- "We have a failed transaction. Can you tell me more about why this failed.\n",
- "\n",
- "Transaction ID: 12123123432\n",
- "\n",
- "Thanks\n",
- "Vishnu\n",
- "\"\"\"\n",
- "\n",
- "email_input_for_housing_loan = \"\"\"\n",
- "Hi Tom,\n",
- "\n",
- "I am looking for a housing loan, is the interest percentage on the loan\n",
- "\n",
- "The tenure is 8 years\n",
- "Amount am looking for is 85 lakhs\n",
- "And am looking for fixed rate housing loan\n",
- "\n",
- "Thanks\n",
- "Vishnu\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 9,
- "metadata": {},
- "outputs": [],
- "source": [
- "input_prompt = \"\"\"\n",
- "You are a support ticket handling agent. You are expected to understand the email, and perform the following steps:\n",
- "\n",
- "1. Reply asking for more information if there is missing information in the email\n",
- "2. Perform the neccessary steps to required to answer the users question and reply the user query as email.\n",
- "3. If are not able to come up with a answer just reply I dont know over the email\n",
- "\n",
- "Email:\n",
- "\n",
- "\"\"\" + email_input_for_transaction_with_id"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": []
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Run the flo"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'SupportTicketHandler': {'next': 'TransactionFetcher'}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `fetch_transactions` with `{'reference_number': '12123123432'}`\n",
- "\n",
- "\n",
- "\u001b[0m\u001b[36;1m\u001b[1;3mThe transaction happened on 23/07/2024 IST and it failed because there was not enough balance in the account\u001b[0m\u001b[32;1m\u001b[1;3mSubject: Re: Failed Transaction Inquiry\n",
- "\n",
- "Hi Vishnu,\n",
- "\n",
- "Thank you for reaching out regarding the failed transaction.\n",
- "\n",
- "The transaction with ID 12123123432 occurred on 23/07/2024 IST and failed due to insufficient balance in the account.\n",
- "\n",
- "If you have any further questions or need assistance with anything else, feel free to ask!\n",
- "\n",
- "Best regards, \n",
- "Tom\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'TransactionFetcher': {'messages': [HumanMessage(content='Subject: Re: Failed Transaction Inquiry\\n\\nHi Vishnu,\\n\\nThank you for reaching out regarding the failed transaction.\\n\\nThe transaction with ID 12123123432 occurred on 23/07/2024 IST and failed due to insufficient balance in the account.\\n\\nIf you have any further questions or need assistance with anything else, feel free to ask!\\n\\nBest regards, \\nTom', additional_kwargs={}, response_metadata={}, name='TransactionFetcher')]}}\n",
- "----\n",
- "{'SupportTicketHandler': {'next': 'EmailSender'}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'email': 'Subject: Re: Failed Transaction Inquiry\\n\\nHi Vishnu,\\n\\nThank you for reaching out regarding the failed transaction.\\n\\nThe transaction with ID 12123123432 occurred on 23/07/2024 IST and failed due to insufficient balance in the account.\\n\\nIf you have any further questions or need assistance with anything else, feel free to ask!\\n\\nBest regards,\\nTom'}`\n",
- "\n",
- "\n",
- "\u001b[0mSubject: Re: Failed Transaction Inquiry\n",
- "\n",
- "Hi Vishnu,\n",
- "\n",
- "Thank you for reaching out regarding the failed transaction.\n",
- "\n",
- "The transaction with ID 12123123432 occurred on 23/07/2024 IST and failed due to insufficient balance in the account.\n",
- "\n",
- "If you have any further questions or need assistance with anything else, feel free to ask!\n",
- "\n",
- "Best regards,\n",
- "Tom\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully\u001b[0m\u001b[32;1m\u001b[1;3mI have sent the response to Vishnu regarding the failed transaction. If you need any further assistance, feel free to ask!\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'EmailSender': {'messages': [HumanMessage(content='I have sent the response to Vishnu regarding the failed transaction. If you need any further assistance, feel free to ask!', additional_kwargs={}, response_metadata={}, name='EmailSender')]}}\n",
- "----\n",
- "{'SupportTicketHandler': {'next': 'FINISH'}}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "flo: Flo = Flo.build(session, yaml=agent_yaml)\n",
- "for s in flo.stream(input_prompt):\n",
- " if \"__end__\" not in s:\n",
- " print(s)\n",
- " print(\"----\")"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.19"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/flo_tool_example.py b/flo_ai/examples/flo_tool_example.py
new file mode 100644
index 00000000..6ab3afdd
--- /dev/null
+++ b/flo_ai/examples/flo_tool_example.py
@@ -0,0 +1,191 @@
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.tool import flo_tool, create_tool_from_function
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.llm.anthropic_llm import Anthropic
+from flo_ai.llm.base_llm import BaseLLM
+
+
+# Example 1: Using the @flo_tool decorator with parameter descriptions
+@flo_tool(
+ description='Perform basic calculations (add, subtract, multiply, divide)',
+ parameter_descriptions={
+ 'operation': 'The operation to perform (add, subtract, multiply, divide)',
+ 'x': 'First number',
+ 'y': 'Second number',
+ },
+)
+async def calculate(operation: str, x: float, y: float) -> float:
+ """Calculate mathematical operations between two numbers."""
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+
+# Example 2: Using the @flo_tool decorator with docstring-based descriptions
+@flo_tool()
+async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ """
+ Convert between different units (km/miles, kg/lbs, celsius/fahrenheit).
+
+ Args:
+ value: The value to convert
+ from_unit: The unit to convert from
+ to_unit: The unit to convert to
+ """
+ conversions = {
+ ('km', 'miles'): lambda x: x * 0.621371,
+ ('miles', 'km'): lambda x: x * 1.60934,
+ ('kg', 'lbs'): lambda x: x * 2.20462,
+ ('lbs', 'kg'): lambda x: x * 0.453592,
+ ('celsius', 'fahrenheit'): lambda x: (x * 9 / 5) + 32,
+ ('fahrenheit', 'celsius'): lambda x: (x - 32) * 5 / 9,
+ }
+
+ key = (from_unit.lower(), to_unit.lower())
+ if key not in conversions:
+ raise ValueError(f'Unsupported conversion: {from_unit} to {to_unit}')
+
+ result = conversions[key](value)
+ return f'{value} {from_unit} = {result:.2f} {to_unit}'
+
+
+# Example 3: Using the @flo_tool decorator with custom name
+@flo_tool(
+ name='weather_checker', description='Get current weather information for a city'
+)
+async def get_weather(city: str, country: str = None) -> str:
+ """Get weather information for a specific city."""
+ # This is a mock weather tool - in real use, you'd call a weather API
+ weather_data = {
+ 'london': {'temp': 18, 'condition': 'cloudy'},
+ 'paris': {'temp': 22, 'condition': 'sunny'},
+ 'new york': {'temp': 25, 'condition': 'partly cloudy'},
+ 'tokyo': {'temp': 28, 'condition': 'rainy'},
+ }
+
+ city_key = city.lower()
+ if city_key not in weather_data:
+ return f'Weather data for {city} is not available'
+
+ data = weather_data[city_key]
+ location = f'{city}, {country}' if country else city
+ return f"Current weather in {location}: {data['temp']}ยฐC, {data['condition']}"
+
+
+# Example 4: Regular function that we'll convert to a tool later
+async def format_text(text: str, style: str = 'normal') -> str:
+ """Format text in different styles."""
+ styles = {
+ 'uppercase': text.upper(),
+ 'lowercase': text.lower(),
+ 'title': text.title(),
+ 'normal': text,
+ }
+ return styles.get(style, text)
+
+
+async def test_flo_tool_decorator():
+ """Test the @flo_tool decorator functionality."""
+ print('=== Testing @flo_tool Decorator ===\n')
+
+ # Test 1: Function can be called normally
+ print('1. Testing function calls:')
+ result1 = await calculate('add', 5, 3)
+ print(f" calculate('add', 5, 3) = {result1}")
+
+ result2 = await convert_units(10, 'km', 'miles')
+ print(f" convert_units(10, 'km', 'miles') = {result2}")
+
+ result3 = await get_weather('Paris', 'France')
+ print(f" get_weather('Paris', 'France') = {result3}")
+
+ # Test 2: Tool objects are accessible via .tool attribute
+ print('\n2. Testing tool objects:')
+ print(f' calculate.tool.name = {calculate.tool.name}')
+ print(f' calculate.tool.description = {calculate.tool.description}')
+ print(f' calculate.tool.parameters = {calculate.tool.parameters}')
+
+ print(f' convert_units.tool.name = {convert_units.tool.name}')
+ print(f' get_weather.tool.name = {get_weather.tool.name}')
+
+ # Test 3: Using create_tool_from_function for existing functions
+ print('\n3. Testing create_tool_from_function:')
+ format_tool = create_tool_from_function(
+ format_text,
+ name='text_formatter',
+ description='Format text in different styles',
+ parameter_descriptions={
+ 'text': 'The text to format',
+ 'style': 'The formatting style (uppercase, lowercase, title, normal)',
+ },
+ )
+ print(f' format_tool.name = {format_tool.name}')
+ print(f' format_tool.parameters = {format_tool.parameters}')
+
+
+async def test_multi_tool_agent_with_decorator(llm: BaseLLM, agent_name: str):
+ """Test the decorated tools with an agent."""
+ # Collect all tools from decorated functions
+ tools = [
+ calculate.tool,
+ convert_units.tool,
+ get_weather.tool,
+ create_tool_from_function(
+ format_text,
+ name='text_formatter',
+ description='Format text in different styles',
+ ),
+ ]
+
+ agent = (
+ AgentBuilder()
+ .with_name(agent_name)
+ .with_prompt("""You are a helpful assistant that can perform calculations,
+ unit conversions, check weather information, and format text.
+ Use the available tools to provide accurate responses.""")
+ .with_llm(llm)
+ .with_tools(tools)
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
+ )
+
+ # Test cases that require multiple tool usage
+ test_queries = [
+ "If it's 25ยฐC in Paris, what's that in Fahrenheit? Also, how's the weather there?",
+ "I'm planning a 10 km run in London. How many miles is that, and what's the weather like for running?",
+ 'If I have 2.5 kg of flour and need to triple it for a large batch, how many pounds would that be?',
+ "Format the text 'hello world' in uppercase and then convert 5 miles to kilometers",
+ ]
+
+ print(f'\n=== Testing {agent_name} with @flo_tool decorated functions ===')
+ for query in test_queries:
+ print(f'\nQuery: {query}')
+ response = await agent.run(query)
+ print(f'Response: {response}')
+ print('-' * 80)
+
+
+async def main():
+ # Test the decorator functionality
+ await test_flo_tool_decorator()
+
+ # Test with OpenAI
+ openai_llm = OpenAI(model='gpt-40-mini', temperature=0.7)
+ await test_multi_tool_agent_with_decorator(openai_llm, 'OpenAI @flo_tool Agent')
+
+ # Test with Claude
+ claude_llm = Anthropic(model='claude-3-5-sonnet-20240620', temperature=0.7)
+ await test_multi_tool_agent_with_decorator(claude_llm, 'Claude @flo_tool Agent')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/images/agentic-rag.png b/flo_ai/examples/images/agentic-rag.png
deleted file mode 100644
index 5de3601d..00000000
Binary files a/flo_ai/examples/images/agentic-rag.png and /dev/null differ
diff --git a/flo_ai/examples/linear_router_example.ipynb b/flo_ai/examples/linear_router_example.ipynb
deleted file mode 100644
index 51cec457..00000000
--- a/flo_ai/examples/linear_router_example.ipynb
+++ /dev/null
@@ -1,304 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import Flo\n",
- "from flo_ai import FloSession\n",
- "from langchain_openai import ChatOpenAI\n",
- "\n",
- "from dotenv import load_dotenv\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "\n",
- "class SendEmailInput(BaseModel):\n",
- " to: str = Field(description=\"Comma seperared list of users emails to which email needs to be sent\")\n",
- " email: str = Field(description=\"The email text to be sent\")\n",
- "\n",
- "class SendEmailTool(BaseTool):\n",
- " name = \"email_triage\"\n",
- " description = \"useful for when you need to send an email to someone\"\n",
- " args_schema: Type[BaseModel] = SendEmailInput\n",
- "\n",
- " def _run(\n",
- " self, \n",
- " email: str,\n",
- " to: str\n",
- " ) -> str:\n",
- " print(email)\n",
- " return f\"Email sent successfully to {to}\"\n",
- "\n",
- " async def _arun(\n",
- " self, \n",
- " email: str,\n",
- " to: str\n",
- " ) -> str:\n",
- " print(email)\n",
- " return f\"Email sent successfully to {to}\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 11,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o')\n",
- "session = FloSession(llm)\n",
- "\n",
- "session.register_tool(\n",
- " name=\"SendEmailTool\", \n",
- " tool=SendEmailTool()\n",
- ")"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 17,
- "metadata": {},
- "outputs": [],
- "source": [
- "agent_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: invite-handler\n",
- "team:\n",
- " name: EventInviteBot\n",
- " router:\n",
- " name: Sequential-Router\n",
- " kind: linear\n",
- " agents:\n",
- " - name: EmailFriends\n",
- " job: You job is to send an invite to the christmas party at my house to my friends and friends only, not collegues, invite their spouses too. Keep the email warm and friendly.\n",
- " role: personal ai assistant\n",
- " tools:\n",
- " - name: SendEmailTool\n",
- " - name: EmailColleagues\n",
- " job: You job is to send an invite to the christmas party at my house to my colleagues and not friends. Keep the email formal, and DO NOT invite the spouses.\n",
- " role: office ai assistant\n",
- " tools:\n",
- " - name: SendEmailTool\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 18,
- "metadata": {},
- "outputs": [],
- "source": [
- "input_prompt = \"\"\"\n",
- "Here is the list of user emails and there relations to me\n",
- "\n",
- "vishnu@gmail.com / friend\n",
- "nk@gmail.com / friend\n",
- "jk@gmail.com / colleague\n",
- "ck@hotmail.com / friend\n",
- "hk@gmail.com / colleague\n",
- "jak@gmail.com / colleague\n",
- "ck@gmail.com / friend.\n",
- "\n",
- "Please invite these nice folks to my christmas party\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 19,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'vishnu@gmail.com', 'email': \"Hey Vishnu,\\n\\nI hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\\n\\nLooking forward to celebrating together!\\n\\nBest,\\n[Your Name]\"}`\n",
- "\n",
- "\n",
- "\u001b[0mHey Vishnu,\n",
- "\n",
- "I hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\n",
- "\n",
- "Looking forward to celebrating together!\n",
- "\n",
- "Best,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to vishnu@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'nk@gmail.com', 'email': \"Hey NK,\\n\\nI hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\\n\\nLooking forward to celebrating together!\\n\\nBest,\\n[Your Name]\"}`\n",
- "\n",
- "\n",
- "\u001b[0mHey NK,\n",
- "\n",
- "I hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\n",
- "\n",
- "Looking forward to celebrating together!\n",
- "\n",
- "Best,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to nk@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'ck@hotmail.com', 'email': \"Hey CK,\\n\\nI hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\\n\\nLooking forward to celebrating together!\\n\\nBest,\\n[Your Name]\"}`\n",
- "\n",
- "\n",
- "\u001b[0mHey CK,\n",
- "\n",
- "I hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\n",
- "\n",
- "Looking forward to celebrating together!\n",
- "\n",
- "Best,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to ck@hotmail.com\u001b[0m\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'ck@gmail.com', 'email': \"Hey CK,\\n\\nI hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\\n\\nLooking forward to celebrating together!\\n\\nBest,\\n[Your Name]\"}`\n",
- "\n",
- "\n",
- "\u001b[0mHey CK,\n",
- "\n",
- "I hope this email finds you well! I'm excited to invite you and your spouse to my Christmas party at my house. It's going to be a fun and festive evening, and I'd love for you both to join us.\n",
- "\n",
- "Looking forward to celebrating together!\n",
- "\n",
- "Best,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to ck@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3mAll your friends have been successfully invited to your Christmas party! ๐โจ\n",
- "\n",
- "If you need any further assistance, feel free to ask. Enjoy the festivities!\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'EmailFriends': {'messages': [HumanMessage(content='All your friends have been successfully invited to your Christmas party! ๐โจ\\n\\nIf you need any further assistance, feel free to ask. Enjoy the festivities!', name='EmailFriends-SJ0aQ')]}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'jk@gmail.com', 'email': 'Dear Colleague,\\n\\nI hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\\n\\nDate: [Insert Date]\\nTime: [Insert Time]\\nVenue: [Insert Address]\\n\\nPlease note that this invitation is extended to you only, and not to spouses or other guests.\\n\\nLooking forward to your presence.\\n\\nBest regards,\\n[Your Name]'}`\n",
- "\n",
- "\n",
- "\u001b[0mDear Colleague,\n",
- "\n",
- "I hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\n",
- "\n",
- "Date: [Insert Date]\n",
- "Time: [Insert Time]\n",
- "Venue: [Insert Address]\n",
- "\n",
- "Please note that this invitation is extended to you only, and not to spouses or other guests.\n",
- "\n",
- "Looking forward to your presence.\n",
- "\n",
- "Best regards,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to jk@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'hk@gmail.com', 'email': 'Dear Colleague,\\n\\nI hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\\n\\nDate: [Insert Date]\\nTime: [Insert Time]\\nVenue: [Insert Address]\\n\\nPlease note that this invitation is extended to you only, and not to spouses or other guests.\\n\\nLooking forward to your presence.\\n\\nBest regards,\\n[Your Name]'}`\n",
- "\n",
- "\n",
- "\u001b[0mDear Colleague,\n",
- "\n",
- "I hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\n",
- "\n",
- "Date: [Insert Date]\n",
- "Time: [Insert Time]\n",
- "Venue: [Insert Address]\n",
- "\n",
- "Please note that this invitation is extended to you only, and not to spouses or other guests.\n",
- "\n",
- "Looking forward to your presence.\n",
- "\n",
- "Best regards,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to hk@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `email_triage` with `{'to': 'jak@gmail.com', 'email': 'Dear Colleague,\\n\\nI hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\\n\\nDate: [Insert Date]\\nTime: [Insert Time]\\nVenue: [Insert Address]\\n\\nPlease note that this invitation is extended to you only, and not to spouses or other guests.\\n\\nLooking forward to your presence.\\n\\nBest regards,\\n[Your Name]'}`\n",
- "\n",
- "\n",
- "\u001b[0mDear Colleague,\n",
- "\n",
- "I hope this message finds you well. I am pleased to invite you to a Christmas party at my house. It would be a pleasure to have you join us for an evening of celebration and good cheer.\n",
- "\n",
- "Date: [Insert Date]\n",
- "Time: [Insert Time]\n",
- "Venue: [Insert Address]\n",
- "\n",
- "Please note that this invitation is extended to you only, and not to spouses or other guests.\n",
- "\n",
- "Looking forward to your presence.\n",
- "\n",
- "Best regards,\n",
- "[Your Name]\n",
- "\u001b[36;1m\u001b[1;3mEmail sent successfully to jak@gmail.com\u001b[0m\u001b[32;1m\u001b[1;3mAll your colleagues have been successfully invited to your Christmas party! ๐โจ\n",
- "\n",
- "If you need any further assistance, feel free to ask. Enjoy the festivities!\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'EmailColleagues': {'messages': [HumanMessage(content='All your colleagues have been successfully invited to your Christmas party! ๐โจ\\n\\nIf you need any further assistance, feel free to ask. Enjoy the festivities!', name='EmailColleagues-R2b4C')]}}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "flo: Flo = Flo.build(session, yaml=agent_yaml)\n",
- "for s in flo.stream(input_prompt):\n",
- " if \"__end__\" not in s:\n",
- " print(s)\n",
- " print(\"----\")"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/llm_router_example.ipynb b/flo_ai/examples/llm_router_example.ipynb
deleted file mode 100644
index b53c5509..00000000
--- a/flo_ai/examples/llm_router_example.ipynb
+++ /dev/null
@@ -1,114 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": 1,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 1,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from flo_ai import Flo\n",
- "from flo_ai import FloSession\n",
- "from langchain_openai import ChatOpenAI, OpenAIEmbeddings\n",
- "\n",
- "from dotenv import load_dotenv\n",
- "load_dotenv()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 2,
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_community.tools.tavily_search.tool import TavilySearchResults\n",
- "\n",
- "llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')\n",
- "session = FloSession(llm)\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 3,
- "metadata": {},
- "outputs": [],
- "source": [
- "agent_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: adding-team\n",
- "team:\n",
- " name: AddingTeam1\n",
- " router:\n",
- " name: router\n",
- " kind: llm\n",
- " job: Sent to the next member which was not called\n",
- " agents:\n",
- " - name: Agent1\n",
- " kind: llm\n",
- " role: Expert mathematician\n",
- " job: You are an expert in mathematics. Add one to the number given to you. And pass the result to the next agent\n",
- " - name: Agent2\n",
- " kind: llm\n",
- " role: Expert mathematician\n",
- " job: You are an expert in mathematics. Add one to the number given to you. And pass the result to the next agent\n",
- " - name: Agent3\n",
- " kind: llm\n",
- " role: Expert mathematician\n",
- " job: You are an expert in mathematics. Add one to the number given to you. And pass the result to the next agent\n",
- "\"\"\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 4,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'messages': [HumanMessage(content='Start with number 7'), HumanMessage(content='The result is 8.', name='Agent1-rzQ1B'), HumanMessage(content='The result is 9.', name='Agent2-izmIo'), HumanMessage(content='The result is 10.', name='Agent3-WgTbh')], 'next': 'FINISH'}\n"
- ]
- }
- ],
- "source": [
- "flo = Flo.build(session, agent_yaml)\n",
- "flo.draw_to_file(\"aasd.png\")\n",
- "\n",
- "print(flo.invoke(\"Start with number 7\"))"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/multi_tool_example.py b/flo_ai/examples/multi_tool_example.py
new file mode 100644
index 00000000..41583f96
--- /dev/null
+++ b/flo_ai/examples/multi_tool_example.py
@@ -0,0 +1,146 @@
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.tool.base_tool import Tool
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.openai_llm import OpenAI
+
+from flo_ai.llm.anthropic_llm import Anthropic
+from flo_ai.llm.base_llm import BaseLLM
+
+
+async def create_tools():
+ """Create a set of tools for the agents to use"""
+
+ # Calculator tool
+ async def calculate(operation: str, x: float, y: float) -> float:
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+ calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations (add, subtract, multiply, divide)',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add, subtract, multiply, divide)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+ # Unit conversion tool
+ async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ conversions = {
+ ('km', 'miles'): lambda x: x * 0.621371,
+ ('miles', 'km'): lambda x: x * 1.60934,
+ ('kg', 'lbs'): lambda x: x * 2.20462,
+ ('lbs', 'kg'): lambda x: x * 0.453592,
+ ('celsius', 'fahrenheit'): lambda x: (x * 9 / 5) + 32,
+ ('fahrenheit', 'celsius'): lambda x: (x - 32) * 5 / 9,
+ }
+
+ key = (from_unit.lower(), to_unit.lower())
+ if key not in conversions:
+ raise ValueError(f'Unsupported conversion: {from_unit} to {to_unit}')
+
+ result = conversions[key](value)
+ return f'{value} {from_unit} = {result:.2f} {to_unit}'
+
+ converter_tool = Tool(
+ name='convert_units',
+ description='Convert between different units (km/miles, kg/lbs, celsius/fahrenheit)',
+ function=convert_units,
+ parameters={
+ 'value': {'type': 'number', 'description': 'The value to convert'},
+ 'from_unit': {'type': 'string', 'description': 'The unit to convert from'},
+ 'to_unit': {'type': 'string', 'description': 'The unit to convert to'},
+ },
+ )
+
+ # Weather tool (mock)
+ async def get_weather(city: str, country: str = None) -> str:
+ # This is a mock weather tool - in real use, you'd call a weather API
+ weather_data = {
+ 'london': {'temp': 18, 'condition': 'cloudy'},
+ 'paris': {'temp': 22, 'condition': 'sunny'},
+ 'new york': {'temp': 25, 'condition': 'partly cloudy'},
+ 'tokyo': {'temp': 28, 'condition': 'rainy'},
+ }
+
+ city_key = city.lower()
+ if city_key not in weather_data:
+ return f'Weather data for {city} is not available'
+
+ data = weather_data[city_key]
+ location = f'{city}, {country}' if country else city
+ return f"Current weather in {location}: {data['temp']}ยฐC, {data['condition']}"
+
+ weather_tool = Tool(
+ name='get_weather',
+ description='Get current weather information for a city',
+ function=get_weather,
+ parameters={
+ 'city': {'type': 'string', 'description': 'The city to get weather for'},
+ 'country': {
+ 'type': 'string',
+ 'description': 'The country (optional)',
+ 'required': False,
+ },
+ },
+ )
+
+ return [calculator_tool, converter_tool, weather_tool]
+
+
+async def test_multi_tool_agent(llm: BaseLLM, agent_name: str):
+ tools = await create_tools()
+
+ agent = (
+ AgentBuilder()
+ .with_name(agent_name)
+ .with_prompt("""You are a helpful assistant that can perform calculations,
+ unit conversions, and check weather information.
+ Use the available tools to provide accurate responses.""")
+ .with_llm(llm)
+ .with_tools(tools)
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
+ )
+
+ # Test cases that require multiple tool usage
+ test_queries = [
+ "If it's 25ยฐC in Paris, what's that in Fahrenheit? Also, how's the weather there?",
+ "I'm planning a 10 km run in London. How many miles is that, and what's the weather like for running?",
+ 'If I have 2.5 kg of flour and need to triple it for a large batch, how many pounds would that be?',
+ ]
+
+ print(f'\n=== Testing {agent_name} ===')
+ for query in test_queries:
+ print(f'\nQuery: {query}')
+ response = await agent.run(query)
+ print(f'Response: {response}')
+ print('-' * 80)
+
+
+async def main():
+ # Test with OpenAI
+ openai_llm = OpenAI(model='gpt-4-turbo-preview', temperature=0.7)
+ await test_multi_tool_agent(openai_llm, 'OpenAI Multi-Tool Agent')
+
+ # # Test with Claude
+ claude_llm = Anthropic(model='claude-3-5-sonnet-20240620', temperature=0.7)
+ await test_multi_tool_agent(claude_llm, 'Claude Multi-Tool Agent')
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/ollama_agent_example.py b/flo_ai/examples/ollama_agent_example.py
new file mode 100644
index 00000000..28ef3851
--- /dev/null
+++ b/flo_ai/examples/ollama_agent_example.py
@@ -0,0 +1,173 @@
+import asyncio
+from flo_ai.builder.agent_builder import AgentBuilder
+from flo_ai.llm.ollama_llm import OllamaLLM
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.tool.base_tool import Tool
+
+
+async def create_tools():
+ """Create a set of tools for the Ollama agent to use"""
+
+ # Calculator tool
+ async def calculate(operation: str, x: float, y: float) -> float:
+ operations = {
+ 'add': lambda: x + y,
+ 'subtract': lambda: x - y,
+ 'multiply': lambda: x * y,
+ 'divide': lambda: x / y if y != 0 else 'Cannot divide by zero',
+ }
+ if operation not in operations:
+ raise ValueError(f'Unknown operation: {operation}')
+ return operations[operation]()
+
+ calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations (add, subtract, multiply, divide)',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add, subtract, multiply, divide)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+ # Unit conversion tool
+ async def convert_units(value: float, from_unit: str, to_unit: str) -> str:
+ conversions = {
+ ('km', 'miles'): lambda x: x * 0.621371,
+ ('miles', 'km'): lambda x: x * 1.60934,
+ ('kg', 'lbs'): lambda x: x * 2.20462,
+ ('lbs', 'kg'): lambda x: x * 0.453592,
+ }
+
+ key = (from_unit.lower(), to_unit.lower())
+ if key not in conversions:
+ raise ValueError(f'Unsupported conversion: {from_unit} to {to_unit}')
+
+ result = conversions[key](value)
+ return f'{value} {from_unit} = {result:.2f} {to_unit}'
+
+ converter_tool = Tool(
+ name='convert_units',
+ description='Convert between different units (km/miles, kg/lbs)',
+ function=convert_units,
+ parameters={
+ 'value': {'type': 'number', 'description': 'The value to convert'},
+ 'from_unit': {'type': 'string', 'description': 'The unit to convert from'},
+ 'to_unit': {'type': 'string', 'description': 'The unit to convert to'},
+ },
+ )
+
+ return [calculator_tool, converter_tool]
+
+
+async def example_ollama_agent():
+ # Create an Ollama LLM instance using the phi4 model
+ ollama_llm = OllamaLLM(
+ model='phi4', temperature=0.7, base_url='http://localhost:11434'
+ )
+
+ # Create a simple conversational agent with Ollama
+ agent = (
+ AgentBuilder()
+ .with_name('Ollama Assistant')
+ .with_prompt('You are a helpful AI assistant powered by Ollama.')
+ .with_llm(ollama_llm)
+ .with_retries(2)
+ .build()
+ )
+
+ # Test the agent with a simple question
+ response = await agent.run('What is the capital of France?')
+ print(f'Ollama Agent Response: {response}')
+
+
+async def example_ollama_structured_output():
+ # Define output schema for structured responses
+ location_schema = {
+ 'type': 'object',
+ 'properties': {
+ 'city': {'type': 'string', 'description': 'The name of the city'},
+ 'country': {'type': 'string', 'description': 'The name of the country'},
+ 'population': {
+ 'type': 'number',
+ 'description': 'The population of the city',
+ },
+ },
+ 'required': ['city', 'country', 'population'],
+ }
+
+ # Create an Ollama LLM instance using the llama3.2:1b model
+ ollama_llm = OllamaLLM(
+ model='llama3.2:1b', temperature=0.7, base_url='http://localhost:11434'
+ )
+
+ # Create an agent with structured output
+ agent = (
+ AgentBuilder()
+ .with_name('Structured Location Assistant')
+ .with_prompt(
+ 'You are a location information assistant that provides structured data about cities.'
+ )
+ .with_llm(ollama_llm)
+ .with_output_schema(location_schema)
+ .build()
+ )
+
+ response = await agent.run('Tell me about Tokyo')
+ print(f'Structured Output Response: {response}')
+
+
+async def example_ollama_tools():
+ # Create an Ollama LLM instance using the phi4 model
+ ollama_llm = OllamaLLM(
+ model='phi4', temperature=0.7, base_url='http://localhost:11434'
+ )
+
+ # Create tools
+ tools = await create_tools()
+
+ # Create a tool-using agent with Ollama
+ agent = (
+ AgentBuilder()
+ .with_name('Ollama Tool Assistant')
+ .with_prompt("""You are a helpful assistant that can perform calculations and unit conversions.
+ Use the available tools to provide accurate responses.""")
+ .with_llm(ollama_llm)
+ .with_tools(tools)
+ .with_reasoning(ReasoningPattern.REACT)
+ .with_retries(2)
+ .build()
+ )
+
+ # Test cases that require tool usage
+ test_queries = [
+ 'What is 25 kilometers in miles?',
+ 'If I have 2.5 kg of flour and need to triple it, how many pounds would that be?',
+ 'Calculate 15 multiplied by 7 and then convert the result from kg to lbs',
+ ]
+
+ print('\n=== Testing Ollama Tool Agent ===')
+ for query in test_queries:
+ print(f'\nQuery: {query}')
+ response = await agent.run(query)
+ print(f'Response: {response}')
+ print('-' * 80)
+
+
+async def main():
+ print('\n=== Simple Ollama Conversational Agent ===')
+ await example_ollama_agent()
+
+ print('\n=== Ollama Structured Output Agent ===')
+ await example_ollama_structured_output()
+
+ print('\n=== Ollama Tool Agent ===')
+ await example_ollama_tools()
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/output_formatter.py b/flo_ai/examples/output_formatter.py
new file mode 100644
index 00000000..88f9c806
--- /dev/null
+++ b/flo_ai/examples/output_formatter.py
@@ -0,0 +1,198 @@
+import asyncio
+from textwrap import dedent
+from pydantic import BaseModel, Field
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.llm.anthropic_llm import Anthropic
+from flo_ai.models.agent import Agent as ToolAgent
+from flo_ai.builder.agent_builder import AgentBuilder
+
+
+# Define the output schema using Pydantic
+class Step(BaseModel):
+ explanation: str
+ output: str
+
+
+class MathReasoning(BaseModel):
+ steps: list[Step]
+ final_answer: str
+
+
+class MathSolution(BaseModel):
+ solution: str = Field(description='The step-by-step solution to the math problem')
+ answer: str = Field(description='The final answer')
+
+
+math_tutor_prompt = """
+ You are a helpful math tutor. You will be provided with a math problem,
+ and your goal will be to output a step by step solution, along with a final answer.
+ For each step, just provide the output as an equation use the explanation field to detail the reasoning.
+
+ Provide your response in JSON format following the specified schema.
+"""
+
+
+async def pydantic_builder_example():
+ """Example demonstrating the use of Pydantic models with AgentBuilder"""
+ # Initialize LLMs
+ openai_llm = OpenAI(model='gpt-4-turbo-preview')
+ claude_llm = Anthropic()
+
+ # Create OpenAI agent using AgentBuilder with Pydantic model
+ openai_agent = (
+ AgentBuilder()
+ .with_name('OpenAI Math Tutor')
+ .with_prompt(
+ dedent("""
+ You are a helpful math tutor. When solving problems:
+ 1. Show your step-by-step solution
+ 2. Provide the final answer
+
+ Format your response according to the specified json schema.
+ """)
+ )
+ .with_llm(openai_llm)
+ .with_output_schema(MathSolution) # Using Pydantic model directly
+ .build()
+ )
+
+ # Create Claude agent using AgentBuilder with Pydantic model
+ claude_agent = (
+ AgentBuilder()
+ .with_name('Claude Math Tutor')
+ .with_prompt(
+ dedent("""
+ You are a helpful math tutor. When solving problems:
+ 1. Show your step-by-step solution
+ 2. Provide the final answer
+
+ Format your response according to the specified schema.
+ """)
+ )
+ .with_llm(claude_llm)
+ .with_output_schema(MathSolution) # Using Pydantic model directly
+ .build()
+ )
+
+ # Run both agents
+ problem = 'Solve 8x + 7 = -23'
+ openai_response = await openai_agent.run(problem)
+ claude_response = await claude_agent.run(problem)
+
+ print('\n=== Pydantic Builder Example ===')
+ print('\nOpenAI Agent Response:', openai_response)
+ print('\nClaude Agent Response:', claude_response)
+
+
+async def main():
+ # Initialize LLMs
+ openai_llm = OpenAI(model='gpt-4-turbo-preview')
+ claude_llm = Anthropic()
+
+ # OpenAI example
+ openai_response = await openai_llm.generate(
+ messages=[
+ {'role': 'system', 'content': dedent(math_tutor_prompt)},
+ {'role': 'user', 'content': 'Solve 8x + 7 = -23'},
+ ],
+ output_schema={
+ 'name': 'math_reasoning',
+ 'schema': {
+ 'type': 'object',
+ 'properties': {
+ 'steps': {
+ 'type': 'array',
+ 'items': {
+ 'type': 'object',
+ 'properties': {
+ 'explanation': {'type': 'string'},
+ 'output': {'type': 'string'},
+ },
+ 'required': ['explanation', 'output'],
+ 'additionalProperties': False,
+ },
+ },
+ 'final_answer': {'type': 'string'},
+ },
+ 'required': ['steps', 'final_answer'],
+ 'additionalProperties': False,
+ },
+ },
+ )
+ print('OpenAI Response:', openai_response)
+
+ # Claude example
+ claude_response = await claude_llm.generate(
+ messages=[{'role': 'user', 'content': 'Solve 8x + 7 = -23'}],
+ output_schema=MathReasoning.model_json_schema(),
+ )
+ print('Claude Response:', claude_response)
+
+
+async def agent_example():
+ # Initialize LLMs
+ openai_llm = OpenAI(model='gpt-4-turbo-preview')
+ claude_llm = Anthropic()
+
+ # Define output schema
+ math_schema = {
+ 'type': 'object',
+ 'properties': {
+ 'solution': {
+ 'type': 'string',
+ 'description': 'The step-by-step solution to the math problem',
+ },
+ 'answer': {'type': 'string', 'description': 'The final answer'},
+ },
+ 'required': ['solution', 'answer'],
+ }
+
+ # Create OpenAI agent
+ openai_agent = ToolAgent(
+ name='OpenAI Math Tutor',
+ system_prompt=dedent("""
+ You are a helpful math tutor. When solving problems:
+ 1. Show your step-by-step solution
+ 2. Provide the final answer
+
+ Format your response as JSON with this structure:
+ {
+ "solution": "Step by step solution here",
+ "answer": "Final answer here"
+ }
+ """),
+ llm=openai_llm,
+ output_schema=math_schema,
+ )
+
+ # Create Claude agent
+ claude_agent = ToolAgent(
+ name='Claude Math Tutor',
+ system_prompt=dedent("""
+ You are a helpful math tutor. When solving problems:
+ 1. Show your step-by-step solution
+ 2. Provide the final answer
+
+ Format your response as JSON with this structure:
+ {
+ "solution": "Step by step solution here",
+ "answer": "Final answer here"
+ }
+ """),
+ llm=claude_llm,
+ output_schema=math_schema,
+ )
+
+ # Run both agents
+ problem = 'Solve 8x + 7 = -23'
+ openai_response = await openai_agent.run(problem)
+ claude_response = await claude_agent.run(problem)
+
+ print('\nOpenAI Agent Response:', openai_response)
+ print('\nClaude Agent Response:', claude_response)
+
+
+if __name__ == '__main__':
+ # asyncio.run(main())
+ # asyncio.run(agent_example())
+ asyncio.run(pydantic_builder_example())
diff --git a/flo_ai/examples/population_simulator.ipynb b/flo_ai/examples/population_simulator.ipynb
deleted file mode 100644
index e3380b2b..00000000
--- a/flo_ai/examples/population_simulator.ipynb
+++ /dev/null
@@ -1,1072 +0,0 @@
-{
- "cells": [
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Gen AI Population Simulation Testing for AI Workflow Automation Validation\n",
- "\n",
- "## Goal\n",
- "The goal is to generate a population of banking users and use them to simulate banking operations, to test AI workflow automations. \n",
- "\n",
- "The idea here is to perform a population testing on banking systems using AI, especially AI automated workflows\n",
- "\n",
- "## Strategy\n",
- "\n",
- "The strategy here is to generate customer profiles using Gen AI and use Gen AI is create backstories for these customers. Once we have customers with backstory we simulate an event in customer life which requires interaction with their banks or wealth advisor. This interaction we be handled by an automated AI agent and we see how good the system is able to handle this scenario\n",
- "\n",
- "## Implementation\n",
- "1. Define the customer schema and the properties the customer should have.\n",
- "2. We create a agentic flo which will generate such customers randomly, the agent saves this data to SQLite database\n",
- "3. The flo also has an agent which can query the created database which will help the agentic flo to check back the distribution of generated customers\n",
- "4. Once we have the generated customers, we loop through each customer and use their backstore to create a life event, requiring banking help. This will be a new agentic flo which can trigger different banking apis like customer support or sending email etc.\n",
- "\n",
- "Through this excersice we create AI generated customer population and through this we test our automations"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 1. Define the customer schema\n",
- "We will define an elaborate customer schema to have all the variables associated with the customer"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 41,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "from typing import List\n",
- "from peewee import Model, SqliteDatabase, CharField, IntegerField, TextField\n",
- "from dotenv import load_dotenv\n",
- "\n",
- "load_dotenv()\n",
- "\n",
- "class Customer(BaseModel):\n",
- " # base info\n",
- " name: str = Field(\"Name of the customer\")\n",
- " age: int = Field(\"Age of the customer\")\n",
- " income: int = Field(\"Annual income in INR\")\n",
- "\n",
- " #assets\n",
- " cash: int = Field(\"Total cash in hand in INR\")\n",
- " equities: int = Field(\"Amount invested in equities market in INR\")\n",
- " realEstate: int = Field(\"Amount invested in real estate in INR\")\n",
- " retirementAccounts: int = Field(\"Amount invested in pension schemes in INR\")\n",
- " commodities: int = Field(\"Amount invested in gold or silver in INR\")\n",
- " alternativeInvestments: int = Field(\"Amount invested in private equity and hedge funds in INR\")\n",
- " cars: int = Field(\"Market value of cars owned in INR\")\n",
- " bikes: int = Field(\"Market value of bikes owned in INR\")\n",
- " insurance: int = Field(\"Coverage across health and life insurance in INR\")\n",
- "\n",
- " # debts\n",
- " studentLoans: int = Field(\"Amount of student debt in INR\")\n",
- " creditCardDebt: int = Field(\"Amount of credit card debt in INR\")\n",
- " autoLoans: int = Field(\"Amount of automobile loan in INR\")\n",
- " personalLoans: int = Field(\"Amount of personal loan in INR\")\n",
- " homeMortgages: int = Field(\"Amount of home loan in INR\")\n",
- " smallBusinessLoans: int = Field(\"Amount of business loans in INR\")\n",
- " medicalDebt: int = Field(\"Amount of medical loan in INR\")\n",
- "\n",
- " # salary status\n",
- " salaryStatus: str = Field(\"\"\"Salary status of the customer, it should one of these:\n",
- " \"salaried\",\n",
- " \"unemployed\",\n",
- " \"selfEmployed\",\n",
- " \"contract\",\n",
- " \"intern\",\n",
- " \"retired\"\n",
- " \"\"\")\n",
- "\n",
- " # financial goals\n",
- " financical_goal: str = Field(\"\"\"\n",
- " A comma seperated list of financial goals the customer would like to persue in the future.\n",
- " Try to make it consistent with customers backstory. Following are the possible values:\n",
- " \n",
- " \"educationFunding\"\n",
- " \"startingInvestments\"\n",
- " \"smallPurchases\"\n",
- " \"debtRepayment\"\n",
- " \"buildingEmergencyFund\"\n",
- " \"firstCarPurchase\"\n",
- " \"homeDownPayment\"\n",
- " \"wealthBuilding\"\n",
- " \"insurance\"\n",
- " \"retirementPlanning\"\n",
- " \"childrenEducation\"\n",
- " \"homePurchase\"\n",
- " \"investmentDiversification\"\n",
- " \"taxPlanning\"\n",
- " \"emergencyFundExpansion\"\n",
- " \"retirementFundGrowth\"\n",
- " \"homeRenovation\"\n",
- " \"longTermInvestments\"\n",
- " \"childrenHigherEducation\"\n",
- " \"healthAndLifeInsurance\"\n",
- " \"philanthropy\"\n",
- " \"retirementIncomeManagement\"\n",
- " \"travelAndLeisure\"\n",
- " \"sustainableIncome\"\n",
- " \"lifestyleMaintenance\"\n",
- " \"legacyBuilding\"\n",
- " \"giftingAndDonations\"\n",
- " \"assistedLiving\"\n",
- " \"wealthDistribution\"\n",
- " \"\"\")\n",
- "\n",
- " # life events\n",
- " life_events: str = Field(\"\"\"\n",
- " A comma seperated list of life events that happened in life\n",
- " Try to make it consistent with customers backstory. Following are the possible values:\n",
- "\n",
- " \"graduation\"\n",
- " \"higherEducationEnrollment\"\n",
- " \"firstJob\"\n",
- " \"jobPromotions\"\n",
- " \"firstVehiclePurchase\"\n",
- " \"homePurchase\"\n",
- " \"marriage\"\n",
- " \"jointFinancialPlanning\"\n",
- " \"childbirth\"\n",
- " \"jobLoss\"\n",
- " \"careerTransition\"\n",
- " \"entrepreneurship\"\n",
- " \"seriousIllness\"\n",
- " \"disability\"\n",
- " \"receivingAnInheritance\"\n",
- " \"divorce\"\n",
- " \"retirement\"\n",
- " \"widowhood\"\n",
- " \"largeUnexpectedExpenses\"\n",
- " \"homeRepairs\"\n",
- " \"legalIssues\"\n",
- " \"relocation\"\n",
- " \"movingToANewCityCountry\"\n",
- " \"buyingASecondHome\"\n",
- " \"philanthropy\"\n",
- " \"largeDonations\"\n",
- " \"\"\")\n",
- "\n",
- "\n",
- " backstory: str = Field(\"\"\" \n",
- " A backstory of the user in less than 300 words, about the persons career, education and up and downs in their life.\n",
- " Be creative in building the backstory and try to make it different for everyone\n",
- " \"\"\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 2. Define the SQL Insert Tool\n",
- "\n",
- "Next we create a tool which can do the following things:\n",
- "\n",
- "1. Take in customer list as input\n",
- "2. Create an SQLite db, and a customer table if they dont exist already\n",
- "3. Insert the customers into the table\n",
- "\n",
- "This tool will be used by our agent to store customer data"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 42,
- "metadata": {},
- "outputs": [],
- "source": [
- "class SQLLiteInsertInput(BaseModel):\n",
- " customers: List[Customer] = Field(description=\"The list of banking customers to be saved\")\n",
- "\n",
- "class SQLLiteInsertTool(BaseTool):\n",
- " name = \"sql_insert_tool\"\n",
- " description = \"useful for inserting customer profile to sqllite db\"\n",
- " args_schema: Type[BaseModel] = SQLLiteInsertInput\n",
- "\n",
- " def _run(\n",
- " self, customers: List[Customer]\n",
- " ) -> str:\n",
- " try:\n",
- " db = SqliteDatabase('population.sql')\n",
- " class Customers(Model):\n",
- " name = CharField(max_length=255)\n",
- " age = IntegerField()\n",
- " income = IntegerField()\n",
- "\n",
- " # assets\n",
- " cash = IntegerField()\n",
- " equities = IntegerField()\n",
- " realEstate = IntegerField()\n",
- " retirementAccounts = IntegerField()\n",
- " commodities = IntegerField()\n",
- " alternativeInvestments = IntegerField()\n",
- " cars = IntegerField()\n",
- " bikes = IntegerField()\n",
- " insurance = IntegerField()\n",
- "\n",
- " # debts\n",
- " studentLoans = IntegerField()\n",
- " creditCardDebt = IntegerField()\n",
- " autoLoans = IntegerField()\n",
- " personalLoans = IntegerField()\n",
- " homeMortgages = IntegerField()\n",
- " smallBusinessLoans = IntegerField()\n",
- " medicalDebt = IntegerField()\n",
- "\n",
- " # salary status\n",
- " salaryStatus = CharField(max_length=255)\n",
- "\n",
- " # financial goals\n",
- " financical_goal = TextField()\n",
- "\n",
- " # life events\n",
- " life_events = TextField()\n",
- "\n",
- " # backstory\n",
- " backstory = TextField()\n",
- "\n",
- " class Meta:\n",
- " database = db\n",
- "\n",
- " db.connect()\n",
- " db.create_tables([Customers])\n",
- "\n",
- " with db.atomic():\n",
- " [Customers.create(**x.dict()) for x in customers]\n",
- " \n",
- " except Exception as error:\n",
- " print (f\"Failed to insert data into sqlite: {error}\")\n",
- " return f\"Insert failed with error, {error}\"\n",
- " finally:\n",
- " db.close()\n",
- " print(\"The SQLite connection is closed\")\n",
- " return \"Insert sucess\"\n",
- " \n",
- " async def _arun(\n",
- " self, email: str\n",
- " ) -> str:\n",
- " raise Exception(\"Not implemented\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 3. Create SQLite query tool\n",
- "\n",
- "Next we create an SQLite Query tool which does the follows:\n",
- "\n",
- "1. Take query string as input\n",
- "2. Connect to the customers table and run the query and return the result."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 43,
- "metadata": {},
- "outputs": [],
- "source": [
- "class SQLLiteQueryInput(BaseModel):\n",
- " query: str = Field(description=\"Valid SQLite query for fetching customer information\")\n",
- "\n",
- "class SQLLiteQueryTool(BaseTool):\n",
- " name = \"sql_query_tool\"\n",
- " description = \"useful for querying customer data from the sqlite db\"\n",
- " args_schema: Type[BaseModel] = SQLLiteQueryInput\n",
- "\n",
- " def _run(\n",
- " self, query: str\n",
- " ) -> str:\n",
- " db = SqliteDatabase('population.sql')\n",
- " db.connect()\n",
- "\n",
- " results = db.execute_sql(query).fetchall()\n",
- " result_str = \"Query Results:\\n\"\n",
- " for row in results:\n",
- " result_str += f\" - {row}\\n\"\n",
- " return result_str\n",
- "\n",
- " async def _arun(\n",
- " self, email: str\n",
- " ) -> str:\n",
- " raise Exception(\"Not implemented\") "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 4. Set up the LLM\n",
- "Ready the LLM to run the agent flo with.\n",
- "You are free to change this to your open ai backend by changing this to ChatOpenAI"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 44,
- "metadata": {},
- "outputs": [],
- "source": [
- "from langchain_openai import AzureChatOpenAI\n",
- "from flo_ai import FloSession\n",
- "import os\n",
- "\n",
- "llm = AzureChatOpenAI(\n",
- " azure_endpoint=os.getenv(\"AZURE_GPT4_ENDPOINT\"),\n",
- " model_name=\"gpt-4\",\n",
- " temperature=0.9,\n",
- " max_tokens=4096,\n",
- " api_version=\"2023-03-15-preview\",\n",
- " api_key=os.getenv(\"AZURE_OPEN_AI_API_KEY\")\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 5. Setup customer population generator agent\n",
- "\n",
- "This will be a agentic team. The team will have 2 agents:\n",
- "\n",
- "Agent 1: An agent to generate the list of customers and with the ability to save the sqllite\n",
- "\n",
- "Agent 2: An agent that can query the sqlite db to understand the customer demographics and distribution\n",
- "\n",
- "We use these two agents to work together to generate desired number of customers"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 45,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 45,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "agent_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloRoutedTeam\n",
- "name: population-generator\n",
- "team:\n",
- " name: PopulationGeneratorTeam\n",
- " router:\n",
- " name: PopulatioGenerationSupervisor\n",
- " kind: supervisor\n",
- " agents:\n",
- " - name: CustomerGenerator\n",
- " job: You job is to generate a list of bank customers with name and age, then save it to sqlite db.\n",
- " tools:\n",
- " - name: SQLLiteInsertTool\n",
- " - name: CustomerDataFetcher\n",
- " job: >\n",
- " Your job is to build and run queries to answer the questions asked.\n",
- "\n",
- " Here is the customer peewee model, the name of the table is \"customers\":\n",
- "\n",
- " name = CharField(max_length=255)\n",
- " age = IntegerField()\n",
- " income = IntegerField()\n",
- " cash = IntegerField()\n",
- " equities = IntegerField()\n",
- " realEstate = IntegerField()\n",
- " retirementAccounts = IntegerField()\n",
- " commodities = IntegerField()\n",
- " alternativeInvestments = IntegerField()\n",
- " cars = IntegerField()\n",
- " bikes = IntegerField()\n",
- " insurance = IntegerField()\n",
- " studentLoans = IntegerField()\n",
- " creditCardDebt = IntegerField()\n",
- " autoLoans = IntegerField()\n",
- " personalLoans = IntegerField()\n",
- " homeMortgages = IntegerField()\n",
- " smallBusinessLoans = IntegerField()\n",
- " medicalDebt = IntegerField()\n",
- " salaryStatus = CharField(max_length=255)\n",
- " financical_goal = TextField()\n",
- " life_events = TextField()\n",
- " backstory = TextField()\n",
- " \n",
- " tools:\n",
- " - name: SQLLiteQueryTool\n",
- "\"\"\"\n",
- "\n",
- "session = FloSession(llm)\n",
- "\n",
- "session.register_tool(\n",
- " name=\"SQLLiteInsertTool\", \n",
- " tool=SQLLiteInsertTool()\n",
- ").register_tool(\n",
- " name=\"SQLLiteQueryTool\", \n",
- " tool=SQLLiteQueryTool()\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## 6. Build and run\n",
- "\n",
- "Use the flo builder to build and run the flo to generate the customers."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 46,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "{'PopulatioGenerationSupervisor-t6t7S': {'next': 'CustomerGenerator-3lHy7'}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3mI will start by creating the first batch of 10 customer personas representing a diverse range of demographics and financial profiles akin to a large bank's clientele in India. These will include various income brackets and employment statuses. After creating and reviewing this first batch, I'll proceed with the second set of 10 customer personas. Let's start with the first 10:\n",
- "\n",
- "### First Batch of Customer Personas:\n",
- "\n",
- "1. **Name**: Amit Sharma, **Age**: 35\n",
- " - **Income**: 1200000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 50000\n",
- " - Equities: 200000\n",
- " - Real Estate: 5000000\n",
- " - Retirement Accounts: 300000\n",
- " - Commodities: 10000\n",
- " - Alternative Investments: 50000\n",
- " - Cars: 600000\n",
- " - Bikes: 150000\n",
- " - Insurance: 100000\n",
- " - Student Loans: 0\n",
- " - Credit Card Debt: 10000\n",
- " - Auto Loans: 200000\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 4000000\n",
- " - Small Business Loans: 0\n",
- " - Medical Debt: 0\n",
- " - Salary Status: \"regular\"\n",
- " - Financial Goal: \"Save for retirement\"\n",
- " - Life Events: \"Married, two children\"\n",
- " - Backstory: \"A corporate manager in an IT firm, focusing on investments for a secure future.\"\n",
- "\n",
- "2. **Name**: Priya Rajan, **Age**: 28\n",
- " - **Income**: 600000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 20000\n",
- " - Equities: 50000\n",
- " - Real Estate: 3200000\n",
- " - Retirement Accounts: 100000\n",
- " - Commodities: 5000\n",
- " - Alternative Investments: 10000\n",
- " - Cars: 300000\n",
- " - Insurance: 50000\n",
- " - Credit Card Debt: 40000\n",
- " - Personal Loans: 50000\n",
- " - Home Mortgages: 2500000\n",
- " - Salary Status: \"regular\"\n",
- " - Financial Goal: \"Buy a new house\"\n",
- " - Life Events: \"Recently married\"\n",
- " - Backstory: \"A rising entrepreneur, looking to settle in a new home with her spouse.\"\n",
- "\n",
- "3. **Name**: Rohit Bansal, **Age**: 45\n",
- " - **Income**: 1800000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 100000\n",
- " - Equities: 600000\n",
- " - Real Estate: 8000000\n",
- " - Retirement Accounts: 500000\n",
- " - Commodities: 20000\n",
- " - Alternative Investments: 150000\n",
- " - Cars: 1000000\n",
- " - Bikes: 200000\n",
- " - Insurance: 150000\n",
- " - Credit Card Debt: 50000\n",
- " - Auto Loans: 0\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 5000000\n",
- " - Salary Status: \"high\"\n",
- " - Financial Goal: \"Expand business operations\"\n",
- " - Life Events: \"Business expansion\"\n",
- " - Backstory: \"Owns a manufacturing firm and is looking to expand operations overseas.\"\n",
- "\n",
- "...and so on for a diverse set of personas including various age groups, financial situations, and life stages, up to 10 personas for the first batch.\n",
- "\n",
- "### Inserting to Database\n",
- "Once the ten profiles are designed, I will make use of the `sql_insert_tool` to insert these personas into the database.\n",
- "\n",
- "### Creating Second Batch\n",
- "After reviewing the distribution from the first batch, the second batch of 10 personas will be created to fill in any gaps and better mimic the customer distribution, ensuring a representation across different demographic and financial backgrounds.\n",
- "\n",
- "Shall I proceed with designing the remaining profiles and initiating the database insertion?\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'CustomerGenerator-3lHy7': {'messages': [HumanMessage(content='I will start by creating the first batch of 10 customer personas representing a diverse range of demographics and financial profiles akin to a large bank\\'s clientele in India. These will include various income brackets and employment statuses. After creating and reviewing this first batch, I\\'ll proceed with the second set of 10 customer personas. Let\\'s start with the first 10:\\n\\n### First Batch of Customer Personas:\\n\\n1. **Name**: Amit Sharma, **Age**: 35\\n - **Income**: 1200000 (Annual)\\n - **Financial Profile**:\\n - Cash: 50000\\n - Equities: 200000\\n - Real Estate: 5000000\\n - Retirement Accounts: 300000\\n - Commodities: 10000\\n - Alternative Investments: 50000\\n - Cars: 600000\\n - Bikes: 150000\\n - Insurance: 100000\\n - Student Loans: 0\\n - Credit Card Debt: 10000\\n - Auto Loans: 200000\\n - Personal Loans: 0\\n - Home Mortgages: 4000000\\n - Small Business Loans: 0\\n - Medical Debt: 0\\n - Salary Status: \"regular\"\\n - Financial Goal: \"Save for retirement\"\\n - Life Events: \"Married, two children\"\\n - Backstory: \"A corporate manager in an IT firm, focusing on investments for a secure future.\"\\n\\n2. **Name**: Priya Rajan, **Age**: 28\\n - **Income**: 600000 (Annual)\\n - **Financial Profile**:\\n - Cash: 20000\\n - Equities: 50000\\n - Real Estate: 3200000\\n - Retirement Accounts: 100000\\n - Commodities: 5000\\n - Alternative Investments: 10000\\n - Cars: 300000\\n - Insurance: 50000\\n - Credit Card Debt: 40000\\n - Personal Loans: 50000\\n - Home Mortgages: 2500000\\n - Salary Status: \"regular\"\\n - Financial Goal: \"Buy a new house\"\\n - Life Events: \"Recently married\"\\n - Backstory: \"A rising entrepreneur, looking to settle in a new home with her spouse.\"\\n\\n3. **Name**: Rohit Bansal, **Age**: 45\\n - **Income**: 1800000 (Annual)\\n - **Financial Profile**:\\n - Cash: 100000\\n - Equities: 600000\\n - Real Estate: 8000000\\n - Retirement Accounts: 500000\\n - Commodities: 20000\\n - Alternative Investments: 150000\\n - Cars: 1000000\\n - Bikes: 200000\\n - Insurance: 150000\\n - Credit Card Debt: 50000\\n - Auto Loans: 0\\n - Personal Loans: 0\\n - Home Mortgages: 5000000\\n - Salary Status: \"high\"\\n - Financial Goal: \"Expand business operations\"\\n - Life Events: \"Business expansion\"\\n - Backstory: \"Owns a manufacturing firm and is looking to expand operations overseas.\"\\n\\n...and so on for a diverse set of personas including various age groups, financial situations, and life stages, up to 10 personas for the first batch.\\n\\n### Inserting to Database\\nOnce the ten profiles are designed, I will make use of the `sql_insert_tool` to insert these personas into the database.\\n\\n### Creating Second Batch\\nAfter reviewing the distribution from the first batch, the second batch of 10 personas will be created to fill in any gaps and better mimic the customer distribution, ensuring a representation across different demographic and financial backgrounds.\\n\\nShall I proceed with designing the remaining profiles and initiating the database insertion?', name='CustomerGenerator-3lHy7')]}}\n",
- "----\n",
- "{'PopulatioGenerationSupervisor-t6t7S': {'next': 'CustomerGenerator-3lHy7'}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3mYes, please proceed with designing the remaining profiles for the first batch, include them along with the initial three profiles you have outlined, and initiate the database insertion. Afterwards, you can start creating the second batch of 10 customer personas.\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'CustomerGenerator-3lHy7': {'messages': [HumanMessage(content='Yes, please proceed with designing the remaining profiles for the first batch, include them along with the initial three profiles you have outlined, and initiate the database insertion. Afterwards, you can start creating the second batch of 10 customer personas.', name='CustomerGenerator-3lHy7')]}}\n",
- "----\n",
- "{'PopulatioGenerationSupervisor-t6t7S': {'next': 'CustomerGenerator-3lHy7'}}\n",
- "----\n",
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `sql_insert_tool` with `{'customers': [{'name': 'Amit Sharma', 'age': 35, 'income': 1200000, 'cash': 50000, 'equities': 200000, 'realEstate': 5000000, 'retirementAccounts': 300000, 'commodities': 10000, 'alternativeInvestments': 50000, 'cars': 600000, 'bikes': 150000, 'insurance': 100000, 'studentLoans': 0, 'creditCardDebt': 10000, 'autoLoans': 200000, 'personalLoans': 0, 'homeMortgages': 4000000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'regular', 'financical_goal': 'Save for retirement', 'life_events': 'Married, two children', 'backstory': 'A corporate manager in an IT firm, focusing on investments for a secure future.'}, {'name': 'Priya Rajan', 'age': 28, 'income': 600000, 'cash': 20000, 'equities': 50000, 'realEstate': 3200000, 'retirementAccounts': 100000, 'commodities': 5000, 'alternativeInvestments': 10000, 'cars': 300000, 'insurance': 50000, 'studentLoans': 0, 'creditCardDebt': 40000, 'autoLoans': 0, 'personalLoans': 50000, 'homeMortgages': 2500000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'regular', 'financical_goal': 'Buy a new house', 'life_events': 'Recently married', 'backstory': 'A rising entrepreneur, looking to settle in a new home with her spouse.'}, {'name': 'Rohit Bansal', 'age': 45, 'income': 1800000, 'cash': 100000, 'equities': 600000, 'realEstate': 8000000, 'retirementAccounts': 500000, 'commodities': 20000, 'alternativeInvestments': 150000, 'cars': 1000000, 'bikes': 200000, 'insurance': 150000, 'studentLoans': 0, 'creditCardDebt': 50000, 'autoLoans': 0, 'personalLoans': 0, 'homeMortgages': 5000000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'high', 'financical_goal': 'Expand business operations', 'life_events': 'Business expansion', 'backstory': 'Owns a manufacturing firm and is looking to expand operations overseas.'}, {'name': 'Sunita Iyer', 'age': 32, 'income': 300000, 'cash': 10000, 'equities': 30000, 'realEstate': 0, 'retirementAccounts': 5000, 'commodities': 3000, 'alternativeInvestments': 0, 'cars': 0, 'insurance': 30000, 'studentLoans': 50000, 'creditCardDebt': 20000, 'autoLoans': 0, 'personalLoans': 30000, 'homeMortgages': 0, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'irregular', 'financical_goal': 'Debt clearance', 'life_events': 'Starting a family', 'backstory': 'A freelance graphic designer, working on securing financial stability.'}, {'name': 'Anil Kumar', 'age': 53, 'income': 2400000, 'cash': 200000, 'equities': 1000000, 'realEstate': 12000000, 'retirementAccounts': 800000, 'commodities': 50000, 'alternativeInvestments': 300000, 'cars': 1500000, 'insurance': 200000, 'studentLoans': 0, 'creditCardDebt': 0, 'autoLoans': 0, 'personalLoans': 0, 'homeMortgages': 7000000, 'smallBusinessLoans': 300000, 'medicalDebt': 0, 'salaryStatus': 'high', 'financical_goal': 'Maintain a luxury lifestyle', 'life_events': 'Children studying abroad', 'backstory': 'A seasoned real estate developer enjoying the benefits of long-term investments.'}, {'name': 'Deepa Mehta', 'age': 24, 'income': 360000, 'cash': 15000, 'equities': 10000, 'realEstate': 0, 'retirementAccounts': 0, 'commodities': 2000, 'alternativeInvestments': 0, 'cars': 80000, 'bikes': 30000, 'insurance': 10000, 'studentLoans': 150000, 'creditCardDebt': 30000, 'autoLoans': 70000, 'personalLoans': 0, 'homeMortgages': 0, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'regular', 'financical_goal': 'Get higher education', 'life_events': \"Applying for master's programs\", 'backstory': 'A young professional with ambitions of furthering her education in management.'}, {'name': 'Vijay Singh', 'age': 40, 'income': 900000, 'cash': 50000, 'equities': 150000, 'realEstate': 4000000, 'retirementAccounts': 250000, 'commodities': 15000, 'alternativeInvestments': 70000, 'cars': 500000, 'bikes': 100000, 'insurance': 80000, 'studentLoans': 0, 'creditCardDebt': 25000, 'autoLoans': 0, 'personalLoans': 100000, 'homeMortgages': 3000000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'regular', 'financical_goal': 'Establish a new startup', 'life_events': 'Exploring new business ventures', 'backstory': 'An IT consultant looking to transition into entrepreneurship with a tech startup.'}, {'name': 'Geeta Choudhary', 'age': 38, 'income': 480000, 'cash': 25000, 'equities': 40000, 'realEstate': 1500000, 'retirementAccounts': 70000, 'commodities': 8000, 'alternativeInvestments': 25000, 'cars': 250000, 'insurance': 60000, 'studentLoans': 0, 'creditCardDebt': 15000, 'autoLoans': 100000, 'personalLoans': 50000, 'homeMortgages': 1000000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'part-time', 'financical_goal': 'Education funding for children', 'life_events': 'Managing work-life balance', 'backstory': 'A part-time teacher and full-time mother aiming to provide the best education for her children.'}, {'name': 'Mohan Das', 'age': 26, 'income': 200000, 'cash': 7000, 'equities': 5000, 'realEstate': 0, 'retirementAccounts': 0, 'commodities': 1000, 'alternativeInvestments': 0, 'cars': 0, 'bikes': 40000, 'insurance': 20000, 'studentLoans': 80000, 'creditCardDebt': 10000, 'autoLoans': 50000, 'personalLoans': 20000, 'homeMortgages': 0, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'irregular', 'financical_goal': 'Start a small business', 'life_events': 'Recent graduate', 'backstory': 'A recent college graduate with dreams of starting his own cafe.'}, {'name': 'Lakshmi Patel', 'age': 30, 'income': 540000, 'cash': 30000, 'equities': 80000, 'realEstate': 2500000, 'retirementAccounts': 120000, 'commodities': 6000, 'alternativeInvestments': 20000, 'cars': 0, 'insurance': 50000, 'studentLoans': 0, 'creditCardDebt': 5000, 'autoLoans': 0, 'personalLoans': 0, 'homeMortgages': 2000000, 'smallBusinessLoans': 0, 'medicalDebt': 0, 'salaryStatus': 'regular', 'financical_goal': 'Invest in health and wellness', 'life_events': 'Fitness enthusiast', 'backstory': 'A gym owner and fitness trainer focused on expanding her business and personal health.'}]}`\n",
- "responded: ### Completion of First Batch of Customer Personas:\n",
- "\n",
- "4. **Name**: Sunita Iyer, **Age**: 32\n",
- " - **Income**: 300000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 10000\n",
- " - Equities: 30000\n",
- " - Real Estate: 0\n",
- " - Retirement Accounts: 5000\n",
- " - Commodities: 3000\n",
- " - Alternative Investments: 0\n",
- " - Cars: 0\n",
- " - Insurance: 30000\n",
- " - Student Loans: 50000\n",
- " - Credit Card Debt: 20000\n",
- " - Personal Loans: 30000\n",
- " - Home Mortgages: 0\n",
- " - Salary Status: \"irregular\"\n",
- " - Financial Goal: \"Debt clearance\"\n",
- " - Life Events: \"Starting a family\"\n",
- " - Backstory: \"A freelance graphic designer, working on securing financial stability.\"\n",
- "\n",
- "5. **Name**: Anil Kumar, **Age**: 53\n",
- " - **Income**: 2400000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 200000\n",
- " - Equities: 1000000\n",
- " - Real Estate: 12000000\n",
- " - Retirement Accounts: 800000\n",
- " - Commodities: 50000\n",
- " - Alternative Investments: 300000\n",
- " - Cars: 1500000\n",
- " - Insurance: 200000\n",
- " - Small Business Loans: 300000\n",
- " - Credit Card Debt: 0\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 7000000\n",
- " - Salary Status: \"high\"\n",
- " - Financial Goal: \"Maintain a luxury lifestyle\"\n",
- " - Life Events: \"Children studying abroad\"\n",
- " - Backstory: \"A seasoned real estate developer enjoying the benefits of long-term investments.\"\n",
- "\n",
- "6. **Name**: Deepa Mehta, **Age**: 24\n",
- " - **Income**: 360000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 15000\n",
- " - Equities: 10000\n",
- " - Real Estate: 0\n",
- " - Retirement Accounts: 0\n",
- " - Commodities: 2000\n",
- " - Alternative Investments: 0\n",
- " - Cars: 80000\n",
- " - Bikes: 30000\n",
- " - Insurance: 10000\n",
- " - Student Loans: 150000\n",
- " - Credit Card Debt: 30000\n",
- " - Auto Loans: 70000\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 0\n",
- " - Salary Status: \"regular\"\n",
- " - Financial Goal: \"Get higher education\"\n",
- " - Life Events: \"Applying for master's programs\"\n",
- " - Backstory: \"A young professional with ambitions of furthering her education in management.\"\n",
- "\n",
- "7. **Name**: Vijay Singh, **Age**: 40\n",
- " - **Income**: 900000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 50000\n",
- " - Equities: 150000\n",
- " - Real Estate: 4000000\n",
- " - Retirement Accounts: 250000\n",
- " - Commodities: 15000\n",
- " - Alternative Investments: 70000\n",
- " - Cars: 500000\n",
- " - Bikes: 100000\n",
- " - Insurance: 80000\n",
- " - Credit Card Debt: 25000\n",
- " - Auto Loans: 0\n",
- " - Personal Loans: 100000\n",
- " - Home Mortgages: 3000000\n",
- " - Salary Status: \"regular\"\n",
- " - Financial Goal: \"Establish a new startup\"\n",
- " - Life Events: \"Exploring new business ventures\"\n",
- " - Backstory: \"An IT consultant looking to transition into entrepreneurship with a tech startup.\"\n",
- "\n",
- "8. **Name**: Geeta Choudhary, **Age**: 38\n",
- " - **Income**: 480000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 25000\n",
- " - Equities: 40000\n",
- " - Real Estate: 1500000\n",
- " - Retirement Accounts: 70000\n",
- " - Commodities: 8000\n",
- " - Alternative Investments: 25000\n",
- " - Cars: 250000\n",
- " - Insurance: 60000\n",
- " - Student Loans: 0\n",
- " - Credit Card Debt: 15000\n",
- " - Auto Loans: 100000\n",
- " - Personal Loans: 50000\n",
- " - Home Mortgages: 1000000\n",
- " - Salary Status: \"part-time\"\n",
- " - Financial Goal: \"Education funding for children\"\n",
- " - Life Events: \"Managing work-life balance\"\n",
- " - Backstory: \"A part-time teacher and full-time mother aiming to provide the best education for her children.\"\n",
- "\n",
- "9. **Name**: Mohan Das, **Age**: 26\n",
- " - **Income**: 200000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 7000\n",
- " - Equities: 5000\n",
- " - Real Estate: 0\n",
- " - Retirement Accounts: 0\n",
- " - Commodities: 1000\n",
- " - Alternative Investments: 0\n",
- " - Cars: 0\n",
- " - Bikes: 40000\n",
- " - Insurance: 20000\n",
- " - Student Loans: 80000\n",
- " - Credit Card Debt: 10000\n",
- " - Auto Loans: 50000\n",
- " - Personal Loans: 20000\n",
- " - Home Mortgages: 0\n",
- " - Salary Status: \"irregular\"\n",
- " - Financial Goal: \"Start a small business\"\n",
- " - Life Events: \"Recent graduate\"\n",
- " - Backstory: \"A recent college graduate with dreams of starting his own cafe.\"\n",
- "\n",
- "10. **Name**: Lakshmi Patel, **Age**: 30\n",
- " - **Income**: 540000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 30000\n",
- " - Equities: 80000\n",
- " - Real Estate: 2500000\n",
- " - Retirement Accounts: 120000\n",
- " - Commodities: 6000\n",
- " - Alternative Investments: 20000\n",
- " - Cars: 0\n",
- " - Insurance: 50000\n",
- " - Student Loans: 0\n",
- " - Credit Card Debt: 5000\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 2000000\n",
- " - Salary Status: \"regular\"\n",
- " - Financial Goal: \"Invest in health and wellness\"\n",
- " - Life Events: \"Fitness enthusiast\"\n",
- " - Backstory: \"A gym owner and fitness trainer focused on expanding her business and personal health.\"\n",
- "\n",
- "### Inserting First Batch to Database\n",
- "I will now utilize the `sql_insert_tool` to insert these first 10 customer personas into the database.\n",
- "\n",
- "\u001b[0mThe SQLite connection is closed\n",
- "\u001b[36;1m\u001b[1;3mInsert sucess\u001b[0m\u001b[32;1m\u001b[1;3mThe first batch of 10 customer personas has been successfully inserted into the database.\n",
- "\n",
- "### Creating Second Batch of Customer Personas\n",
- "\n",
- "Now, let's proceed with creating the second batch of 10 customer personas. This batch will fill in any gaps from the first batch and aim for an even more accurate representation of the typical customer distribution at a large bank in India.\n",
- "\n",
- "### Second Batch of Customer Personas:\n",
- "\n",
- "11. **Name**: Nisha Gupta, **Age**: 22\n",
- " - **Income**: 180000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 8000\n",
- " - Equities: 0\n",
- " - Real Estate: 0\n",
- " - Retirement Accounts: 0\n",
- " - Commodities: 0\n",
- " - Alternative Investments: 0\n",
- " - Cars: 0\n",
- " - Bikes: 20000\n",
- " - Insurance: 15000\n",
- " - Student Loans: 120000\n",
- " - Credit Card Debt: 15000\n",
- " - Auto Loans: 0\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 0\n",
- " - Salary Status: \"part-time\"\n",
- " - Financial Goal: \"Education completion\"\n",
- " - Life Events: \"College student\"\n",
- " - Backstory: \"A part-time retail worker and full-time student studying commerce.\"\n",
- "\n",
- "12. **Name**: Rajesh Singh, **Age**: 57\n",
- " - **Income**: 1500000 (Annual)\n",
- " - **Financial Profile**:\n",
- " - Cash: 500000\n",
- " - Equities: 800000\n",
- " - Real Estate: 10000000\n",
- " - Retirement Accounts: 1200000\n",
- " - Commodities: 100000\n",
- " - Alternative Investments: 200000\n",
- " - Cars: 800000\n",
- " - Insurance: 200000\n",
- " - Student Loans: 0\n",
- " - Credit Card Debt: 10000\n",
- " - Auto Loans: 0\n",
- " - Personal Loans: 0\n",
- " - Home Mortgages: 2000000\n",
- " - Salary Status: \"retired\"\n",
- " - Financial Goal: \"Secure retirement\"\n",
- " - Life Events: \"Preparation for retirement\"\n",
- " - Backstory: \"A retired government officer, focusing on managing investments post-retirement.\"\n",
- "\n",
- "...and so on until reaching a total of 20 personas.\n",
- "\n",
- "Shall I proceed with designing and including the remaining customer personas for the second batch and complete the insertion to the database?\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'CustomerGenerator-3lHy7': {'messages': [HumanMessage(content='The first batch of 10 customer personas has been successfully inserted into the database.\\n\\n### Creating Second Batch of Customer Personas\\n\\nNow, let\\'s proceed with creating the second batch of 10 customer personas. This batch will fill in any gaps from the first batch and aim for an even more accurate representation of the typical customer distribution at a large bank in India.\\n\\n### Second Batch of Customer Personas:\\n\\n11. **Name**: Nisha Gupta, **Age**: 22\\n - **Income**: 180000 (Annual)\\n - **Financial Profile**:\\n - Cash: 8000\\n - Equities: 0\\n - Real Estate: 0\\n - Retirement Accounts: 0\\n - Commodities: 0\\n - Alternative Investments: 0\\n - Cars: 0\\n - Bikes: 20000\\n - Insurance: 15000\\n - Student Loans: 120000\\n - Credit Card Debt: 15000\\n - Auto Loans: 0\\n - Personal Loans: 0\\n - Home Mortgages: 0\\n - Salary Status: \"part-time\"\\n - Financial Goal: \"Education completion\"\\n - Life Events: \"College student\"\\n - Backstory: \"A part-time retail worker and full-time student studying commerce.\"\\n\\n12. **Name**: Rajesh Singh, **Age**: 57\\n - **Income**: 1500000 (Annual)\\n - **Financial Profile**:\\n - Cash: 500000\\n - Equities: 800000\\n - Real Estate: 10000000\\n - Retirement Accounts: 1200000\\n - Commodities: 100000\\n - Alternative Investments: 200000\\n - Cars: 800000\\n - Insurance: 200000\\n - Student Loans: 0\\n - Credit Card Debt: 10000\\n - Auto Loans: 0\\n - Personal Loans: 0\\n - Home Mortgages: 2000000\\n - Salary Status: \"retired\"\\n - Financial Goal: \"Secure retirement\"\\n - Life Events: \"Preparation for retirement\"\\n - Backstory: \"A retired government officer, focusing on managing investments post-retirement.\"\\n\\n...and so on until reaching a total of 20 personas.\\n\\nShall I proceed with designing and including the remaining customer personas for the second batch and complete the insertion to the database?', name='CustomerGenerator-3lHy7')]}}\n",
- "----\n",
- "{'PopulatioGenerationSupervisor-t6t7S': {'next': 'FINISH'}}\n",
- "----\n"
- ]
- }
- ],
- "source": [
- "from flo_ai import Flo\n",
- "\n",
- "flo: Flo = Flo.build(session, yaml=agent_yaml)\n",
- "for s in flo.stream(\"\"\"\n",
- "Create 10 banking customer personas whose demographics & financial profiles mimic the distribution of banking customers \n",
- "in a large bank in India, and insert them to db. Do note that these customer personas should represent all income brackets, employment statuses. \n",
- "After creating the first set of 10 personas, create another 10 profiles so that personas mimic a customer distribution even more accurately and insert it\n",
- "You can choose to create and insert as batch if that is best for you\n",
- "\"\"\"):\n",
- " if \"__end__\" not in s:\n",
- " print(s)\n",
- " print(\"----\")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Execution Phase\n",
- "\n",
- "This is the next phase we use these customers to generate lifevents which needs banking help and create an agent to simulate the same\n",
- "\n",
- "We are gonna create 3 functionalities the bank supports:\n",
- "\n",
- "1. Purchase items: Where the customer can purchase items from outside and the customer account balance comes down\n",
- "2. Send a customer support email\n",
- "3. Request for a loan"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 47,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "\n",
- "class Purchase(BaseModel):\n",
- " purchase_item: str = Field(\"The item purchase like TV, microwave, laptop etc\")\n",
- " purchase_price: int = Field(description=\"A payment amount made by the customer\")\n",
- "\n",
- "class PurchaseTool(BaseTool):\n",
- " name = \"transaction\"\n",
- " description = \"useful for when you want to by something\"\n",
- " args_schema: Type[BaseModel] = Purchase\n",
- "\n",
- " def _run(\n",
- " self, purchase_item: str, purchase_price: int\n",
- " ) -> str:\n",
- " print(f\"{purchase_item} @ {purchase_price}\")\n",
- " return \"Completed transaction successfully\"\n",
- "\n",
- " async def _arun(\n",
- " self, purchase_item: str, purchase_price: int\n",
- " ) -> str:\n",
- " print(f\"{purchase_item} @ {purchase_price}\")\n",
- " return \"Completed transaction successfully\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 48,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Optional, Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "\n",
- "class CustomerSupportInput(BaseModel):\n",
- " email: str = Field(description=\"The email text to be sent to customer support\")\n",
- "\n",
- "class CustomerSupportTool(BaseTool):\n",
- " name = \"customer_support_tool\"\n",
- " description = \"useful for when you need to send an email to customer support\"\n",
- " args_schema: Type[BaseModel] = CustomerSupportInput\n",
- "\n",
- " def _run(\n",
- " self, email: str\n",
- " ) -> str:\n",
- " print(email)\n",
- " return \"Email sent successfully\"\n",
- "\n",
- " async def _arun(\n",
- " self, email: str\n",
- " ) -> str:\n",
- " print(email)\n",
- " return \"Email sent successfully\""
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 49,
- "metadata": {},
- "outputs": [],
- "source": [
- "from typing import Type\n",
- "from pydantic import BaseModel, Field\n",
- "from langchain.tools import BaseTool\n",
- "\n",
- "class LoanRequest(BaseModel):\n",
- " request: str = Field(description=\"An message asking for a loan along with loan amount, loan type and reason for the loan\")\n",
- "\n",
- "class LoanRequestTool(BaseTool):\n",
- " name = \"loan_request\"\n",
- " description = \"useful for when you need a loan from the bank\"\n",
- " args_schema: Type[BaseModel] = LoanRequest\n",
- "\n",
- " def _run(\n",
- " self, request: str\n",
- " ) -> str:\n",
- " print(request)\n",
- " return \"Loan request successful\"\n",
- "\n",
- " async def _arun(\n",
- " self, request: str\n",
- " ) -> str:\n",
- " print(request)\n",
- " return \"Loan request successful\""
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Set up the customer simulator agent\n",
- "\n",
- "This is the agent which will mock customer life event"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 54,
- "metadata": {},
- "outputs": [
- {
- "data": {
- "text/plain": [
- ""
- ]
- },
- "execution_count": 54,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "execution_yaml = \"\"\"\n",
- "apiVersion: flo/alpha-v1\n",
- "kind: FloAgent\n",
- "name: banking-assistant\n",
- "agent:\n",
- " name: BankingCustomer\n",
- " job: >\n",
- " You have the capability to interact with the bank in different ways. Depending upon your need take the right actions\n",
- " tools:\n",
- " - name: PurchaseTool\n",
- " - name: LoanRequestTool\n",
- " - name: CustomerSupportTool\n",
- "\"\"\"\n",
- "\n",
- "from flo_ai import FloSession\n",
- "\n",
- "session = FloSession(llm)\n",
- "\n",
- "session.register_tool(\n",
- " name=\"PurchaseTool\", \n",
- " tool=PurchaseTool()\n",
- ").register_tool(\n",
- " name=\"LoanRequestTool\", \n",
- " tool=LoanRequestTool()\n",
- ").register_tool(\n",
- " name=\"CustomerSupportTool\",\n",
- " tool=CustomerSupportTool()\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Simulation\n",
- "\n",
- "We read each customer from the SQLite db and use the simulator to mock an event, which will in turn interact with a banking backend"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 58,
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "\n",
- "\n",
- "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
- "{'actions': [ToolAgentAction(tool='loan_request', tool_input={'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}, log=\"\\nInvoking: `loan_request` with `{'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}`\\nresponded: Customer Profile:\\n- Name: Sunita Iyer\\n- Age: 32\\n- Financial Goals: Debt clearance, Starting a family\\n- Occupation: Freelance Graphic Designer\\n- Income: Irregular\\n- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\\n- Debt: Has debts amounting to INR 50,000.\\n\\n**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\\n\\n**Ideal Banking Help:**\\n1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\\n2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\\n\\n**Action to be Taken:**\\nI'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\\n\\n**Loan Request Composition:**\\nI will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\\n\\nLet's proceed with drafting the loan request.\\n\\n\", message_log=[AIMessageChunk(content=\"Customer Profile:\\n- Name: Sunita Iyer\\n- Age: 32\\n- Financial Goals: Debt clearance, Starting a family\\n- Occupation: Freelance Graphic Designer\\n- Income: Irregular\\n- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\\n- Debt: Has debts amounting to INR 50,000.\\n\\n**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\\n\\n**Ideal Banking Help:**\\n1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\\n2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\\n\\n**Action to be Taken:**\\nI'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\\n\\n**Loan Request Composition:**\\nI will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\\n\\nLet's proceed with drafting the loan request.\", additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'function': {'arguments': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'name': 'loan_request'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4-turbo-2024-04-09', 'system_fingerprint': 'fp_e49e4201a9'}, id='run-a5932be0-a5ec-4f02-9fc9-a140d085410b', tool_calls=[{'name': 'loan_request', 'args': {'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'type': 'tool_call'}], tool_call_chunks=[{'name': 'loan_request', 'args': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'index': 0, 'type': 'tool_call_chunk'}])], tool_call_id='call_36Ee58XRhi2RX39RFT9JXxBp')], 'messages': [AIMessageChunk(content=\"Customer Profile:\\n- Name: Sunita Iyer\\n- Age: 32\\n- Financial Goals: Debt clearance, Starting a family\\n- Occupation: Freelance Graphic Designer\\n- Income: Irregular\\n- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\\n- Debt: Has debts amounting to INR 50,000.\\n\\n**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\\n\\n**Ideal Banking Help:**\\n1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\\n2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\\n\\n**Action to be Taken:**\\nI'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\\n\\n**Loan Request Composition:**\\nI will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\\n\\nLet's proceed with drafting the loan request.\", additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'function': {'arguments': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'name': 'loan_request'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4-turbo-2024-04-09', 'system_fingerprint': 'fp_e49e4201a9'}, id='run-a5932be0-a5ec-4f02-9fc9-a140d085410b', tool_calls=[{'name': 'loan_request', 'args': {'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'type': 'tool_call'}], tool_call_chunks=[{'name': 'loan_request', 'args': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'index': 0, 'type': 'tool_call_chunk'}])]}\n",
- "----\n",
- "\u001b[32;1m\u001b[1;3m\n",
- "Invoking: `loan_request` with `{'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}`\n",
- "responded: Customer Profile:\n",
- "- Name: Sunita Iyer\n",
- "- Age: 32\n",
- "- Financial Goals: Debt clearance, Starting a family\n",
- "- Occupation: Freelance Graphic Designer\n",
- "- Income: Irregular\n",
- "- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\n",
- "- Debt: Has debts amounting to INR 50,000.\n",
- "\n",
- "**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\n",
- "\n",
- "**Ideal Banking Help:**\n",
- "1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\n",
- "2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\n",
- "\n",
- "**Action to be Taken:**\n",
- "I'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\n",
- "\n",
- "**Loan Request Composition:**\n",
- "I will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\n",
- "\n",
- "Let's proceed with drafting the loan request.\n",
- "\n",
- "\u001b[0mI am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\n",
- "\u001b[33;1m\u001b[1;3mLoan request successful\u001b[0m{'steps': [AgentStep(action=ToolAgentAction(tool='loan_request', tool_input={'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}, log=\"\\nInvoking: `loan_request` with `{'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}`\\nresponded: Customer Profile:\\n- Name: Sunita Iyer\\n- Age: 32\\n- Financial Goals: Debt clearance, Starting a family\\n- Occupation: Freelance Graphic Designer\\n- Income: Irregular\\n- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\\n- Debt: Has debts amounting to INR 50,000.\\n\\n**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\\n\\n**Ideal Banking Help:**\\n1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\\n2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\\n\\n**Action to be Taken:**\\nI'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\\n\\n**Loan Request Composition:**\\nI will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\\n\\nLet's proceed with drafting the loan request.\\n\\n\", message_log=[AIMessageChunk(content=\"Customer Profile:\\n- Name: Sunita Iyer\\n- Age: 32\\n- Financial Goals: Debt clearance, Starting a family\\n- Occupation: Freelance Graphic Designer\\n- Income: Irregular\\n- Savings and Assets: Notably owns bikes valued at INR 30,000 with a total savings of INR 30,000 and expenses of INR 10,000 monthly.\\n- Debt: Has debts amounting to INR 50,000.\\n\\n**Scenario:** Sunita is trying to stabilize her financial standing as a freelance graphic designer. With irregular income patterns and a goal to clear her debts as well as start a family, she requires solid financial planning and potentially extra funds to manage both her debt and future family expenses.\\n\\n**Ideal Banking Help:**\\n1. Debt Consolidation Loan: To consolidate and pay off her existing debts.\\n2. Planning a Family Loan or Financial Support: Assistance for future family planning and related expenses.\\n\\n**Action to be Taken:**\\nI'll proceed with sending a loan request to the bank to help Sunita consolidate her debts, which could help her manage her monthly expenses better and also plan her family's future.\\n\\n**Loan Request Composition:**\\nI will request a consolidated loan for INR 50,000 to clear her debts and an additional loan for family planning expenses. This will be presented as a structured plan showing how it will enable her to stabilize her finances and secure her goals of starting a family.\\n\\nLet's proceed with drafting the loan request.\", additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'function': {'arguments': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'name': 'loan_request'}, 'type': 'function'}]}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'gpt-4-turbo-2024-04-09', 'system_fingerprint': 'fp_e49e4201a9'}, id='run-a5932be0-a5ec-4f02-9fc9-a140d085410b', tool_calls=[{'name': 'loan_request', 'args': {'request': 'I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.'}, 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'type': 'tool_call'}], tool_call_chunks=[{'name': 'loan_request', 'args': '{\"request\":\"I am Sunita Iyer, a freelance graphic designer, reaching out to request financial assistance to help stabilize my financial situation and support my upcoming goals. I am currently looking to consolidate my debts totaling INR 50,000 to streamline my finances and ease the burden of irregular income flows. Additionally, as I plan to start a family soon, I seek additional financial support to cover potential expenses related to this significant life event. A structured loan plan would greatly assist in managing my finances more efficiently and achieving my personal goals. I appreciate your consideration and look forward to a favorable response.\"}', 'id': 'call_36Ee58XRhi2RX39RFT9JXxBp', 'index': 0, 'type': 'tool_call_chunk'}])], tool_call_id='call_36Ee58XRhi2RX39RFT9JXxBp'), observation='Loan request successful')], 'messages': [FunctionMessage(content='Loan request successful', name='loan_request')]}\n",
- "----\n",
- "\u001b[32;1m\u001b[1;3mThe loan request for debt consolidation and additional financial support for family planning expenses has been successfully submitted. Sunita can expect a response from the bank regarding the details and conditions of the loan. This assistance should help her manage her finances more effectively and move closer to her goals of clearing debts and starting a family securely.\u001b[0m\n",
- "\n",
- "\u001b[1m> Finished chain.\u001b[0m\n",
- "{'output': 'The loan request for debt consolidation and additional financial support for family planning expenses has been successfully submitted. Sunita can expect a response from the bank regarding the details and conditions of the loan. This assistance should help her manage her finances more effectively and move closer to her goals of clearing debts and starting a family securely.', 'messages': [AIMessage(content='The loan request for debt consolidation and additional financial support for family planning expenses has been successfully submitted. Sunita can expect a response from the bank regarding the details and conditions of the loan. This assistance should help her manage her finances more effectively and move closer to her goals of clearing debts and starting a family securely.')]}\n",
- "----\n"
- ]
- },
- {
- "data": {
- "text/plain": [
- "True"
- ]
- },
- "execution_count": 58,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "from peewee import SqliteDatabase\n",
- "\n",
- "db = SqliteDatabase('population.sql')\n",
- "db.connect()\n",
- "\n",
- "results = db.execute_sql(\"SELECT * from customers\").fetchall()\n",
- "\n",
- "customer = results[3]\n",
- "\n",
- "pr = f\"\"\"\n",
- "Based on the customers backstory & financial goals, create a scenario where user might need a banking help, and take the required action\n",
- "\n",
- "Here are the customer details:\n",
- "{customer}\n",
- "\"\"\"\n",
- "\n",
- "from flo_ai import Flo\n",
- "\n",
- "flo: Flo = Flo.build(session, yaml=execution_yaml)\n",
- "for s in flo.stream(pr):\n",
- " if \"__end__\" not in s:\n",
- " print(s)\n",
- " print(\"----\")\n",
- "\n",
- "\n",
- "db.close()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Summary\n",
- "\n",
- "As you can use we generated a population of banking customers and used their profile to generate a backstory. Based on backstory and their financial goals as well as part life events, we created new life events which triggered banking workflows. \n",
- "\n",
- "This demonstrate the use of agentic AI for population testing of AI systems.\n",
- "\n",
- "### How can this be improved ?\n",
- "\n",
- "1. Add more checks to make sure that the distribution of generatec customers is consistent, meaning its in normal distribution\n",
- "2. Add a reflection layer in the generator agent team to make sure that enough records are generated"
- ]
- }
- ],
- "metadata": {
- "kernelspec": {
- "display_name": ".venv",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.11.6"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 2
-}
diff --git a/flo_ai/examples/python/delegator_example.py b/flo_ai/examples/python/delegator_example.py
deleted file mode 100644
index f7f2287c..00000000
--- a/flo_ai/examples/python/delegator_example.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from flo_ai.core import Flo
-from flo_ai import FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-load_dotenv()
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: adding-team
-team:
- name: EssayTeam
- agents:
- - name: EssayWriter
- kind: llm
- job: >
- You are an essay assistant tasked with writing excellent 300 words essay. Generate the best essay possible for the user's request.
- If the you are provided critique view, respond with a revised version of your previous attempts. A maximum of total 100 words
- - name: DelegatorAgent
- kind: delegator
- retry: 1
- to:
- - name: EssayWriter
- job: >
- You are a teacher grading an essay submission. Score the essay between 1 to 10, with 10 being perfect
- If the score is greater than 7 sent it to FinalEssayProducer
- else if its less than or equal to 7 sent it to EssayWriter with suggestions to change
- - name: FinalEssayProducer
- kind: llm
- job: >
- Generate the final assay to be returned to the user
- router:
- name: router
- kind: linear
-"""
-
-input_prompt = """
-Question: Write me an interesting blog about latest advancements in agentic AI by reasearching the internet
-"""
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-session = FloSession(llm).register_tool(
- name='TavilySearchResults', tool=TavilySearchResults()
-)
-
-flo: Flo = Flo.build(session, yaml=yaml_data)
-Flo.set_log_level('INFO')
-data = flo.invoke(input_prompt)
-print((data['messages'][-1]).content)
diff --git a/flo_ai/examples/python/hierarchical_blogging_team.py b/flo_ai/examples/python/hierarchical_blogging_team.py
deleted file mode 100644
index 07252624..00000000
--- a/flo_ai/examples/python/hierarchical_blogging_team.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from flo_ai.core import Flo
-from langchain_openai import ChatOpenAI
-from flo_ai import FloSession
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-load_dotenv()
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: blogging-team
-team:
- name: BloggingTeam
- router:
- name: parent-supervisor
- kind: supervisor
- subteams:
- - name: BlogResearchTeam
- router:
- name: bgsupervisor
- kind: supervisor
- agents:
- - name: Reasercher
- job: Do a research on the internet and find articles of relevent to the topic asked by the user, always try to find the latest information on the same
- tools:
- - name: TavilySearchResults
- - name: Blogger
- job: From the documents provider by the researcher write a blog of 300 words with can be readily published, make in engaging and add reference links to original blogs
- tools:
- - name: TavilySearchResults
- - name: BlogWritingTeam
- router:
- name: bwsupervisor
- kind: supervisor
- agents:
- - name: Figure
- job: Do somethinh
- tools:
- - name: TavilySearchResults
-"""
-
-input_prompt = """
-Question: Write me an interesting blog about latest advancements in agentic AI
-"""
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
-session = FloSession(llm).register_tool(
- name='TavilySearchResults', tool=TavilySearchResults()
-)
-flo: Flo = Flo.build(session, yaml=yaml_data)
diff --git a/flo_ai/examples/python/json_training_data_generation.py b/flo_ai/examples/python/json_training_data_generation.py
deleted file mode 100644
index ae26640a..00000000
--- a/flo_ai/examples/python/json_training_data_generation.py
+++ /dev/null
@@ -1,46 +0,0 @@
-import os
-from langchain_openai import ChatOpenAI
-from langchain_core.prompts import PromptTemplate
-from flo_ai.callbacks import FloExecutionLogger
-from flo_ai.storage.data_collector import JSONLFileCollector
-from flo_ai import Flo, FloSession
-from flo_ai.models.flo_agent import FloAgent
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-
-load_dotenv()
-
-file_collector = JSONLFileCollector('.logs')
-
-# Create a tool logger with the collector
-local_tracker = FloExecutionLogger(file_collector)
-# Create the LLM object
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-
-
-prompt = PromptTemplate.from_template('1 + {number} = ')
-
-chain = prompt | llm
-print(chain.invoke({'number': 2}))
-
-
-session = FloSession(llm)
-session.register_callback(local_tracker)
-
-os.environ['TAVILY_API_KEY'] = os.getenv('TAVILY_API_KEY')
-tavily_tool = TavilySearchResults()
-
-session.register_tool('thappal', tavily_tool)
-
-weather_agent = FloAgent.create(
- session=session,
- name='Blogger',
- job='You can research the internet and create a blog about the topic given by the user',
- tools=[tavily_tool],
-)
-
-
-agent_flo: Flo = Flo.create(session, weather_agent)
-
-print(agent_flo.invoke('Whats the whether in New Delhi, India ?'))
diff --git a/flo_ai/examples/python/linear_router_team.py b/flo_ai/examples/python/linear_router_team.py
deleted file mode 100644
index ba26eed2..00000000
--- a/flo_ai/examples/python/linear_router_team.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from flo_ai.core import Flo
-from langchain_openai import ChatOpenAI
-from flo_ai import FloSession
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-
-from typing import Optional, Type
-from pydantic import BaseModel, Field
-from langchain.tools import BaseTool
-from langchain.callbacks.manager import (
- AsyncCallbackManagerForToolRun,
- CallbackManagerForToolRun,
-)
-
-load_dotenv()
-
-
-class FetchTrxInput(BaseModel):
- reference_number: str = Field(description='The transaction reference number')
-
-
-class FetchTransactionTool(BaseTool):
- name = 'fetch_transactions'
- description = 'useful for when you want to fetch the transaction details given reference number'
- args_schema: Type[BaseModel] = FetchTrxInput
-
- def _run(
- self,
- reference_number: str,
- run_manager: Optional[CallbackManagerForToolRun] = None,
- ) -> str:
- return 'The transaction happened on 23/07/2024 IST and it failed because there was not enough balance in the account'
-
- async def _arun(
- self,
- reference_number: str,
- run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
- ) -> str:
- return 'The transaction happened on 23/07/2024 IST and it failed because there was not enough balance in the account'
-
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: data-processing
-team:
- name: DataProcessing
- router:
- name: data-processing-pipline
- kind: linear
- agents:
- - name: Reasercher
- job: Do a research on the internet and find articles of relevent to the topic asked by the user, always try to find the latest information on the same
- tools:
- - name: TavilySearchResults
- - name: Blogger
- job: From the documents provider by the researcher write a blog of 300 words with can be readily published, make in engaging and add reference links to original blogs
- tools:
- - name: TavilySearchResults
-"""
-
-input_prompt = """
-Question: Write me an interesting blog about latest advancements in agentic AI
-"""
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o')
-session = FloSession(llm).register_tool(
- name='TavilySearchResults', tool=TavilySearchResults()
-)
-flo: Flo = Flo.build(session, yaml=yaml_data)
-
-for event in flo.stream(input_prompt):
- for k, v in event.items():
- if k != '__end__':
- print(v)
diff --git a/flo_ai/examples/python/llm_extensibility.py b/flo_ai/examples/python/llm_extensibility.py
deleted file mode 100644
index 0b6a7ad1..00000000
--- a/flo_ai/examples/python/llm_extensibility.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from flo_ai import Flo
-from flo_ai import FloSession
-from pydantic import BaseModel, Field
-from langchain_openai import ChatOpenAI
-from flo_ai.tools.flo_tool import flotool
-from flo_ai.callbacks.flo_callbacks import flo_agent_callback, FloCallbackResponse
-
-from dotenv import load_dotenv
-import warnings
-
-load_dotenv()
-
-
-warnings.simplefilter('default', DeprecationWarning)
-
-gpt35 = ChatOpenAI(temperature=0, model_name='gpt-3.5-turbo')
-gpt_4o_mini = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-gpt_4o = ChatOpenAI(temperature=0, model_name='gpt-4o')
-session = FloSession(gpt35)
-
-session.register_model('bronze', gpt35)
-session.register_model('silver', gpt_4o_mini)
-session.register_model('gold', gpt_4o)
-
-
-class SendEmailInput(BaseModel):
- to: str = Field(
- description='Comma seperared list of users emails to which email needs to be sent'
- )
- message: str = Field(description='The email text to be sent')
-
-
-@flotool(
- 'email_triage',
- 'useful for when you need to send an email to someone',
- argument_contract=SendEmailInput,
-)
-def email_tool(to: str, message: str):
- return f'Email sent successfully to: {to}'
-
-
-@flo_agent_callback
-def agent_callback(response: FloCallbackResponse):
- print('------------- START AGENT CALLBACK -----------')
- print(response)
- print('------------- END AGENT CALLBACK -----------')
-
-
-session.register_tool('SendEmailTool', email_tool)
-session.register_callback(agent_callback)
-
-agent_yaml = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: invite-handler
-team:
- name: Personal-Assistant-Bot
- router:
- name: Personal-Assistant
- kind: supervisor
- model: silver
- agents:
- - name: EmailFriends
- job: You job is to send an invite to the christmas party at my house to my friends and friends only, not collegues, invite their spouses too. Keep the email warm and friendly.
- role: personal ai assistant
- model: bronze
- tools:
- - name: SendEmailTool
- - name: EmailColleagues
- job: You job is to send an invite to the christmas party at my house to my colleagues and not friends. Keep the email formal, and DO NOT invite the spouses.
- role: office ai assistant
- model: gold
- tools:
- - name: SendEmailTool
-"""
-input_prompt = """
-Here is the list of user emails and there relations to me
-
-vishnu@gmail.com / friend
-nk@gmail.com / friend
-jk@gmail.com / colleague
-ck@hotmail.com / friend
-hk@gmail.com / colleague
-jak@gmail.com / colleague
-ck@gmail.com / friend.
-
-Please invite these nice folks to my christmas party
-"""
-
-flo: Flo = Flo.build(session, yaml=agent_yaml)
-for s in flo.stream(input_prompt):
- if '__end__' not in s:
- print(s)
- print('----')
diff --git a/flo_ai/examples/python/output_parser.py b/flo_ai/examples/python/output_parser.py
deleted file mode 100644
index b59a9bfe..00000000
--- a/flo_ai/examples/python/output_parser.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from flo_ai import FloLLMAgent, FloSession, Flo
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-from langchain_openai import ChatOpenAI
-from flo_ai.parsers import FloJsonParser
-from flo_ai.state import FloJsonOutputCollector
-from flo_ai.callbacks import FloExecutionLogger
-from flo_ai.storage.data_collector import JSONLFileCollector
-
-load_dotenv()
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-
-session = FloSession(llm).register_tool(
- name='TavilySearchResults', tool=TavilySearchResults()
-)
-
-
-file_collector = JSONLFileCollector('.logs')
-
-local_tracker = FloExecutionLogger(file_collector)
-
-session.register_callback(local_tracker)
-
-format = {
- 'name': 'NameFormat',
- 'fields': [
- {
- 'type': 'str',
- 'description': 'The first name of the person',
- 'name': 'first_name',
- },
- {
- 'type': 'str',
- 'description': 'The middle name of the person',
- 'name': 'middle_name',
- },
- {
- 'type': 'literal',
- 'description': 'The last name of the person, the value can be either of Vishnu or Satis',
- 'name': 'last_name',
- 'values': [
- {'value': 'Vishnu', 'description': 'If the first_name starts with K'},
- {'value': 'Satis', 'description': 'If the first_name starts with M'},
- ],
- 'default_value_prompt': 'If none of the above value is suited, please use value other than the above in snake-case',
- },
- ],
-}
-
-dc = FloJsonOutputCollector()
-
-researcher = FloLLMAgent.create(
- session,
- name='Formatter',
- role='Output formatter',
- job='What is the first name, last name and middle name of the the person user asks about',
- parser=FloJsonParser.create(json_dict=format),
- data_collector=dc,
-)
-
-
-Flo.set_log_level('INFO')
-flo: Flo = Flo.create(session, researcher)
-result = flo.invoke('Mahatma Gandhi')
-
-print(result)
-print(dc.fetch())
diff --git a/flo_ai/examples/python/output_parser_yaml.py b/flo_ai/examples/python/output_parser_yaml.py
deleted file mode 100644
index 5043763c..00000000
--- a/flo_ai/examples/python/output_parser_yaml.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import os
-from flo_ai import FloSession, Flo
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-from langchain_openai import AzureChatOpenAI
-from flo_ai.state import FloJsonOutputCollector
-
-load_dotenv()
-llm = AzureChatOpenAI(
- azure_endpoint=os.getenv('AZURE_GPT4_ENDPOINT'),
- model_name='gpt-4o',
- temperature=0.2,
- max_tokens=4096,
- api_version='2024-08-01-preview',
- api_key=os.getenv('AZURE_OPEN_AI_API_KEY'),
-)
-
-session = FloSession(llm).register_tool(
- name='InternetSearchTool', tool=TavilySearchResults()
-)
-
-dc = FloJsonOutputCollector()
-
-session.register_output_collector('kv', dc)
-
-simple_reseacher = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- kind: agentic
- job: >
- Given the person name, guess the first and last name
- tools:
- - name: InternetSearchTool
- parser:
- name: NameFormatter
- fields:
- - type: str
- description: The first name of the person
- name: first_name
- - type: str
- description: The first name of the person
- name: last_name
- - name: location
- type: object
- description: The details about birth location
- fields:
- - name: state
- type: str
- description: The Indian State in whihc the person was born
- data_collector: kv
-"""
-
-flo: Flo = Flo.build(session, simple_reseacher)
-result = flo.invoke('Gandhi')
-
-print(dc.fetch())
diff --git a/flo_ai/examples/python/rag_tool.py b/flo_ai/examples/python/rag_tool.py
deleted file mode 100644
index d98bbb77..00000000
--- a/flo_ai/examples/python/rag_tool.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from flo_ai import Flo
-from flo_ai import FloSession
-from langchain_openai import ChatOpenAI, OpenAIEmbeddings
-from langchain_chroma import Chroma
-from langchain_community.document_loaders import TextLoader
-from langchain_community.embeddings.sentence_transformer import (
- SentenceTransformerEmbeddings,
-)
-from langchain_text_splitters import CharacterTextSplitter
-
-from dotenv import load_dotenv
-from flo_ai.retrievers.flo_retriever import FloRagBuilder
-from flo_ai.retrievers.flo_compression_pipeline import FloCompressionPipeline
-
-
-load_dotenv()
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-
-session = FloSession(llm, log_level='ERROR')
-
-# load the document and split it into chunks
-loader = TextLoader('./examples/data/rag_document.txt')
-documents = loader.load()
-
-# split it into chunks
-text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
-docs = text_splitter.split_documents(documents)
-
-# create the open-source embedding function
-embedding_function = SentenceTransformerEmbeddings(model_name='all-MiniLM-L6-v2')
-
-# load it into Chroma
-db = Chroma.from_documents(docs, embedding_function)
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-session = FloSession(llm)
-builder = FloRagBuilder(session, db.as_retriever())
-compression_pipeline = FloCompressionPipeline(
- OpenAIEmbeddings(model='text-embedding-3-small')
-)
-compression_pipeline.add_embedding_reduntant_filter()
-compression_pipeline.add_embedding_relevant_filter()
-# Reranking
-
-retriever_tool = builder.with_compression(compression_pipeline).build_rag_tool(
- name='HousingLoanRetreiver', description='Tool to fetch data around housing loans'
-)
-session.register_tool(name='HousingLoanTool', tool=retriever_tool)
-
-simple_tool_agent = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: llm-assistant
-agent:
- name: tool-get-loan
- kind: agentic
- job: To retrieve and answer user questions
- tools:
- - name: HousingLoanTool
-"""
-
-flo = Flo.build(session, simple_tool_agent)
-
-print(flo.invoke('Whats interest rate on loan'))
diff --git a/flo_ai/examples/python/rag_with_reranking.py b/flo_ai/examples/python/rag_with_reranking.py
deleted file mode 100644
index f3bc9eb3..00000000
--- a/flo_ai/examples/python/rag_with_reranking.py
+++ /dev/null
@@ -1,70 +0,0 @@
-import os
-from langchain_mongodb import MongoDBAtlasVectorSearch
-from pymongo import MongoClient
-from langchain_openai import OpenAIEmbeddings
-from langchain_openai import ChatOpenAI
-from dotenv import load_dotenv
-
-
-from flo_ai import FloSession
-from flo_ai.retrievers.flo_retriever import FloRagBuilder
-from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
-from flo_ai.retrievers.flo_compression_pipeline import FloCompressionPipeline
-import logging
-
-
-load_dotenv()
-db_url = os.getenv('MONGO_DB_URL')
-
-connection_timeout = 60000
-mongo_client = MongoClient(
- db_url, connectTimeoutMS=connection_timeout, socketTimeoutMS=connection_timeout
-)
-mongo_embedding_collection = mongo_client.get_database('dohabank').get_collection(
- 'products'
-)
-
-store = MongoDBAtlasVectorSearch(
- collection=mongo_embedding_collection,
- embedding_key='embedding',
- embedding=OpenAIEmbeddings(model='text-embedding-3-small'),
- index_name='bank-products-index',
-)
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-session = FloSession(llm)
-rag_builder = FloRagBuilder(session, store.as_retriever())
-
-
-logging.basicConfig()
-logging.getLogger('langchain.retrievers.multi_query').setLevel(logging.INFO)
-
-custom_prompt = ChatPromptTemplate.from_messages(
- [
- (
- 'system',
- """You are an assistant for banking employees, of Doha Bank.
- Use the following pieces of retrieved context to answer the question.
- If you don't know the answer, just say that you don't know.
- Try to answer questions as bullet points that are easy to read""",
- ),
- MessagesPlaceholder(variable_name='chat_history'),
- ('human', '{question}'),
- ]
-)
-
-
-compression_pipeline = FloCompressionPipeline(
- OpenAIEmbeddings(model='text-embedding-3-small')
-)
-compression_pipeline.add_embedding_reduntant_filter()
-compression_pipeline.add_embedding_relevant_filter()
-
-rag = (
- rag_builder.with_prompt(custom_prompt)
- .with_multi_query()
- .with_compression(compression_pipeline)
- .build_rag()
-)
-print(rag.invoke({'question': 'What are the documents applying for housing loan'}))
diff --git a/flo_ai/examples/python/reflection_example.py b/flo_ai/examples/python/reflection_example.py
deleted file mode 100644
index 203fdad1..00000000
--- a/flo_ai/examples/python/reflection_example.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from flo_ai.core import Flo
-from flo_ai import FloSession
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-load_dotenv()
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: adding-team
-team:
- name: EssayTeam
- agents:
- - name: EssayWriter
- kind: llm
- job: >
- You are an essay assistant tasked with writing excellent 300-words essays. Generate the best essay possible for the user's request.
- If the you are provided critique view, respond with a revised version of your previous attempts. A maximum of total 100 words
- - name: ReflectionAgent
- kind: reflection
- retry: 1
- to:
- - name: EssayWriter
- job: >
- You are a teacher grading an essay submission. Generate critique and recommendations for the user's submission.
- Provide detailed recommendations, including requests for length, depth, style, etc.
- - name: FinalEssayProducer
- kind: llm
- job: >
- Generate the final assay to be returned to the user
- router:
- name: router
- kind: linear
-"""
-
-input_prompt = """
-Question: Write me an interesting blog about latest advancements in agentic AI by reasearching the internet
-"""
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-session = FloSession(llm).register_tool(
- name='TavilySearchResults', tool=TavilySearchResults()
-)
-
-flo: Flo = Flo.build(session, yaml=yaml_data)
-data = flo.invoke(input_prompt)
-print((data['messages'][-1]).content)
diff --git a/flo_ai/examples/python/simple_blogging_team.py b/flo_ai/examples/python/simple_blogging_team.py
deleted file mode 100644
index 7d09e0c2..00000000
--- a/flo_ai/examples/python/simple_blogging_team.py
+++ /dev/null
@@ -1,47 +0,0 @@
-from flo_ai import FloSession, Flo
-from langchain_openai import ChatOpenAI
-from langchain_community.tools.tavily_search.tool import TavilySearchResults
-from dotenv import load_dotenv
-
-load_dotenv()
-
-yaml_data = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: blogging-team
-team:
- name: BloggingTeam
- router:
- name: BloggerTeamLead
- kind: supervisor
- agents:
- - name: Researcher
- role: Blog Researcher
- job: Generate a list of topics related to the user questions and accululate articles about them
- tools:
- - name: TavilySearchResults
- - name: Blogger
- role: Blog Writer
- job: From the documents provider by the researcher write a blog of 300 words with can be readily published, make in engaging and add reference links to original blogs
- tools:
- - name: TavilySearchResults
-"""
-
-input_prompt = """
-Question: Write me an interesting blog about latest advancements in agentic AI by reasearching the internet
-"""
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-session = (
- FloSession(llm)
- .register_tool(name='TavilySearchResults', tool=TavilySearchResults())
- .register_tool(
- name='DummyTool',
- tool=TavilySearchResults(description='Tool is a dummy tool, dont use this'),
- )
-)
-
-Flo.set_log_level('INFO')
-flo: Flo = Flo.build(session, yaml=yaml_data)
-data = flo.invoke(input_prompt)
-# print((data['messages'][-1]).content)
diff --git a/flo_ai/examples/python/tool_agent.py b/flo_ai/examples/python/tool_agent.py
deleted file mode 100644
index b7971c81..00000000
--- a/flo_ai/examples/python/tool_agent.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from flo_ai import Flo
-from flo_ai import FloSession
-from langchain_openai import ChatOpenAI
-
-from dotenv import load_dotenv
-from langchain.tools import BaseTool
-
-load_dotenv()
-
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-
-session = FloSession(llm, log_level='ERROR')
-
-
-class PrintStateTool(BaseTool):
- name: str = 'printStateTool'
- description: str = 'Just print the state'
-
- def _run(self, **kwargs) -> str:
- return 'Print tool call success'
-
-
-session.register_tool(name='printStateTool', tool=PrintStateTool())
-
-simple_tool_agent = """
-apiVersion: flo/alpha-v1
-kind: FloRoutedTeam
-name: llm-assistant
-team:
- name: tool-to-print-state
- router:
- name: LinearRouter
- kind: linear
- agents:
- - name: tool-to-print
- kind: tool
- tools:
- - name: printStateTool
-"""
-
-flo = Flo.build(session, simple_tool_agent, log_level='ERROR')
-
-print(flo.invoke('Testing ....'))
diff --git a/flo_ai/examples/python/tool_data_logging_example.py b/flo_ai/examples/python/tool_data_logging_example.py
deleted file mode 100644
index ca4e59d2..00000000
--- a/flo_ai/examples/python/tool_data_logging_example.py
+++ /dev/null
@@ -1,79 +0,0 @@
-from flo_ai.callbacks import FloExecutionLogger
-from flo_ai.storage.data_collector import JSONLFileCollector
-from langchain_openai import AzureChatOpenAI
-import os
-from dotenv import load_dotenv
-from flo_ai import Flo
-from flo_ai import FloSession
-from typing import List
-from flo_ai.tools import flotool
-
-load_dotenv()
-
-llm = AzureChatOpenAI(
- temperature=0,
- deployment_name='gpt-4',
- model_name='gpt-4',
- azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT'),
- api_key=os.getenv('AZURE_OPENAI_API_KEY'),
- api_version='2024-08-01-preview',
-)
-
-session = FloSession(
- llm,
- log_level='ERROR',
-)
-
-
-@flotool(name='AdditionTool', description='Tool to add numbers')
-def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- return f'The sum is {result}'
-
-
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
-)
-def mul_tool(numbers: List[int]) -> str:
- result = 1
- for num in numbers:
- result *= num
- return f'The product is {result}'
-
-
-session.register_tool(name='Adder', tool=addition_tool).register_tool(
- name='Multiplier', tool=mul_tool
-)
-
-simple_calculator_agent = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: calculating-assistant
-agent:
- name: SummationHelper
- kind: agentic
- job: >
- You are a calculation assistant that MUST ONLY use the provided tools for calculations.
- You MUST ONLY return the exact outputs from the tools without modification.
- You MUST NOT perform any calculations yourself.
- If you need both sum and product, you MUST use both tools and combine their exact outputs.
- tools:
- - name: Adder
- - name: Multiplier
-"""
-
-
-current_dir = os.path.dirname(os.path.abspath(__file__))
-log_file_path = os.path.join(current_dir, 'my_llm_logs.jsonl')
-
-file_collector = JSONLFileCollector(log_file_path)
-local_tracker = FloExecutionLogger(file_collector)
-
-session.register_callback(local_tracker)
-
-flo = Flo.build(session, simple_calculator_agent, log_level='ERROR')
-
-result = flo.invoke(
- 'find the sum of first three numbers and last three numbers and multilply the result. Numbers are 1, 3, 4, 2, 0, 1',
-)
diff --git a/flo_ai/examples/python/tool_error_handling.py b/flo_ai/examples/python/tool_error_handling.py
deleted file mode 100644
index 2c4fa2b9..00000000
--- a/flo_ai/examples/python/tool_error_handling.py
+++ /dev/null
@@ -1,81 +0,0 @@
-from flo_ai import Flo
-from flo_ai import FloSession
-from langchain_openai import ChatOpenAI
-from pydantic import BaseModel, Field
-from typing import List
-from dotenv import load_dotenv
-from flo_ai.tools import flotool
-import asyncio
-
-load_dotenv()
-
-llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini')
-
-session = FloSession(llm, log_level='ERROR')
-
-
-class AdditionToolInput(BaseModel):
- numbers: List[int] = Field(..., description='List of numbers to add')
-
-
-# Use flotool to define the tool function
-@flotool(name='AdditionTool', description='Tool to add numbers')
-async def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The sum is {result}'
-
-
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
-)
-def mul_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- # await asyncio.sleep(1)
- return f'The product is {result}'
-
-
-session.register_tool(name='Adder', tool=addition_tool).register_tool(
- name='Multiplier', tool=mul_tool
-)
-
-simple_weather_checking_agent = """
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: SummationHelper
- kind: agentic
- job: >
- Add or multiply numbers. Always answer based on what the tool says
- tools:
- - name: Adder
- - name: Multiplier
-"""
-
-
-flo = Flo.build(session, simple_weather_checking_agent, log_level='ERROR')
-
-
-# Assuming flo.ainvoke is your async method for invoking the tool or chain
-async def invoke_main():
- result = await flo.async_invoke(
- 'Whats the sum of 1, 3, 4, 5 and 6, and their product'
- )
- print(result)
-
-
-asyncio.run(invoke_main())
-
-
-# import asyncio
-
-# async def stream_main():
-# # Use 'async for' to iterate over the asynchronous generator
-# async for s in flo.async_stream("Whats the sum of 1, 3, 4, 5 and 6, and their product"):
-# if "__end__" not in s:
-# print(s)
-# print("----")
-
-# asyncio.run(stream_main())
diff --git a/flo_ai/examples/usage.py b/flo_ai/examples/usage.py
new file mode 100644
index 00000000..0d9cbbbd
--- /dev/null
+++ b/flo_ai/examples/usage.py
@@ -0,0 +1,133 @@
+import asyncio
+from flo_ai.models.agent import Agent as ToolAgent
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.tool.base_tool import Tool
+from flo_ai.models.agent_error import AgentError
+from flo_ai.models.base_agent import ReasoningPattern
+
+
+# Example of using ToolAgent as a conversational agent
+async def test_conversational():
+ llm = OpenAI(model='gpt-4', temperature=0.7)
+ agent = ToolAgent(
+ name='Assistant',
+ system_prompt='You are a helpful AI assistant.',
+ llm=llm,
+ )
+
+ response = await agent.run('What is the capital of France?')
+ print(response)
+
+
+# Example of using ToolAgent with tools
+async def test_tool_agent():
+ # Define a simple tool
+ async def get_weather(city: str) -> str:
+ # This would normally call a weather API
+ return f'The weather in {city} is sunny'
+
+ weather_tool = Tool(
+ name='get_weather',
+ description='Get the weather for a city',
+ function=get_weather,
+ parameters={
+ 'city': {'type': 'string', 'description': 'The city to get weather for'}
+ },
+ )
+
+ llm = OpenAI(model='gpt-3.5-turbo', temperature=0.7)
+ agent = ToolAgent(
+ name='WeatherAssistant',
+ system_prompt='You are a helpful weather assistant.',
+ llm=llm,
+ tools=[weather_tool],
+ )
+
+ response = await agent.run("What's the weather like in Paris?")
+ print(response)
+
+
+async def test_error_handling():
+ # Example of a tool that might fail
+ async def flaky_weather(city: str) -> str:
+ if city.lower() == 'error':
+ raise ValueError('API temporarily unavailable')
+ return f'The weather in {city} is sunny'
+
+ weather_tool = Tool(
+ name='get_weather',
+ description='Get the weather for a city',
+ function=flaky_weather,
+ parameters={
+ 'city': {'type': 'string', 'description': 'The city to get weather for'}
+ },
+ )
+
+ llm = OpenAI(model='gpt-3.5-turbo', temperature=0.7)
+ agent = ToolAgent(
+ name='WeatherAssistant',
+ system_prompt='You are a helpful weather assistant.',
+ llm=llm,
+ tools=[weather_tool],
+ max_retries=3,
+ )
+
+ try:
+ # This will trigger error handling and retries
+ response = await agent.run("What's the weather like in error?")
+ print(response)
+ except AgentError as e:
+ print(f'Agent error: {str(e)}')
+ if e.original_error:
+ print(f'Original error: {str(e.original_error)}')
+
+
+async def test_direct_reasoning():
+ # Define a simple calculator tool
+ async def calculate(operation: str, x: float, y: float) -> float:
+ if operation == 'add':
+ return x + y
+ elif operation == 'multiply':
+ return x * y
+ raise ValueError(f'Unknown operation: {operation}')
+
+ calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add or multiply)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+ llm = OpenAI(model='gpt-3.5-turbo', temperature=0.7)
+ agent = ToolAgent(
+ name='CalculatorAssistant',
+ system_prompt='You are a helpful calculator assistant. Use the calculator tool directly without explanation.',
+ llm=llm,
+ tools=[calculator_tool],
+ reasoning_pattern=ReasoningPattern.DIRECT,
+ )
+
+ response = await agent.run('Calculate 5 plus 3')
+ print(response)
+
+
+# Run the examples
+if __name__ == '__main__':
+ print('Testing conversational agent...\n')
+ asyncio.run(test_conversational())
+
+ print('\nTesting tool agent...\n')
+ asyncio.run(test_tool_agent())
+
+ print('\nTesting error handling...\n')
+ asyncio.run(test_error_handling())
+
+ print('\nTesting direct reasoning...\n')
+ asyncio.run(test_direct_reasoning())
diff --git a/flo_ai/examples/usage_claude.py b/flo_ai/examples/usage_claude.py
new file mode 100644
index 00000000..b5072e94
--- /dev/null
+++ b/flo_ai/examples/usage_claude.py
@@ -0,0 +1,189 @@
+import asyncio
+import os
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.models.agent import Agent as ToolAgent
+from flo_ai.llm.anthropic_llm import Anthropic
+from flo_ai.tool.base_tool import Tool
+from flo_ai.models.agent_error import AgentError
+
+
+async def test_claude_conversational():
+ # Initialize Claude LLM
+ claude_llm = Anthropic(
+ model='claude-3-5-sonnet-20240620',
+ temperature=0.7,
+ api_key=os.getenv('ANTHROPIC_API_KEY'),
+ )
+
+ # Create conversational agent with Claude
+ agent = ToolAgent(
+ name='ClaudeAssistant',
+ system_prompt='You are a helpful AI assistant powered by Claude.',
+ llm=claude_llm,
+ )
+
+ try:
+ response = await agent.run(
+ 'What are the main differences between Python and JavaScript?'
+ )
+ print('\nConversational Agent Response:')
+ print(response)
+ except AgentError as e:
+ print(f'Error: {str(e)}')
+
+
+async def test_claude_tool_agent():
+ # Example weather tool
+ async def get_weather(city: str, country: str = None) -> str:
+ location = f'{city}, {country}' if country else city
+ # This would normally call a weather API
+ return f"Currently in {location}, it's sunny and warm with a temperature of 25ยฐC (77ยฐF)."
+
+ weather_tool = Tool(
+ name='get_weather',
+ description='Get the current weather for a city',
+ function=get_weather,
+ parameters={
+ 'city': {'type': 'string', 'description': 'The city to get weather for'},
+ 'country': {
+ 'type': 'string',
+ 'description': 'The country of the city (optional)',
+ 'required': False,
+ },
+ },
+ )
+
+ # Initialize Claude LLM
+ claude_llm = Anthropic(
+ model='claude-3-5-sonnet-20240620',
+ temperature=0.7,
+ api_key=os.getenv('ANTHROPIC_API_KEY'),
+ )
+
+ # Create tool agent with Claude
+ agent = ToolAgent(
+ name='ClaudeWeatherAssistant',
+ system_prompt="""You are a helpful weather assistant. When asked about weather, always use the weather tool to get information.
+ After getting the weather information, provide a natural response incorporating the data.
+ Do not just think about using the tool - actually use it and share the results.""",
+ llm=claude_llm,
+ tools=[weather_tool],
+ max_retries=1,
+ )
+
+ try:
+ # Test with different queries
+ queries = [
+ "What's the weather like in Tokyo?",
+ 'Tell me the weather in Paris, France',
+ "How's the weather in New York City, USA?",
+ ]
+
+ for query in queries:
+ print(f'\nQuery: {query}')
+ response = await agent.run(query)
+ print(f'Response: {response}')
+
+ except AgentError as e:
+ print(f'Error: {str(e)}')
+ if e.original_error:
+ print(f'Original error: {str(e.original_error)}')
+
+
+async def test_error_handling():
+ # Example of a tool that might fail
+ async def flaky_weather(city: str) -> str:
+ if city.lower() == 'error':
+ raise ValueError('API temporarily unavailable')
+ return f'The weather in {city} is sunny'
+
+ weather_tool = Tool(
+ name='get_weather',
+ description='Get the weather for a city',
+ function=flaky_weather,
+ parameters={
+ 'city': {'type': 'string', 'description': 'The city to get weather for'}
+ },
+ )
+
+ claude_llm = Anthropic(
+ model='claude-3-opus-20240229',
+ temperature=0.7,
+ api_key=os.getenv('ANTHROPIC_API_KEY'),
+ )
+
+ agent = ToolAgent(
+ name='ClaudeWeatherAssistant',
+ system_prompt='You are a helpful weather assistant.',
+ llm=claude_llm,
+ tools=[weather_tool],
+ max_retries=3,
+ )
+
+ try:
+ # This will trigger error handling and retries
+ response = await agent.run("What's the weather like in error?")
+ print('\nResponse:', response)
+ except AgentError as e:
+ print('\nAgent error:', str(e))
+ if e.original_error:
+ print('Original error:', str(e.original_error))
+
+
+async def test_direct_reasoning():
+ # Define a simple calculator tool
+ async def calculate(operation: str, x: float, y: float) -> float:
+ if operation == 'add':
+ return x + y
+ elif operation == 'multiply':
+ return x * y
+ raise ValueError(f'Unknown operation: {operation}')
+
+ calculator_tool = Tool(
+ name='calculate',
+ description='Perform basic calculations',
+ function=calculate,
+ parameters={
+ 'operation': {
+ 'type': 'string',
+ 'description': 'The operation to perform (add or multiply)',
+ },
+ 'x': {'type': 'number', 'description': 'First number'},
+ 'y': {'type': 'number', 'description': 'Second number'},
+ },
+ )
+
+ claude_llm = Anthropic(
+ model='claude-3-5-sonnet-20240620',
+ temperature=0.7,
+ api_key=os.getenv('ANTHROPIC_API_KEY'),
+ )
+
+ agent = ToolAgent(
+ name='ClaudeCalculatorAssistant',
+ system_prompt='You are a helpful calculator assistant. Use the calculator tool directly without explanation.',
+ llm=claude_llm,
+ tools=[calculator_tool],
+ reasoning_pattern=ReasoningPattern.DIRECT,
+ )
+
+ response = await agent.run('Calculate 5 plus 3')
+ print('\nDirect Reasoning Response:', response)
+
+
+async def main():
+ print('\n=== Testing Claude Conversational Agent ===')
+ await test_claude_conversational()
+
+ print('\n=== Testing Claude Tool Agent ===')
+ await test_claude_tool_agent()
+
+ print('\n=== Testing Error Handling ===')
+ await test_error_handling()
+
+ print('\n=== Testing Direct Reasoning ===')
+ await test_direct_reasoning()
+
+
+if __name__ == '__main__':
+ asyncio.run(main())
diff --git a/flo_ai/examples/yaml_agent_example.py b/flo_ai/examples/yaml_agent_example.py
new file mode 100644
index 00000000..e725c21b
--- /dev/null
+++ b/flo_ai/examples/yaml_agent_example.py
@@ -0,0 +1,150 @@
+from flo_ai.builder.agent_builder import AgentBuilder
+
+# Example YAML configuration
+yaml_config = """
+apiVersion: flo/alpha-v1
+kind: FloAgent
+metadata:
+ name: email-summary-flo
+ version: 1.0.0
+ description: "Agent for analyzing email threads between customers and support"
+ tags: ["email", "analysis", "support"]
+agent:
+ name: EmailSummaryAgent
+ kind: llm
+ role: Email communication expert
+ model:
+ provider: openai # or claude
+ name: gpt-4o-mini # or claude-3-5-sonnet-20240620
+ settings:
+ temperature: 0
+ max_retries: 3
+ reasoning_pattern: DIRECT
+ job: >
+ You are given an email thread between a customer and a support agent of a bank.
+ Your job is to analyze the behavior, sentiment, and communication style from the latest email in the thread.
+ Focus the data extraction based on ONLY the latest email, and use the previous emails for context of the conversation and the product.
+ First, identify whether the latest email is from the customer or the support agent.
+
+ parser:
+ name: EmailSummary
+ version: 1.0.0
+ description: "Parser for email thread analysis"
+ fields:
+ - name: sub_category
+ type: literal
+ required: true
+ description: >
+ Identifies who sent the latest email in the thread.
+ values:
+ - value: customer
+ description: The latest email was sent by the customer to the bank
+ examples: ["From: customer@example.com", "Sent by: John Smith"]
+ - value: agent
+ description: The latest email was sent by the bank's support agent to the customer
+ examples: ["From: support@bank.com", "Sent by: Sarah from Support"]
+
+ - name: call_summary
+ type: str
+ required: true
+ description: >
+ A comprehensive summary of the latest email in the thread, capturing all major points raised.
+ Never mention customer's personal identifiable information like full name, account numbers, etc.
+
+ - name: thread_context
+ type: str
+ required: true
+ description: >
+ Brief context of the overall thread based on references in the latest email.
+ This should help understand what has transpired before this email.
+
+ - name: call_resolution
+ type: literal
+ required: true
+ description: >
+ Assessment of whether the customer issue appears to be resolved based on the latest email.
+ values:
+ - value: resolved
+ description: The issue appears to be fully resolved and the customer seems satisfied
+ examples: ["Customer confirms resolution", "Issue has been fixed"]
+
+ - value: partial
+ description: The issue appears to be partially resolved but requires further action or confirmation
+ examples: ["Customer needs to follow up", "Waiting for customer response"]
+
+ - value: unresolved
+ description: The issue remains unresolved and requires further attention
+ examples: ["Customer still experiencing issues", "Problem persists"]
+
+ - value: open
+ description: If only customer email is present in the email thread or cannot determine the resolution status
+ examples: ["Initial customer contact", "No response from support yet"]
+
+ examples:
+ - input: |
+ From: customer@example.com
+ Subject: Issue with my account
+
+ Hi,
+ I'm having trouble accessing my account. The login page keeps showing an error.
+ Can you please help me resolve this?
+
+ Best regards,
+ John
+ output:
+ sub_category: customer
+ call_summary: "Customer reports login issues with their account"
+ thread_context: "Initial contact about account access problems"
+ call_resolution: open
+"""
+
+
+async def main():
+ # Create agent builder from YAML
+ builder = AgentBuilder.from_yaml(yaml_str=yaml_config)
+
+ # Build the agent
+ agent = builder.build()
+
+ # Example email thread
+ email_thread = """
+ From: customer@example.com
+ Subject: Issue with my account
+
+ Hi,
+ I'm having trouble accessing my account. The login page keeps showing an error.
+ Can you please help me resolve this?
+
+ Best regards,
+ John
+
+ ---
+
+ From: support@bank.com
+ Subject: Re: Issue with my account
+
+ Dear John,
+
+ I understand you're having trouble accessing your account. I've checked your account status and everything seems to be in order.
+ Let's try resetting your password. Please follow these steps:
+ 1. Go to our login page
+ 2. Click on "Forgot Password"
+ 3. Enter your email address
+ 4. Follow the instructions in the email you receive
+
+ Let me know if you need any further assistance.
+
+ Best regards,
+ Sarah
+ Support Team
+ """
+
+ # Process the email thread
+ result = await agent.run(email_thread)
+ print('Analysis Result:', result)
+
+
+if __name__ == '__main__':
+ import asyncio
+
+ asyncio.run(main())
diff --git a/flo_ai/flo_ai/__init__.py b/flo_ai/flo_ai/__init__.py
index 2ec5d57e..e69de29b 100644
--- a/flo_ai/flo_ai/__init__.py
+++ b/flo_ai/flo_ai/__init__.py
@@ -1,13 +0,0 @@
-from flo_ai.core import Flo as Flo
-from flo_ai.models.flo_team import FloTeam as FloTeam
-from flo_ai.models.flo_agent import FloAgent as FloAgent
-from flo_ai.router.flo_linear import FloLinear as FloLinear
-from flo_ai.router.flo_router import FloRouter as FloRouter
-from flo_ai.state.flo_session import FloSession as FloSession
-from flo_ai.models.flo_llm_agent import FloLLMAgent as FloLLMAgent
-from flo_ai.models.flo_tool_agent import FloToolAgent as FloToolAgent
-from flo_ai.router.flo_llm_router import FloLLMRouter as FloLLMRouter
-from flo_ai.router.flo_supervisor import FloSupervisor as FloSupervisor
-from flo_ai.retrievers.flo_retriever import FloRagBuilder as FloRagBuilder
-from flo_ai.models.flo_delegation_agent import FloDelegatorAgent as FloDelegatorAgent
-from flo_ai.models.flo_reflection_agent import FloReflectionAgent as FloReflectionAgent
diff --git a/flo_ai/flo_ai/builder/agent_builder.py b/flo_ai/flo_ai/builder/agent_builder.py
new file mode 100644
index 00000000..08228547
--- /dev/null
+++ b/flo_ai/flo_ai/builder/agent_builder.py
@@ -0,0 +1,158 @@
+from typing import List, Optional, Dict, Any, Union, Type
+import yaml
+from flo_ai.models.agent import Agent
+from flo_ai.models.base_agent import ReasoningPattern
+from flo_ai.llm.base_llm import BaseLLM
+from flo_ai.llm.openai_llm import OpenAI
+from flo_ai.llm.anthropic_llm import Anthropic
+from flo_ai.tool.base_tool import Tool
+from flo_ai.formatter.yaml_format_parser import FloYamlParser
+from pydantic import BaseModel
+
+
+class AgentBuilder:
+ """
+ A facade class that simplifies the creation and configuration of AI agents.
+ """
+
+ def __init__(self):
+ self._name = 'AI Assistant'
+ self._system_prompt = 'You are a helpful AI assistant.'
+ self._llm: Optional[BaseLLM] = None
+ self._tools: List[Tool] = []
+ self._max_retries = 3
+ self._reasoning_pattern = ReasoningPattern.DIRECT
+ self._output_schema: Optional[Dict[str, Any]] = None
+ self._role: Optional[str] = None
+
+ def with_name(self, name: str) -> 'AgentBuilder':
+ """Set the agent's name"""
+ self._name = name
+ return self
+
+ def with_prompt(self, system_prompt: str) -> 'AgentBuilder':
+ """Set the system prompt"""
+ self._system_prompt = system_prompt
+ return self
+
+ def with_llm(self, llm: BaseLLM) -> 'AgentBuilder':
+ """Configure the LLM to use
+
+ Args:
+ llm: An instance of a BaseLLM implementation
+ """
+ self._llm = llm
+ return self
+
+ def with_tools(self, tools: List[Tool]) -> 'AgentBuilder':
+ """Add tools to the agent"""
+ self._tools = tools
+ return self
+
+ def with_reasoning(self, pattern: ReasoningPattern) -> 'AgentBuilder':
+ """Set the reasoning pattern"""
+ self._reasoning_pattern = pattern
+ return self
+
+ def with_retries(self, max_retries: int) -> 'AgentBuilder':
+ """Set maximum number of retries"""
+ self._max_retries = max_retries
+ return self
+
+ def with_output_schema(
+ self, schema: Union[Dict[str, Any], Type[BaseModel]]
+ ) -> 'AgentBuilder':
+ """Set output schema for structured responses
+
+ Args:
+ schema: Either a JSON schema dictionary or a Pydantic model class
+ """
+ if isinstance(schema, type) and issubclass(schema, BaseModel):
+ self._output_schema = schema.model_json_schema()
+ else:
+ self._output_schema = schema
+ return self
+
+ def with_role(self, role: str) -> 'AgentBuilder':
+ """Set the agent's role"""
+ self._role = role
+ return self
+
+ def build(self) -> Agent:
+ """Build and return the configured agent"""
+ if not self._llm:
+ raise ValueError('LLM must be configured before building the agent')
+
+ return Agent(
+ name=self._name,
+ system_prompt=self._system_prompt,
+ llm=self._llm,
+ tools=self._tools,
+ max_retries=self._max_retries,
+ reasoning_pattern=self._reasoning_pattern,
+ output_schema=self._output_schema,
+ role=self._role,
+ )
+
+ @classmethod
+ def from_yaml(
+ cls, yaml_str: str, tools: Optional[List[Tool]] = None
+ ) -> 'AgentBuilder':
+ """Create an agent builder from a YAML configuration string
+
+ Args:
+ yaml_str: YAML string containing agent configuration
+ tools: Optional list of tools to use with the agent
+
+ Returns:
+ AgentBuilder: Configured agent builder instance
+ """
+ config = yaml.safe_load(yaml_str)
+
+ if 'agent' not in config:
+ raise ValueError('YAML must contain an "agent" section')
+
+ agent_config = config['agent']
+ builder = cls()
+
+ # Set basic properties
+ builder.with_name(agent_config.get('name', 'AI Assistant'))
+ builder.with_prompt(agent_config.get('job', 'You are a helpful AI assistant.'))
+ builder.with_role(agent_config.get('role'))
+
+ # Configure LLM based on model settings
+ if 'model' in agent_config:
+ model_config = agent_config['model']
+ provider = model_config.get('provider', 'openai').lower()
+ model_name = model_config.get('name')
+
+ if not model_name:
+ raise ValueError('Model name must be specified in YAML configuration')
+
+ if provider == 'openai':
+ builder.with_llm(OpenAI(model=model_name))
+ elif provider == 'claude':
+ builder.with_llm(Anthropic(model=model_name))
+ else:
+ raise ValueError(f'Unsupported model provider: {provider}')
+
+ # Set tools if provided
+ if tools:
+ builder.with_tools(tools)
+
+ # Set parser if present
+ if 'parser' in agent_config:
+ parser = FloYamlParser.create(yaml_dict=config)
+ builder.with_output_schema(parser.get_format())
+
+ # Apply settings if present
+ if 'settings' in agent_config:
+ settings = agent_config['settings']
+ if 'temperature' in settings:
+ builder._llm.temperature = settings['temperature']
+ if 'max_retries' in settings:
+ builder.with_retries(settings['max_retries'])
+ if 'reasoning_pattern' in settings:
+ builder.with_reasoning(ReasoningPattern[settings['reasoning_pattern']])
+
+ return builder
diff --git a/flo_ai/flo_ai/builders/__init__.py b/flo_ai/flo_ai/builders/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flo_ai/flo_ai/builders/yaml_builder.py b/flo_ai/flo_ai/builders/yaml_builder.py
deleted file mode 100644
index f4a5e0f4..00000000
--- a/flo_ai/flo_ai/builders/yaml_builder.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from typing import Union
-from flo_ai.models.flo_team import FloTeam
-from flo_ai.yaml.config import (
- FloRoutedTeamConfig,
- TeamConfig,
- AgentConfig,
- FloAgentConfig,
-)
-from flo_ai.models.flo_executable import ExecutableFlo
-from flo_ai.state.flo_session import FloSession
-from flo_ai.router.flo_router_factory import FloRouterFactory
-from flo_ai.factory.agent_factory import AgentFactory
-from flo_ai.yaml.validators import raise_for_name_error
-from flo_ai.common.flo_logger import get_logger
-
-
-def build_supervised_team(
- session: FloSession, flo_config: Union[FloRoutedTeamConfig, FloAgentConfig]
-) -> ExecutableFlo:
- name_set = set()
- if isinstance(flo_config, FloRoutedTeamConfig):
- team_config: TeamConfig = flo_config.team
- team = parse_and_build_subteams(session, team_config, name_set)
- return team
- elif isinstance(flo_config, FloAgentConfig):
- agent_config: AgentConfig = flo_config.agent
- validate_names(name_set, agent_config.name, session)
- agent = parse_build_agent(session, agent_config, name_set)
- return agent
-
-
-def validate_team(name_set: set, team_config: TeamConfig, session: FloSession):
- validate_names(name_set, team_config.name, session)
-
-
-def parse_and_build_subteams(
- session: FloSession, team_config: TeamConfig, name_set=set()
-) -> ExecutableFlo:
- flo_team = None
- validate_team(name_set, team_config, session)
- if team_config.agents:
- members = [AgentFactory.create(session, agent) for agent in team_config.agents]
- flo_team = FloTeam.Builder(session, team_config.name, members=members).build()
- router = FloRouterFactory.create(
- session, team_config.router.kind, team_config, flo_team
- )
- flo_routed_team = router.build_routed_team()
- else:
- flo_teams = []
- for subteam in team_config.subteams:
- flo_subteam = parse_and_build_subteams(session, subteam, name_set)
- flo_teams.append(flo_subteam)
- flo_team = FloTeam.Builder(session, team_config.name, members=flo_teams).build()
- router = FloRouterFactory.create(
- session, team_config.router.kind, team_config, flo_team
- )
- flo_routed_team = router.build_routed_team()
- return flo_routed_team
-
-
-def parse_build_agent(
- session: FloSession, agent_config: AgentConfig, name_set=set()
-) -> ExecutableFlo:
- validate_names(name_set, agent_config.name, session)
- agent = AgentFactory.create(session, agent_config)
- router = FloRouterFactory.create(session, 'agent', agent_config, agent)
- return router.build_routed_team()
-
-
-def validate_names(name_set: set, name, session: FloSession):
- raise_for_name_error(name)
- if name in name_set:
- get_logger().warn(f"Duplicate name found: '{name}'", session)
- name_set.add(name)
diff --git a/flo_ai/flo_ai/callbacks/__init__.py b/flo_ai/flo_ai/callbacks/__init__.py
deleted file mode 100644
index 03eb1a64..00000000
--- a/flo_ai/flo_ai/callbacks/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from flo_ai.callbacks.flo_callbacks import (
- flo_agent_callback,
- flo_router_callback,
- flo_tool_callback,
- flo_call_back,
-)
-from flo_ai.callbacks.flo_execution_logger import FloExecutionLogger
-
-__all__ = [
- 'flo_agent_callback',
- 'flo_router_callback',
- 'flo_tool_callback',
- 'flo_call_back',
- 'FloExecutionLogger',
-]
diff --git a/flo_ai/flo_ai/callbacks/flo_callbacks.py b/flo_ai/flo_ai/callbacks/flo_callbacks.py
deleted file mode 100644
index 5ae0afdb..00000000
--- a/flo_ai/flo_ai/callbacks/flo_callbacks.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from typing import Any, Union, Callable, Optional, Dict
-from dataclasses import dataclass, field
-from flo_ai.common.flo_logger import get_logger
-
-
-@dataclass
-class FloCallbackResponse:
- type: str
- name: Optional[str] = None
- model_name: Optional[str] = None
- input: Optional[str] = None
- output: Optional[str] = None
- error: Union[Exception, KeyboardInterrupt, None] = None
- args: Dict = field(default_factory=dict)
-
-
-class FloToolCallback:
- def __init__(self) -> None:
- pass
-
- def on_tool_start(
- self, name: str, input: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_tool_end(
- self, name: str, output: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_tool_error(
- self, name: str, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
-
-class FloAgentCallback:
- def __init__(self) -> None:
- pass
-
- def on_agent_start(
- self, name: str, model_name: str, input: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_agent_end(
- self, name: str, model_name: str, output: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_agent_error(
- self,
- name: str,
- model_name: str,
- error: Union[Exception, KeyboardInterrupt],
- **kwargs: Any,
- ) -> Optional[FloCallbackResponse]:
- pass
-
-
-class FloRouterCallback:
- def __init__(self) -> None:
- pass
-
- def on_router_start(
- self, name: str, model_name: str, input: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_router_end(
- self, name: str, model_name: str, output: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- pass
-
- def on_router_error(
- self,
- name: str,
- model_name: str,
- error: Union[Exception, KeyboardInterrupt],
- **kwargs: Any,
- ) -> None:
- Optional[FloCallbackResponse]
-
-
-def safe_call_cb(func, cb_response: FloCallbackResponse, ignore_error=True):
- try:
- func(cb_response)
- except Exception as e:
- if ignore_error:
- get_logger().warning(e)
- else:
- raise e
-
-
-class FunctionalFloToolCallbackImpl(FloToolCallback):
- def __init__(self, func: Callable, ignore_error: bool = True) -> None:
- super().__init__()
- self.func = func
- self.ignore_error = ignore_error
-
- def on_tool_start(
- self, name: str, input: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- cb_response = FloRouterCallback('on_tool_start', name, input=input, args=kwargs)
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_tool_end(
- self, name: str, output: Any, **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- cb_response = FloRouterCallback('on_tool_end', name, output=output, args=kwargs)
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_tool_error(
- self, name: str, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
- ) -> Optional[FloCallbackResponse]:
- cb_response = FloRouterCallback('on_tool_error', name, error=error, args=kwargs)
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
-
-class FunctionalFloAgentCallbackImpl(FloAgentCallback):
- def __init__(self, func: Callable, ignore_error: bool = True) -> None:
- super().__init__()
- self.func = func
- self.ignore_error = ignore_error
-
- def on_agent_start(
- self, name: str, model_name: str, input: Any, **kwargs: Any
- ) -> Any:
- cb_response = FloCallbackResponse(
- 'on_agent_start', name, input=input, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_agent_end(
- self, name: str, model_name: str, output: Any, **kwargs: Any
- ) -> None:
- cb_response = FloCallbackResponse(
- 'on_agent_end', name, output=output, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_agent_error(
- self,
- name: str,
- model_name: str,
- error: Union[Exception, KeyboardInterrupt],
- **kwargs: Any,
- ) -> None:
- cb_response = FloCallbackResponse(
- 'on_agent_error', name, error=error, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
-
-class FunctionalFloRouterCallbackImpl(FloRouterCallback):
- def __init__(self, func: Callable, ignore_error: bool = True) -> None:
- super().__init__()
- self.func = func
- self.ignore_error = ignore_error
-
- def on_router_start(
- self, name: str, model_name: str, input: Any, **kwargs: Any
- ) -> Any:
- cb_response = FloCallbackResponse(
- 'on_router_start', name, input=input, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_router_end(
- self, name: str, model_name: str, output: Any, **kwargs: Any
- ) -> None:
- cb_response = FloCallbackResponse(
- 'on_router_end', name, output=output, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
- def on_router_error(
- self,
- name: str,
- model_name: str,
- error: Union[Exception, KeyboardInterrupt],
- **kwargs: Any,
- ) -> None:
- cb_response = FloCallbackResponse(
- 'on_router_error', name, error=error, args=kwargs, model_name=model_name
- )
- safe_call_cb(self.func, cb_response, self.ignore_error)
- return cb_response
-
-
-class FloCallback(
- FunctionalFloToolCallbackImpl,
- FunctionalFloAgentCallbackImpl,
- FunctionalFloRouterCallbackImpl,
-):
- def __init__(self, func: Callable, ignore_error: bool = True) -> None:
- FunctionalFloToolCallbackImpl.__init__(self, func, ignore_error)
- FunctionalFloAgentCallbackImpl.__init__(self, func, ignore_error)
- FunctionalFloRouterCallbackImpl.__init__(self, func, ignore_error)
-
-
-def flo_tool_callback(func: Callable, ignore_error=True) -> FloToolCallback:
- return FunctionalFloToolCallbackImpl(func, ignore_error)
-
-
-def flo_agent_callback(func: Callable, ignore_error=True) -> FloToolCallback:
- return FunctionalFloAgentCallbackImpl(func, ignore_error)
-
-
-def flo_router_callback(func: Callable, ignore_error=True) -> FloRouterCallback:
- return FunctionalFloRouterCallbackImpl(func, ignore_error)
-
-
-def flo_call_back(func: Callable, ignore_error=True) -> FloRouterCallback:
- return FloCallback(func, ignore_error)
diff --git a/flo_ai/flo_ai/callbacks/flo_execution_logger.py b/flo_ai/flo_ai/callbacks/flo_execution_logger.py
deleted file mode 100644
index 29d174ac..00000000
--- a/flo_ai/flo_ai/callbacks/flo_execution_logger.py
+++ /dev/null
@@ -1,277 +0,0 @@
-import json
-from typing import Any, Dict, Optional
-from datetime import datetime
-from uuid import UUID
-from langchain_core.callbacks import BaseCallbackHandler
-from langchain.schema.agent import AgentAction, AgentFinish
-from langchain.schema import HumanMessage, AIMessage, BaseMessage
-from langchain_core.prompts.chat import ChatPromptValue
-from flo_ai.storage.data_collector import DataCollector
-from flo_ai.common.flo_logger import get_logger
-from abc import ABC, abstractmethod
-
-
-class ToolLogger(ABC):
- @abstractmethod
- def log_all_tools(session_tools):
- pass
-
-
-class EnhancedJSONEncoder(json.JSONEncoder):
- def default(self, obj):
- if isinstance(obj, (HumanMessage, AIMessage, BaseMessage)):
- return {
- 'type': obj.__class__.__name__,
- 'content': obj.content,
- 'additional_kwargs': obj.additional_kwargs,
- }
- elif isinstance(obj, AgentAction):
- return {
- 'type': 'AgentAction',
- 'tool': obj.tool,
- 'tool_input': obj.tool_input,
- 'log': obj.log,
- }
- elif isinstance(obj, AgentFinish):
- return {
- 'type': 'AgentFinish',
- 'return_values': obj.return_values,
- 'log': obj.log,
- }
- elif isinstance(obj, ChatPromptValue):
- return {
- 'type': 'ChatPromptValue',
- 'messages': [self.default(msg) for msg in obj.messages],
- }
- elif isinstance(obj, datetime):
- return obj.isoformat()
- elif isinstance(obj, UUID):
- return str(obj)
- elif hasattr(obj, 'to_dict'):
- return obj.to_dict()
- return super().default(obj)
-
-
-class FloExecutionLogger(BaseCallbackHandler, ToolLogger):
- def __init__(self, data_collector: DataCollector):
- self.data_collector = data_collector
- self.runs = {}
- self.encoder = EnhancedJSONEncoder()
- self.query = None
- self.added_tools = set()
- self.prompt = {}
-
- def _encode_entry(self, entry: Dict[str, Any]) -> Dict[str, Any]:
- return json.loads(self.encoder.encode(entry))
-
- def _store_entry(self, entry: Dict[str, Any]) -> None:
- try:
- encoded_entry = self._encode_entry(entry)
- self.data_collector.store_log(encoded_entry)
- except Exception as e:
- get_logger().error(f'Error storing entry in FloExecutionLogger: {e}')
-
- def on_llm_start(
- self,
- serialized: dict[str, Any],
- prompts: list[str],
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- tags: Optional[list[str]] = None,
- metadata: Optional[dict[str, Any]] = None,
- **kwargs: Any,
- ) -> None:
- self.prompt[str(run_id)] = prompts
-
- def on_chain_start(
- self,
- serialized: Dict[str, Any],
- inputs: Dict[str, Any],
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- chain_name = (
- serialized.get('name', 'unnamed_chain') if serialized else 'unnamed_chain'
- )
-
- if parent_run_id and chain_name != 'agent_chain':
- return
- if isinstance(inputs, dict):
- user_input = inputs.get('messages', {})
- else:
- user_input = {}
- if (
- user_input
- and len(user_input) > 0
- and isinstance(user_input[0], HumanMessage)
- ):
- if isinstance(user_input[0], HumanMessage):
- self.query = user_input[0].content
-
- self.runs[str(run_id)] = {
- 'type': 'chain',
- 'start_time': datetime.utcnow(),
- 'inputs': inputs,
- 'name': chain_name,
- 'run_id': str(run_id),
- 'parent_run_id': str(parent_run_id) if parent_run_id else None,
- }
-
- def on_chain_end(
- self,
- outputs: Dict[str, Any],
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- if str(run_id) in self.runs:
- run_info = self.runs[str(run_id)]
- if run_info['type'] != 'chain' and run_info['type'] != 'llm':
- return
- run_info['end_time'] = datetime.utcnow()
- run_info['outputs'] = outputs
- run_info['status'] = 'completed'
- run_info['parent_run_id'] = str(parent_run_id) if parent_run_id else None
- run_info['prompt'] = (
- self.prompt[str(run_id)] if str(run_id) in self.prompt else []
- )
- self._store_entry(run_info)
- del self.runs[str(run_id)]
- else:
- if isinstance(outputs, ChatPromptValue) or isinstance(outputs, AgentFinish):
- run_info = {}
- run_info['type'] = 'llm'
- run_info['end_time'] = datetime.utcnow()
- run_info['inputs'] = outputs
- run_info['status'] = 'completed'
- run_info['run_id'] = str(run_id)
- run_info['parent_run_id'] = (
- str(parent_run_id) if parent_run_id else None
- )
- self.runs[str(parent_run_id)] = run_info
-
- def on_chain_error(
- self,
- error: Exception,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- pass
-
- def on_tool_start(
- self,
- serialized: Dict[str, Any],
- input_str: str,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- tags: Optional[list[str]] = None,
- metadata: Optional[dict[str, Any]] = None,
- inputs: Optional[dict[str, Any]] = None,
- **kwargs: Any,
- ) -> None:
- self.runs[str(run_id)] = {
- 'type': 'tool',
- 'query': self.query,
- 'start_time': datetime.utcnow(),
- 'tool_name': serialized.get('name', 'unnamed_tool'),
- 'input': input_str,
- 'parent_run_id': str(parent_run_id) if parent_run_id else None,
- }
-
- def on_tool_end(
- self,
- output: str,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- tags: Optional[list[str]] = None,
- **kwargs: Any,
- ) -> None:
- if str(run_id) in self.runs:
- run_info = self.runs[str(run_id)]
- run_info['end_time'] = datetime.utcnow()
- run_info['output'] = output
- run_info['status'] = 'completed'
- self._store_entry(run_info)
- del self.runs[str(run_id)]
-
- def on_tool_error(
- self,
- error: Exception,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- if str(run_id) in self.runs:
- run_info = self.runs[str(run_id)]
- run_info['end_time'] = datetime.utcnow()
- run_info['error'] = str(error)
- run_info['status'] = 'error'
- self._store_entry(run_info)
- del self.runs[str(run_id)]
-
- def on_agent_action(
- self,
- action: AgentAction,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- agent_info = {
- 'type': 'agent_action',
- 'start_time': datetime.utcnow(),
- 'tool': action.tool,
- 'tool_input': action.tool_input,
- 'log': action.log,
- 'parent_run_id': str(parent_run_id) if parent_run_id else None,
- }
- self.runs[str(run_id)] = agent_info
- self._store_entry(agent_info)
-
- def on_agent_finish(
- self,
- finish: AgentFinish,
- *,
- run_id: UUID,
- parent_run_id: Optional[UUID] = None,
- **kwargs: Any,
- ) -> None:
- log_entry = {
- 'type': 'agent_finish',
- 'time': datetime.utcnow(),
- 'output': finish.return_values,
- 'log': finish.log,
- 'parent_run_id': str(parent_run_id) if parent_run_id else None,
- }
- self._store_entry(log_entry)
-
- def log_all_tools(self, session_tools):
- try:
- tools = []
-
- for val in session_tools:
- tool_name = session_tools[val].name
- if tool_name not in self.added_tools:
- tools.append(
- {
- 'tool_name': tool_name,
- 'description': session_tools.get(val).description,
- 'args': session_tools.get(val).args,
- }
- )
- self.added_tools.add(tool_name)
-
- encoded_entry = self._encode_entry(tools)
- if encoded_entry:
- self.data_collector.store_tool_log(encoded_entry)
- except Exception as e:
- get_logger().error(f'Error storing tool in FloExecutionLogger: {e}')
diff --git a/flo_ai/flo_ai/common/__init__.py b/flo_ai/flo_ai/common/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flo_ai/flo_ai/common/flo_langchain_logger.py b/flo_ai/flo_ai/common/flo_langchain_logger.py
deleted file mode 100644
index 216fd8ae..00000000
--- a/flo_ai/flo_ai/common/flo_langchain_logger.py
+++ /dev/null
@@ -1,68 +0,0 @@
-from typing import Any, Dict, List, Union
-from langchain.callbacks.base import BaseCallbackHandler
-from langchain.schema import AgentAction, AgentFinish, LLMResult
-from flo_ai.common.flo_logger import get_logger
-from flo_ai.callbacks.flo_callbacks import FloToolCallback
-
-
-class FloLangchainLogger(BaseCallbackHandler):
- def __init__(self, session_id: str, tool_callbacks: List[FloToolCallback] = []):
- self.session_id = session_id
- self.tool_callbacks = tool_callbacks
-
- def on_llm_start(
- self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
- ) -> None:
- get_logger().debug(f'onLLMStart: {prompts}', self)
-
- def on_llm_new_token(self, token: str, **kwargs: Any) -> None:
- get_logger().debug(f'onNewToken: {token}', self)
-
- def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
- get_logger().debug(f'onLLMEnd: {response.generations}', self)
-
- def on_llm_error(
- self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
- ) -> None:
- get_logger().debug(f'onLLMEnd: {error}', self)
-
- def on_chain_start(
- self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
- ) -> None:
- get_logger().debug(f'onChainStart: {inputs}', self)
-
- def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> None:
- get_logger().debug(f'onChainEnd: {outputs}', self)
-
- def on_chain_error(
- self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
- ) -> None:
- get_logger().debug(f'onChainError: {error}', self)
-
- def on_tool_start(
- self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
- ) -> None:
- get_logger().debug(f'onToolStart: {input_str}', self)
- [
- x.on_tool_start(serialized['name'], kwargs['inputs'], kwargs)
- for x in self.tool_callbacks
- ]
-
- def on_tool_end(self, output: str, **kwargs: Any) -> None:
- get_logger().debug(f'onToolEnd: {output}', self)
- [x.on_tool_end(kwargs['name'], output, kwargs) for x in self.tool_callbacks]
-
- def on_tool_error(
- self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
- ) -> None:
- get_logger().debug(f'onToolError: {error}', self)
- [x.on_tool_error(kwargs['name'], error, kwargs) for x in self.tool_callbacks]
-
- def on_text(self, text: str, **kwargs: Any) -> None:
- get_logger().debug(f'onText: {text}', self)
-
- def on_agent_action(self, action: AgentAction, **kwargs: Any) -> Any:
- get_logger().debug(f'onAgentAction: {action.tool} - {action.tool_input}', self)
-
- def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> None:
- get_logger().debug(f'onAgentFinish: {finish.return_values}', self)
diff --git a/flo_ai/flo_ai/common/flo_logger.py b/flo_ai/flo_ai/common/flo_logger.py
deleted file mode 100644
index a1610b57..00000000
--- a/flo_ai/flo_ai/common/flo_logger.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import logging
-from typing import Any
-from typing import Dict, Optional, Union
-from logging.handlers import RotatingFileHandler
-from dataclasses import dataclass
-
-DEFAULT_LOGGER_NAME = 'FloAI'
-DEFAULT_LOG_LEVEL = 'ERROR'
-
-LEVEL_MAP = {
- 'DEBUG': logging.DEBUG,
- 'INFO': logging.INFO,
- 'WARNING': logging.WARNING,
- 'ERROR': logging.ERROR,
- 'CRITICAL': logging.CRITICAL,
-}
-
-
-@dataclass
-class FloLogConfig:
- name: str
- level: Union[str, int] = DEFAULT_LOG_LEVEL
- file_path: str = None
- max_bytes: int = 1048576
-
- def get_level(self) -> int:
- """Convert string level to logging level integer if needed"""
- if isinstance(self.level, str):
- return LEVEL_MAP.get(self.level.upper(), logging.ERROR)
- return self.level
-
-
-class FloLoggerUtil(logging.Logger):
- def __init__(self, config: FloLogConfig):
- level = config.get_level()
- super().__init__(config.name, level)
- self.setLevel(level)
- for handler in self.handlers:
- self.removeHandler(handler)
- self.setConfig(config)
-
- def setConfig(self, config: FloLogConfig):
- formatter = logging.Formatter(
- '%(asctime)s - %(name)s - %(session)s - %(levelname)s - %(message)s'
- )
-
- console_handler = logging.StreamHandler()
- console_handler.setFormatter(formatter)
- console_handler.setLevel(self.level)
- self.addHandler(console_handler)
-
- if config.file_path:
- file_handler = RotatingFileHandler(
- config.file_path, maxBytes=config.max_bytes
- )
- file_handler.setFormatter(formatter)
- file_handler.setLevel(self.level)
- self.addHandler(file_handler)
-
- def setLevel(self, level: Union[str, int]) -> None:
- if isinstance(level, str):
- level = LEVEL_MAP.get(level.upper(), logging.ERROR)
- super().setLevel(level)
- for handler in self.handlers:
- print('Setting levels in handler: ' + str(level))
- handler.setLevel(level)
-
- def _log(
- self, level: int, msg: str, session: Optional[Any] = None, *args, **kwargs
- ):
- if not self.isEnabledFor(level):
- return
- if kwargs.get('extra') is None:
- kwargs['extra'] = {}
- kwargs['extra']['session'] = f'[{session.session_id}]' if session else '[-]'
- super()._log(level, msg, args, **kwargs)
-
- def debug(self, msg: str, session: Optional[Any] = None, *args, **kwargs):
- self._log(logging.DEBUG, msg, session, *args, **kwargs)
-
- def info(self, msg: str, session: Optional[Any] = None, *args, **kwargs):
- self._log(logging.INFO, msg, session, *args, **kwargs)
-
- def warning(self, msg: str, session: Optional[Any] = None, *args, **kwargs):
- self._log(logging.WARNING, msg, session, *args, **kwargs)
-
- def error(self, msg: str, session: Optional[Any] = None, *args, **kwargs):
- self._log(logging.ERROR, msg, session, *args, **kwargs)
-
- def critical(self, msg: str, session: Optional[Any] = None, *args, **kwargs):
- self._log(logging.CRITICAL, msg, session, *args, **kwargs)
-
-
-logging_cache: Dict[str, FloLoggerUtil] = dict(
- {
- DEFAULT_LOGGER_NAME: FloLoggerUtil(
- FloLogConfig(DEFAULT_LOGGER_NAME, DEFAULT_LOG_LEVEL)
- )
- }
-)
-
-
-def get_logger(
- config: FloLogConfig = FloLogConfig(DEFAULT_LOGGER_NAME),
-) -> FloLoggerUtil:
- if config.name not in logging_cache:
- logging_cache[config.name] = FloLoggerUtil(config)
- return logging_cache[config.name]
-
-
-def set_log_level_internal(level: Union[str, int]) -> None:
- updated_logger = FloLoggerUtil(FloLogConfig(DEFAULT_LOGGER_NAME, level))
- logging_cache[DEFAULT_LOGGER_NAME] = updated_logger
-
-
-def set_log_config_internal(config: FloLogConfig):
- updated_logger = FloLoggerUtil(config)
- logging_cache[DEFAULT_LOGGER_NAME] = updated_logger
-
-
-def set_logger_internal(logger: logging.Logger):
- logging_cache[DEFAULT_LOGGER_NAME] = logger
diff --git a/flo_ai/flo_ai/constants/__init__.py b/flo_ai/flo_ai/constants/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flo_ai/flo_ai/constants/common_constants.py b/flo_ai/flo_ai/constants/common_constants.py
deleted file mode 100644
index f0b809a2..00000000
--- a/flo_ai/flo_ai/constants/common_constants.py
+++ /dev/null
@@ -1,4 +0,0 @@
-DOCUMENTATION_WEBSITE = 'https://flo-ai.rootflo.ai'
-DOCUMENTATION_ROUTER_ANCHOR = f'{DOCUMENTATION_WEBSITE}/basics/routers'
-DOCUMENTATION_AGENT_ANCHOR = f'{DOCUMENTATION_WEBSITE}/basics/agents'
-DOCUMENTATION_AGENT_TOOLS = f'{DOCUMENTATION_WEBSITE}/basics/tools'
diff --git a/flo_ai/flo_ai/constants/flo_node_contants.py b/flo_ai/flo_ai/constants/flo_node_contants.py
deleted file mode 100644
index e9d17bcc..00000000
--- a/flo_ai/flo_ai/constants/flo_node_contants.py
+++ /dev/null
@@ -1,2 +0,0 @@
-INTERNAL_NODE_REFLECTION_MANAGER = 'f/ReflectionManager'
-INTERNAL_NODE_DELEGATION_MANAGER = 'f/DelegationManager'
diff --git a/flo_ai/flo_ai/constants/prompt_constants.py b/flo_ai/flo_ai/constants/prompt_constants.py
deleted file mode 100644
index c8ca9388..00000000
--- a/flo_ai/flo_ai/constants/prompt_constants.py
+++ /dev/null
@@ -1 +0,0 @@
-FLO_FINISH = 'FINISH'
diff --git a/flo_ai/flo_ai/core.py b/flo_ai/flo_ai/core.py
deleted file mode 100644
index e8204114..00000000
--- a/flo_ai/flo_ai/core.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import asyncio
-import warnings
-import logging
-from typing import Optional
-from langchain_core.runnables import Runnable
-from flo_ai.yaml.config import to_supervised_team
-from flo_ai.builders.yaml_builder import build_supervised_team
-from typing import Any, Iterator, Union
-from flo_ai.router.flo_router import FloRouter
-from flo_ai.state.flo_session import FloSession
-from flo_ai.models.flo_executable import ExecutableFlo
-from flo_ai.error.flo_exception import FloException
-from flo_ai.constants.common_constants import DOCUMENTATION_WEBSITE
-from flo_ai.common.flo_logger import (
- get_logger,
- set_log_level_internal,
- set_log_config_internal,
- set_logger_internal,
- FloLogConfig,
-)
-from flo_ai.models.flo_agent import FloAgent
-from flo_ai.models.flo_base_agent import FloBaseAgent
-from langchain.tools import StructuredTool
-from flo_ai.router.flo_agent_router import FloAgentRouter
-from flo_ai.callbacks.flo_execution_logger import ToolLogger
-
-
-class Flo:
- def __init__(self, session: FloSession, executable: Runnable) -> None:
- self.session = session
- self.runnable: ExecutableFlo = executable
-
- self.langchain_logger = session.langchain_logger
- get_logger().info('Flo instance created ...', session)
-
- def stream(self, query, config=None) -> Iterator[Union[dict[str, Any], Any]]:
- self.validate_invoke(self.session)
- get_logger().info(f"streaming query requested: '{query}'", self.session)
- return self.runnable.stream(query, config)
-
- def async_stream(self, query, config=None) -> Iterator[Union[dict[str, Any], Any]]:
- get_logger().info(f"Streaming async query requested: '{query}'", self.session)
- return self.runnable.astream(query, config)
-
- def invoke(self, query, config=None) -> Iterator[Union[dict[str, Any], Any]]:
- config = self.session.prepare_config(config)
-
- for callback in self.session.callbacks:
- if isinstance(callback, ToolLogger):
- callback.log_all_tools(self.session.tools)
-
- self.validate_invoke(self.session)
- get_logger().info(f"Invoking query: '{query}'", self.session)
- return self.runnable.invoke(query, config)
-
- def async_invoke(self, query, config=None) -> Iterator[Union[dict[str, Any], Any]]:
- get_logger().info(f"Invoking async query: '{query}'", self.session)
- return self.runnable.ainvoke(query, config)
-
- @staticmethod
- def build(
- session: FloSession,
- yaml: Optional[str] = None,
- yaml_path: Optional[str] = None,
- routed_team: Optional[FloRouter] = None,
- log_level: Optional[str] = None,
- ):
- if log_level:
- warnings.warn(
- '`log_level` is deprecated and will be removed in a future version. '
- 'Please use `Flo.set_log_level()` instead.',
- DeprecationWarning,
- stacklevel=2,
- )
- Flo.set_log_level(log_level)
- if yaml_path:
- if yaml is not None:
- raise FloException(
- 'Cannot specify both `yaml` and `yaml_path`. Use only one.'
- )
- try:
- with open(yaml_path) as file:
- yaml = file.read()
- except FileNotFoundError:
- raise FloException(f'YAML file at path {yaml_path} not found.')
- except Exception:
- raise FloException(f'Error reading YAML file at path {yaml_path}.')
-
- if yaml is not None:
- get_logger().info('Building Flo instance from YAML ...', session)
- executable: ExecutableFlo = build_supervised_team(
- session, to_supervised_team(yaml)
- )
- return Flo(session, executable)
- if routed_team is not None:
- return Flo(session, routed_team.build_routed_team())
- raise FloException("""Either yaml or routed_team should be not None""")
-
- @staticmethod
- def create(session: FloSession, routed_team: Union[FloRouter, FloAgent]):
- if isinstance(routed_team, FloRouter):
- runnable = routed_team.build_routed_team()
- if isinstance(routed_team, FloBaseAgent):
- agent_router = FloAgentRouter.Builder(
- session,
- f'router-{routed_team.name}',
- flo_agent=routed_team,
- ).build()
- runnable = agent_router.build_routed_team()
- return Flo(session, runnable)
-
- @staticmethod
- def set_log_level(log_level: str):
- set_log_level_internal(log_level)
-
- @staticmethod
- def set_log_config(logging_config: FloLogConfig):
- set_log_config_internal(logging_config)
-
- @staticmethod
- def set_logger(logging_config: logging.Logger):
- set_logger_internal(logging_config)
-
- def draw(self, xray=True):
- from IPython.display import Image, display
-
- image = self.runnable.draw(xray)
- return display(Image(self.runnable.draw(xray))) if image is not None else None
-
- def draw_to_file(self, filename: str, xray=True):
- from PIL import Image as PILImage
- import io
-
- byte_image = self.runnable.draw(xray)
- with io.BytesIO(byte_image) as image_io:
- image = PILImage.open(image_io)
- image.save(filename)
-
- def validate_invoke(self, session: FloSession):
- async_coroutines = filter(
- lambda x: (
- isinstance(x, StructuredTool)
- and hasattr(x, 'coroutine')
- and asyncio.iscoroutinefunction(x.coroutine)
- ),
- session.tools.values(),
- )
- async_tools = list(async_coroutines)
- if len(async_tools) > 0:
- raise FloException(
- f"""You seem to have atleast one async tool registered in this session. Please use flo.async_invoke or flo.async_stream. Checkout {DOCUMENTATION_WEBSITE}"""
- )
diff --git a/flo_ai/flo_ai/error/flo_exception.py b/flo_ai/flo_ai/error/flo_exception.py
deleted file mode 100644
index 2418b521..00000000
--- a/flo_ai/flo_ai/error/flo_exception.py
+++ /dev/null
@@ -1,16 +0,0 @@
-class FloException(Exception):
- def __init__(self, message: str, error_code: int = -1):
- """
- Initialize the FloException with a message and optional error code.
-
- :param message: Error message to be displayed.
- :param error_code: Optional error code to be associated with the exception.
- """
- self.message = message
- self.error_code = error_code
- super().__init__(message)
-
- def __str__(self):
- if self.error_code is not None:
- return f'[Error {self.error_code}] {self.message}'
- return self.message
diff --git a/flo_ai/flo_ai/factory/agent_factory.py b/flo_ai/flo_ai/factory/agent_factory.py
deleted file mode 100644
index 60eae202..00000000
--- a/flo_ai/flo_ai/factory/agent_factory.py
+++ /dev/null
@@ -1,151 +0,0 @@
-import json
-from typing import Optional
-from flo_ai.state.flo_session import FloSession
-from flo_ai.yaml.config import AgentConfig, Parser
-from flo_ai.models.flo_agent import FloAgent
-from flo_ai.models.flo_llm_agent import FloLLMAgent
-from flo_ai.models.flo_reflection_agent import FloReflectionAgent
-from flo_ai.models.flo_delegation_agent import FloDelegatorAgent
-from flo_ai.models.flo_tool_agent import FloToolAgent
-from flo_ai.error.flo_exception import FloException
-from flo_ai.models.delegate import Delegate
-from flo_ai.constants.common_constants import DOCUMENTATION_AGENT_ANCHOR
-from enum import Enum
-from flo_ai.parsers.flo_json_parser import FloJsonParser
-
-
-class AgentKinds(Enum):
- agentic = 'agentic'
- llm = 'llm'
- tool = 'tool'
- function = 'function'
- reflection = 'reflection'
- delegator = 'delegator'
-
-
-class AgentFactory:
- @staticmethod
- def create(session: FloSession, agent: AgentConfig):
- kind = agent.kind
- tool_map = session.tools
- if kind is not None:
- agent_kind = getattr(AgentKinds, kind, None)
- if agent_kind is None:
- raise FloException(f"""Unknown agent kind: `{kind}`. The supported types are llm, tool, reflection, delegator or agentic.
- Check the documentation @ {DOCUMENTATION_AGENT_ANCHOR}""")
- if agent_kind == AgentKinds.llm:
- return AgentFactory.__create_llm_agent(session, agent)
- elif agent_kind == AgentKinds.tool:
- return AgentFactory.__create_runnable_agent(session, agent)
- elif agent_kind == AgentKinds.reflection:
- return AgentFactory.__create_reflection_agent(session, agent)
- elif agent_kind == AgentKinds.delegator:
- return AgentFactory.__create_delegator_agent(session, agent)
-
- return AgentFactory.__create_agentic_agent(session, agent, tool_map)
-
- @staticmethod
- def __resolve_model(session: FloSession, model_name: Optional[str] = None):
- if model_name is None:
- return session.llm
- if model_name not in session.models:
- raise FloException(
- f"""Model not found: {model_name}.
- The model you would like to use should be registered to the session using session.register_model api,
- and the same model name should be used here instead of `{model_name}`"""
- )
- return session.models[model_name]
-
- @staticmethod
- def __create_agentic_agent(
- session: FloSession, agent: AgentConfig, tool_map
- ) -> FloAgent:
- agent_model = AgentFactory.__resolve_model(session, agent.model)
- dc = (
- session.data_collectors[agent.data_collector]
- if agent.data_collector is not None
- else None
- )
- tools = [tool_map[tool.name] for tool in agent.tools]
- if isinstance(agent.parser, Parser):
- parser = FloJsonParser.create(
- json_dict=json.loads(agent.parser.model_dump_json())
- )
- else:
- parser = session.parsers[agent.parser] if agent.parser is not None else None
- flo_agent: FloAgent = FloAgent.Builder(
- session,
- name=agent.name,
- job=agent.job,
- tools=tools,
- role=agent.role,
- llm=agent_model,
- on_error=session.on_agent_error,
- model_name=agent.model,
- parser=parser,
- data_collector=dc,
- ).build()
- return flo_agent
-
- @staticmethod
- def __create_llm_agent(session: FloSession, agent: AgentConfig) -> FloLLMAgent:
- agent_model = AgentFactory.__resolve_model(session, agent.model)
- dc = (
- session.data_collectors[agent.data_collector]
- if agent.data_collector is not None
- else None
- )
- if isinstance(agent.parser, Parser):
- parser = FloJsonParser.create(
- json_dict=json.loads(agent.parser.model_dump_json())
- )
- else:
- parser = session.parsers[agent.parser] if agent.parser is not None else None
- builder = FloLLMAgent.Builder(
- session,
- name=agent.name,
- job=agent.job,
- role=agent.role,
- llm=agent_model,
- model_name=agent.model,
- parser=parser,
- data_collector=dc,
- )
- llm_agent: FloLLMAgent = builder.build()
- return llm_agent
-
- @staticmethod
- def __create_runnable_agent(session: FloSession, agent: AgentConfig) -> FloLLMAgent:
- runnable = session.tools[agent.tools[0].name]
- return FloToolAgent.Builder(
- session, agent.name, runnable, model_name=agent.model
- ).build()
-
- @staticmethod
- def __create_reflection_agent(
- session: FloSession, agent: AgentConfig
- ) -> FloReflectionAgent:
- agent_model = AgentFactory.__resolve_model(session, agent.model)
- return FloReflectionAgent.Builder(
- session,
- name=agent.name,
- job=agent.job,
- role=agent.role,
- llm=agent_model,
- to=Delegate([x.name for x in agent.to], agent.retry),
- model_name=agent.model,
- ).build()
-
- @staticmethod
- def __create_delegator_agent(
- session: FloSession, agent: AgentConfig
- ) -> FloReflectionAgent:
- agent_model = AgentFactory.__resolve_model(session, agent.model)
- return FloDelegatorAgent.Builder(
- session,
- agent.name,
- agent.job,
- delegate=Delegate([x.name for x in agent.to], agent.retry),
- llm=agent_model,
- model_name=agent.model,
- ).build()
diff --git a/flo_ai/flo_ai/parsers/flo_json_parser.py b/flo_ai/flo_ai/formatter/yaml_format_parser.py
similarity index 64%
rename from flo_ai/flo_ai/parsers/flo_json_parser.py
rename to flo_ai/flo_ai/formatter/yaml_format_parser.py
index a060c9fc..d29e4488 100644
--- a/flo_ai/flo_ai/parsers/flo_json_parser.py
+++ b/flo_ai/flo_ai/formatter/yaml_format_parser.py
@@ -1,11 +1,9 @@
import json
import csv
+import yaml
from io import StringIO
-from flo_ai.parsers.flo_parser import FloParser
from typing import List, Dict, Any, Optional, Literal
from pydantic import BaseModel, Field, create_model
-from flo_ai.error.flo_exception import FloException
-from langchain_core.output_parsers import PydanticOutputParser
from dataclasses import dataclass
@@ -15,7 +13,7 @@ class ParseContract:
fields: List[Dict[str, Any]]
-class FloJsonParser(FloParser):
+class FloJsonParser:
def __init__(self, parse_contract: ParseContract):
self.contract = parse_contract
self._cached_models = {}
@@ -128,11 +126,6 @@ def __create_contract_from_json(self) -> BaseModel:
DynamicModel = create_model(self.contract.name, **pydantic_fields)
return DynamicModel
- def get_format_instructions(self):
- return PydanticOutputParser(
- pydantic_object=self.__create_contract_from_json()
- ).get_format_instructions()
-
@staticmethod
def create(json_dict: Optional[Dict] = None, json_path: Optional[str] = None):
return FloJsonParser.Builder(json_dict=json_dict, json_path=json_path).build()
@@ -142,7 +135,7 @@ def __init__(
self, json_dict: Optional[Dict] = None, json_path: Optional[str] = None
):
if json_dict is None and json_path is None:
- raise FloException(
+ raise ValueError(
'Either of json_dict or json_path is required to build a FloJsonParser'
)
self.json_dict = json_dict
@@ -158,3 +151,74 @@ def build(self):
name = json_contract['name']
fields = json_contract['fields']
return FloJsonParser(ParseContract(name=name, fields=fields))
+
+
+class FloYamlParser(FloJsonParser):
+ """
+ A parser class that handles YAML-based parser definitions for Flo agents.
+ Extends FloJsonParser to reuse the model creation logic while adding YAML-specific functionality.
+ """
+
+ @staticmethod
+ def create(yaml_dict: Optional[Dict] = None, yaml_path: Optional[str] = None):
+ """
+ Create a FloYamlParser instance from either a YAML dictionary or a YAML file path.
+
+ Args:
+ yaml_dict: A dictionary containing the YAML parser definition
+ yaml_path: Path to a YAML file containing the parser definition
+
+ Returns:
+ FloYamlParser: A configured parser instance
+ """
+ return FloYamlParser.Builder(yaml_dict=yaml_dict, yaml_path=yaml_path).build()
+
+ class Builder:
+ def __init__(
+ self, yaml_dict: Optional[Dict] = None, yaml_path: Optional[str] = None
+ ):
+ if yaml_dict is None and yaml_path is None:
+ raise ValueError(
+ 'Either yaml_dict or yaml_path is required to build a FloYamlParser'
+ )
+ self.yaml_dict = yaml_dict
+ self.yaml_path = yaml_path
+
+ def build(self):
+ if self.yaml_dict:
+ parser_def = self.yaml_dict
+ else:
+ with open(self.yaml_path) as f:
+ parser_def = yaml.safe_load(f)
+
+ # Extract parser definition from agent YAML
+ if 'agent' in parser_def and 'parser' in parser_def['agent']:
+ parser_def = parser_def['agent']['parser']
+
+ # Extract required fields
+ name = parser_def['name']
+ fields = parser_def['fields']
+
+ # Process fields to handle examples and required flag
+ processed_fields = []
+ for field in fields:
+ processed_field = field.copy()
+
+ # Handle examples in literal values
+ if field['type'] == 'literal' and 'values' in field:
+ for value in field['values']:
+ if 'examples' in value:
+ # Add examples to description
+ examples_str = '\nExamples:\n' + '\n'.join(
+ f'- {ex}' for ex in value['examples']
+ )
+ value['description'] = value['description'] + examples_str
+ del value['examples']
+
+ # Remove required flag as it's not used in model creation
+ if 'required' in processed_field:
+ del processed_field['required']
+
+ processed_fields.append(processed_field)
+
+ return FloYamlParser(ParseContract(name=name, fields=processed_fields))
diff --git a/flo_ai/flo_ai/helpers/utils.py b/flo_ai/flo_ai/helpers/utils.py
deleted file mode 100644
index ed4be8c3..00000000
--- a/flo_ai/flo_ai/helpers/utils.py
+++ /dev/null
@@ -1,13 +0,0 @@
-import random
-import string
-
-
-def random_str(length: int = 5):
- letters = string.ascii_letters + string.digits
- result_str = ''.join(random.choice(letters) for i in range(length))
- return result_str
-
-
-def rotate_array(nums, k: int = 1):
- k = k % len(nums)
- return nums[k:] + nums[:k]
diff --git a/flo_ai/flo_ai/llm/anthropic_llm.py b/flo_ai/flo_ai/llm/anthropic_llm.py
new file mode 100644
index 00000000..ffb0dbb8
--- /dev/null
+++ b/flo_ai/flo_ai/llm/anthropic_llm.py
@@ -0,0 +1,111 @@
+from typing import Dict, Any, List, Optional
+from anthropic import AsyncAnthropic
+import json
+from .base_llm import BaseLLM
+from flo_ai.tool.base_tool import Tool
+
+
+class Anthropic(BaseLLM):
+ def __init__(
+ self,
+ model: str = 'claude-3-5-sonnet-20240620',
+ temperature: float = 0.7,
+ api_key: Optional[str] = None,
+ **kwargs,
+ ):
+ super().__init__(model, api_key, temperature, **kwargs)
+ self.client = AsyncAnthropic(api_key=self.api_key)
+
+ async def generate(
+ self,
+ messages: List[Dict[str, str]],
+ functions: Optional[List[Dict[str, Any]]] = None,
+ output_schema: Optional[Dict[str, Any]] = None,
+ ) -> Dict[str, Any]:
+ # Convert messages to Claude format
+ system_message = next(
+ (msg['content'] for msg in messages if msg['role'] == 'system'), None
+ )
+
+ # If output schema is provided, append it to system message
+ if output_schema and system_message:
+ system_message = f'{system_message}\n\nProvide output in the following JSON schema:\n{json.dumps(output_schema, indent=2)}\n\nResponse:'
+ elif output_schema:
+ system_message = f'Provide output in the following JSON schema:\n{json.dumps(output_schema, indent=2)}\n\nResponse:'
+
+ conversation = []
+ for msg in messages:
+ if msg['role'] != 'system':
+ conversation.append(
+ {
+ 'role': 'assistant' if msg['role'] == 'assistant' else 'user',
+ 'content': msg['content'],
+ }
+ )
+
+ try:
+ kwargs = {
+ 'model': self.model,
+ 'messages': conversation,
+ 'temperature': self.temperature,
+ 'max_tokens': 8192,
+ **self.kwargs,
+ }
+
+ if system_message:
+ kwargs['system'] = system_message
+
+ if functions:
+ kwargs['tools'] = functions
+
+ response = await self.client.messages.create(**kwargs)
+
+ # Check if there's a tool use in the response
+ for content_block in response.content:
+ if content_block.type == 'tool_use':
+ return {
+ 'content': response.content[0].text if response.content else '',
+ 'function_call': {
+ 'name': content_block.name,
+ 'arguments': json.dumps(content_block.input),
+ },
+ }
+
+ # Handle regular text response
+ return {'content': response.content[0].text if response.content else ''}
+
+ except Exception as e:
+ raise Exception(f'Error in Claude API call: {str(e)}')
+
+ def get_message_content(self, response: Any) -> str:
+ """Extract message content from response"""
+ if isinstance(response, dict):
+ return response.get('content', '')
+ return str(response)
+
+ def format_tool_for_llm(self, tool: 'Tool') -> Dict[str, Any]:
+ """Format a single tool for Claude's API"""
+ return {
+ 'type': 'custom',
+ 'name': tool.name,
+ 'description': tool.description,
+ 'input_schema': {
+ 'type': 'object',
+ 'properties': {
+ name: {
+ 'type': info.get('type', 'string'),
+ 'description': info.get('description', ''),
+ }
+ for name, info in tool.parameters.items()
+ },
+ 'required': [
+ name
+ for name, info in tool.parameters.items()
+ if info.get('required', True)
+ ],
+ },
+ }
+
+ def format_tools_for_llm(self, tools: List['Tool']) -> List[Dict[str, Any]]:
+ """Format tools for Claude's API"""
+ return [self.format_tool_for_llm(tool) for tool in tools]
diff --git a/flo_ai/flo_ai/llm/base_llm.py b/flo_ai/flo_ai/llm/base_llm.py
new file mode 100644
index 00000000..0942db57
--- /dev/null
+++ b/flo_ai/flo_ai/llm/base_llm.py
@@ -0,0 +1,52 @@
+from abc import ABC, abstractmethod
+from typing import Dict, Any, List, Optional
+from flo_ai.tool.base_tool import Tool
+
+
+class BaseLLM(ABC):
+ def __init__(
+ self, model: str, api_key: str = None, temperature: float = 0.7, **kwargs
+ ):
+ self.model = model
+ self.api_key = api_key
+ self.temperature = temperature
+ self.kwargs = kwargs
+
+ @abstractmethod
+ async def generate(
+ self,
+ messages: List[Dict[str, str]],
+ functions: Optional[List[Dict[str, Any]]] = None,
+ ) -> Dict[str, Any]:
+ """Generate a response from the LLM"""
+ pass
+
+ async def get_function_call(
+ self, response: Dict[str, Any]
+ ) -> Optional[Dict[str, Any]]:
+ if hasattr(response, 'function_call') and response.function_call:
+ return {
+ 'name': response.function_call.name,
+ 'arguments': response.function_call.arguments,
+ }
+ elif isinstance(response, dict) and 'function_call' in response:
+ return {
+ 'name': response['function_call']['name'],
+ 'arguments': response['function_call']['arguments'],
+ }
+ return None
+
+ @abstractmethod
+ def get_message_content(self, response: Dict[str, Any]) -> str:
+ """Extract message content from response"""
+ pass
+
+ @abstractmethod
+ def format_tool_for_llm(self, tool: 'Tool') -> Dict[str, Any]:
+ """Format a tool for the specific LLM's API"""
+ pass
+
+ @abstractmethod
+ def format_tools_for_llm(self, tools: List['Tool']) -> List[Dict[str, Any]]:
+ """Format a list of tools for the specific LLM's API"""
+ pass
diff --git a/flo_ai/flo_ai/llm/ollama_llm.py b/flo_ai/flo_ai/llm/ollama_llm.py
new file mode 100644
index 00000000..d14d4d6c
--- /dev/null
+++ b/flo_ai/flo_ai/llm/ollama_llm.py
@@ -0,0 +1,98 @@
+from typing import Dict, Any, List, Optional
+import aiohttp
+import json
+from .base_llm import BaseLLM
+from flo_ai.tool.base_tool import Tool
+
+
+class OllamaLLM(BaseLLM):
+ def __init__(
+ self,
+ model: str = 'llama2',
+ api_key: str = None,
+ temperature: float = 0.7,
+ base_url: str = 'http://localhost:11434',
+ **kwargs,
+ ):
+ super().__init__(model, api_key, temperature, **kwargs)
+ self.base_url = base_url.rstrip('/')
+
+ async def generate(
+ self,
+ messages: List[Dict[str, str]],
+ functions: Optional[List[Dict[str, Any]]] = None,
+ output_schema: Optional[Dict[str, Any]] = None,
+ ) -> Dict[str, Any]:
+ # Convert messages to Ollama format
+ prompt = ''
+ for msg in messages:
+ role = msg['role']
+ content = msg['content']
+ if role == 'system':
+ prompt += f'System: {content}\n'
+ elif role == 'user':
+ prompt += f'User: {content}\n'
+ elif role == 'assistant':
+ prompt += f'Assistant: {content}\n'
+
+ # Add output schema instruction if provided
+ if output_schema:
+ prompt += f'\nPlease provide your response in JSON format according to this schema:\n{json.dumps(output_schema, indent=2)}\n'
+
+ # Prepare request payload
+ payload = {
+ 'model': self.model,
+ 'prompt': prompt,
+ 'temperature': self.temperature,
+ 'stream': False,
+ **self.kwargs,
+ }
+
+ # Add function information if provided
+ if functions:
+ payload['functions'] = functions
+
+ async with aiohttp.ClientSession() as session:
+ async with session.post(
+ f'{self.base_url}/api/generate', json=payload
+ ) as response:
+ if response.status != 200:
+ raise Exception(f'Ollama API error: {await response.text()}')
+
+ result = await response.json()
+ return {
+ 'content': result.get('response', ''),
+ 'function_call': result.get('function_call'),
+ }
+
+ def get_message_content(self, response: Any) -> str:
+ """Extract message content from response"""
+ if isinstance(response, dict):
+ return response.get('content', '')
+ return str(response)
+
+ def format_tool_for_llm(self, tool: 'Tool') -> Dict[str, Any]:
+ """Format a single tool for Ollama's API"""
+ return {
+ 'name': tool.name,
+ 'description': tool.description,
+ 'parameters': {
+ 'type': 'object',
+ 'properties': {
+ name: {
+ 'type': info.get('type', 'string'),
+ 'description': info.get('description', ''),
+ }
+ for name, info in tool.parameters.items()
+ },
+ 'required': [
+ name
+ for name, info in tool.parameters.items()
+ if info.get('required', True)
+ ],
+ },
+ }
+
+ def format_tools_for_llm(self, tools: List['Tool']) -> List[Dict[str, Any]]:
+ """Format tools for Ollama's API"""
+ return [self.format_tool_for_llm(tool) for tool in tools]
diff --git a/flo_ai/flo_ai/llm/openai_llm.py b/flo_ai/flo_ai/llm/openai_llm.py
new file mode 100644
index 00000000..6786b629
--- /dev/null
+++ b/flo_ai/flo_ai/llm/openai_llm.py
@@ -0,0 +1,90 @@
+from typing import Dict, Any, List
+from openai import AsyncOpenAI
+from .base_llm import BaseLLM
+from flo_ai.tool.base_tool import Tool
+
+
+class OpenAI(BaseLLM):
+ def __init__(
+ self,
+ model='gpt-40-mini',
+ api_key: str = None,
+ temperature: float = 0.7,
+ **kwargs,
+ ):
+ super().__init__(
+ model=model, api_key=api_key, temperature=temperature, **kwargs
+ )
+ self.client = AsyncOpenAI(api_key=api_key, **kwargs)
+ self.model = model
+ self.kwargs = kwargs
+
+ async def generate(
+ self, messages: list[dict], output_schema: dict = None, **kwargs
+ ) -> Any:
+ # Convert output_schema to OpenAI format if provided
+ if output_schema:
+ kwargs['response_format'] = {'type': 'json_object'}
+ kwargs['functions'] = [
+ {
+ 'name': output_schema.get('name', 'default'),
+ 'parameters': output_schema.get('schema', output_schema),
+ }
+ ]
+ kwargs['function_call'] = {'name': output_schema.get('name', 'default')}
+
+ # Add JSON format instruction to the system prompt
+ if messages and messages[0]['role'] == 'system':
+ messages[0]['content'] = (
+ messages[0]['content']
+ + '\n\nPlease provide your response in JSON format according to the specified schema.'
+ )
+ else:
+ messages.insert(
+ 0,
+ {
+ 'role': 'system',
+ 'content': 'Please provide your response in JSON format according to the specified schema.',
+ },
+ )
+
+ # Prepare OpenAI API parameters
+ openai_kwargs = {
+ 'model': self.model,
+ 'messages': messages,
+ **kwargs,
+ **self.kwargs,
+ }
+
+ # Make the API call
+ response = await self.client.chat.completions.create(**openai_kwargs)
+ message = response.choices[0].message
+
+ # Return the full message object instead of just the content
+ return message
+
+ def get_message_content(self, response: Dict[str, Any]) -> str:
+ # Handle both string responses and message objects
+ if isinstance(response, str):
+ return response
+ # Otherwise return content if available
+ return response.content if hasattr(response, 'content') else str(response)
+
+ def format_tool_for_llm(self, tool: 'Tool') -> Dict[str, Any]:
+ """Format a single tool for OpenAI's API"""
+ return {
+ 'name': tool.name,
+ 'description': tool.description,
+ 'parameters': {
+ 'type': 'object',
+ 'properties': {
+ name: {'type': info['type'], 'description': info['description']}
+ for name, info in tool.parameters.items()
+ },
+ 'required': list(tool.parameters.keys()),
+ },
+ }
+
+ def format_tools_for_llm(self, tools: List['Tool']) -> List[Dict[str, Any]]:
+ """Format tools for OpenAI's API"""
+ return [self.format_tool_for_llm(tool) for tool in tools]
diff --git a/flo_ai/flo_ai/models/agent.py b/flo_ai/flo_ai/models/agent.py
new file mode 100644
index 00000000..0b9f6c8c
--- /dev/null
+++ b/flo_ai/flo_ai/models/agent.py
@@ -0,0 +1,281 @@
+from typing import Dict, Any, List, Optional
+from flo_ai.models.base_agent import BaseAgent, AgentType, ReasoningPattern
+from flo_ai.llm.base_llm import BaseLLM
+from flo_ai.tool.base_tool import Tool, ToolExecutionError
+from flo_ai.models.agent_error import AgentError
+import json
+from flo_ai.utils.logger import logger
+
+
+class Agent(BaseAgent):
+ def __init__(
+ self,
+ name: str,
+ system_prompt: str,
+ llm: BaseLLM,
+ tools: Optional[List[Tool]] = None,
+ max_retries: int = 3,
+ reasoning_pattern: ReasoningPattern = ReasoningPattern.DIRECT,
+ output_schema: Optional[Dict[str, Any]] = None,
+ role: Optional[str] = None,
+ ):
+ # Determine agent type based on tools
+ agent_type = AgentType.TOOL_USING if tools else AgentType.CONVERSATIONAL
+
+ # Enhance system prompt with role if provided
+ enhanced_prompt = system_prompt
+ if role:
+ enhanced_prompt = f'You are {role}. {system_prompt}'
+
+ super().__init__(
+ name=name,
+ system_prompt=enhanced_prompt,
+ agent_type=agent_type,
+ llm=llm,
+ max_retries=max_retries,
+ )
+ self.tools = tools or []
+ self.tools_dict = {tool.name: tool for tool in self.tools}
+ self.reasoning_pattern = reasoning_pattern
+ self.output_schema = output_schema
+ self.role = role
+
+ async def run(self, input_text: str) -> str:
+ self.add_to_history('user', input_text)
+ retry_count = 0
+
+ # If no tools, act as conversational agent
+ if not self.tools:
+ return await self._run_conversational(retry_count)
+
+ # Otherwise, run as tool agent
+ return await self._run_with_tools(retry_count)
+
+ async def _run_conversational(self, retry_count: int) -> str:
+ """Run as a conversational agent when no tools are provided"""
+ while retry_count < self.max_retries:
+ try:
+ messages = [
+ {
+ 'role': 'system',
+ 'content': self._get_cot_prompt()
+ if self.reasoning_pattern == ReasoningPattern.COT
+ else self.system_prompt,
+ }
+ ] + self.conversation_history
+
+ logger.debug('Sending messages to LLM:', messages)
+ response = await self.llm.generate(
+ messages, output_schema=self.output_schema
+ )
+ logger.debug('Raw LLM Response:', response)
+
+ assistant_message = self.llm.get_message_content(response)
+ logger.debug('Extracted message:', assistant_message)
+
+ if assistant_message:
+ self.add_to_history('assistant', assistant_message)
+ return assistant_message
+ else:
+ possible_tool_message = await self.llm.get_function_call(response)
+ if possible_tool_message:
+ return possible_tool_message['arguments']
+ logger.debug('Warning: No message content found in response')
+ return None
+
+ except Exception as e:
+ retry_count += 1
+ context = {
+ 'conversation_history': self.conversation_history,
+ 'attempt': retry_count,
+ }
+
+ should_retry, analysis = await self.handle_error(e, context)
+
+ if should_retry and retry_count < self.max_retries:
+ self.add_to_history(
+ 'system', f'Error occurred. Analysis: {analysis}'
+ )
+ continue
+ else:
+ raise AgentError(
+ f'Failed after {retry_count} attempts. Last error: {analysis}',
+ original_error=e,
+ )
+
+ async def _run_with_tools(self, retry_count: int = 0) -> str:
+ """Run as a tool-using agent when tools are provided"""
+ while retry_count < self.max_retries:
+ try:
+ messages = [
+ {
+ 'role': 'system',
+ 'content': self._get_react_prompt()
+ if self.reasoning_pattern == ReasoningPattern.REACT
+ else self._get_cot_prompt()
+ if self.reasoning_pattern == ReasoningPattern.COT
+ else self.system_prompt,
+ }
+ ] + self.conversation_history
+
+ # Keep executing tools until we get a final answer
+ max_tool_calls = 5 # Limit the number of tool calls per query
+ tool_call_count = 0
+
+ while tool_call_count < max_tool_calls:
+ formatted_tools = self.llm.format_tools_for_llm(self.tools)
+ response = await self.llm.generate(
+ messages,
+ functions=formatted_tools,
+ output_schema=self.output_schema,
+ )
+
+ # Handle ReACT and CoT patterns
+ function_call = await self.llm.get_function_call(response)
+
+ # If no function call, we have our final answer
+ if not function_call:
+ assistant_message = self.llm.get_message_content(response)
+ if assistant_message:
+ self.add_to_history('assistant', assistant_message)
+ return assistant_message
+ break
+
+ # Execute the tool
+ try:
+ function_name = function_call['name']
+ function_args = json.loads(function_call['arguments'])
+
+ tool = self.tools_dict[function_name]
+ function_response = await tool.execute(**function_args)
+ tool_call_count += 1
+
+ # Add function call to history
+ self.add_to_history(
+ 'function',
+ f'Tool response: {str(function_response)}',
+ name=function_name,
+ )
+
+ # Add the function response to messages for context
+ messages.append(
+ {
+ 'role': 'function',
+ 'name': function_name,
+ 'content': str(function_response),
+ }
+ )
+
+ # Add a prompt to continue the reasoning
+ messages.append(
+ {
+ 'role': 'user',
+ 'content': 'Continue with your reasoning based on this result. What should be done next?',
+ }
+ )
+
+ except (json.JSONDecodeError, KeyError, ToolExecutionError) as e:
+ retry_count += 1
+ context = {
+ 'function_call': function_call,
+ 'attempt': retry_count,
+ }
+ should_retry, analysis = await self.handle_error(e, context)
+ if should_retry and retry_count < self.max_retries:
+ self.add_to_history(
+ 'system', f'Tool execution error: {analysis}'
+ )
+ continue
+ raise AgentError(
+ f'Tool execution failed: {analysis}', original_error=e
+ )
+
+ # Generate final response if we've hit the tool call limit or exited the loop
+ final_response = await self.llm.generate(
+ messages
+ + [
+ {
+ 'role': 'system',
+ 'content': 'Please provide a final answer based on all the tool results above.',
+ }
+ ],
+ output_schema=self.output_schema,
+ )
+
+ assistant_message = self.llm.get_message_content(final_response)
+ if assistant_message:
+ self.add_to_history('assistant', assistant_message)
+ return assistant_message
+
+ return f'The final result based on the tool executions is: {function_response}'
+
+ except Exception as e:
+ retry_count += 1
+ context = {
+ 'conversation_history': self.conversation_history,
+ 'attempt': retry_count,
+ }
+
+ should_retry, analysis = await self.handle_error(e, context)
+ if should_retry and retry_count < self.max_retries:
+ self.add_to_history(
+ 'system', f'Error occurred. Analysis: {analysis}'
+ )
+ continue
+
+ raise AgentError(
+ f'Failed after {retry_count} attempts. Last error: {analysis}',
+ original_error=e,
+ )
+
+ raise AgentError(f'Failed after maximum {self.max_retries} attempts.')
+
+ def _get_react_prompt(self) -> str:
+ """Get system prompt modified for ReACT pattern"""
+ tools_desc = '\n'.join(
+ [f'- {tool.name}: {tool.description}' for tool in self.tools]
+ )
+ react_prompt = f"""{self.system_prompt}
+ When solving tasks, follow this format:
+
+ Thought: Analyze the situation and think about what to do
+ Action: Use available tools in the format: tool_name(param1: "value1", param2: "value2")
+ Observation: The result of the action
+ ... (repeat Thought/Action/Observation if needed)
+
+ Available tools:
+ {tools_desc}
+
+ Remember to:
+ 1. Think carefully about what needs to be done
+ 2. Use tools when needed
+ 3. Make observations about tool results
+ 4. Conclude with a final answer when the task is complete"""
+
+ return react_prompt
+
+ def _get_cot_prompt(self) -> str:
+ """Get system prompt modified for Chain of Thought pattern"""
+ tools_desc = '\n'.join(
+ [f'- {tool.name}: {tool.description}' for tool in self.tools]
+ )
+ cot_prompt = f"""{self.system_prompt}
+ When solving tasks, follow this Chain of Thought reasoning format:
+
+ Let me think through this step by step:
+ 1. First, I need to understand what is being asked...
+ 2. Then, I should consider what information or tools I need.... Use available tools in the format: tool_name(param1: "value1", param2: "value2")
+ 3. Next, I'll analyze the available options...
+ 4. Finally, I'll provide a well-reasoned answer...
+
+ Available tools:
+ {tools_desc}
+
+ Remember to:
+ 1. Break down complex problems into smaller steps
+ 2. Think through each step logically
+ 3. Use tools when needed to gather information
+ 4. Provide clear reasoning for your conclusions
+ 5. End with a final, well-justified answer"""
+
+ return cot_prompt
diff --git a/flo_ai/flo_ai/models/agent_error.py b/flo_ai/flo_ai/models/agent_error.py
new file mode 100644
index 00000000..6be3f31d
--- /dev/null
+++ b/flo_ai/flo_ai/models/agent_error.py
@@ -0,0 +1,9 @@
+from typing import Optional
+
+
+class AgentError(Exception):
+ """Base exception for agent errors"""
+
+ def __init__(self, message: str, original_error: Optional[Exception] = None):
+ super().__init__(message)
+ self.original_error = original_error
diff --git a/flo_ai/flo_ai/models/base_agent.py b/flo_ai/flo_ai/models/base_agent.py
new file mode 100644
index 00000000..106c2788
--- /dev/null
+++ b/flo_ai/flo_ai/models/base_agent.py
@@ -0,0 +1,75 @@
+from typing import Dict, Any, List, Tuple
+from abc import ABC, abstractmethod
+from enum import Enum
+from flo_ai.llm.base_llm import BaseLLM
+
+
+class AgentType(Enum):
+ CONVERSATIONAL = 'conversational'
+ TOOL_USING = 'tool_using'
+
+
+class ReasoningPattern(Enum):
+ DIRECT = 'direct' # Direct response without explicit reasoning
+ REACT = 'react' # Thought-Action-Observation cycle
+ COT = 'cot' # Chain of Thought reasoning
+
+
+class BaseAgent(ABC):
+ def __init__(
+ self,
+ name: str,
+ system_prompt: str,
+ agent_type: AgentType,
+ llm: BaseLLM,
+ max_retries: int = 3,
+ ):
+ self.name = name
+ self.system_prompt = system_prompt
+ self.agent_type = agent_type
+ self.llm = llm
+ self.max_retries = max_retries
+ self.conversation_history: List[Dict[str, str]] = []
+
+ @abstractmethod
+ async def run(self, input_text: str) -> str:
+ """Execute the agent's main functionality"""
+ pass
+
+ async def handle_error(
+ self, error: Exception, context: Dict[str, Any]
+ ) -> Tuple[bool, str]:
+ error_prompt = (
+ f'An error occurred while processing the request: {str(error)}\n'
+ f'Context: {context}\n'
+ 'Please analyze the error and suggest a correction. '
+ 'If the error is not recoverable, explain why.'
+ )
+
+ try:
+ messages = [
+ {
+ 'role': 'system',
+ 'content': 'You are an AI error analysis assistant. '
+ 'Analyze errors and suggest corrections when possible.',
+ },
+ {'role': 'user', 'content': error_prompt},
+ ]
+
+ response = await self.llm.generate(messages)
+ analysis = self.llm.get_message_content(response)
+ should_retry = 'not recoverable' not in analysis.lower()
+ return should_retry, analysis
+
+ except Exception as e:
+ return False, f'Error during error handling: {str(e)}'
+
+ def add_to_history(self, role: str, content: str, **kwargs):
+ """Add a message to conversation history"""
+ message = {'role': role, 'content': content}
+ message.update(kwargs) # Add any additional fields like name
+ self.conversation_history.append(message)
+
+ def clear_history(self):
+ """Clear conversation history"""
+ self.conversation_history = []
diff --git a/flo_ai/flo_ai/models/delegate.py b/flo_ai/flo_ai/models/delegate.py
deleted file mode 100644
index 62494b9b..00000000
--- a/flo_ai/flo_ai/models/delegate.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from dataclasses import dataclass
-
-
-@dataclass
-class Delegate:
- to: list[str]
- retry: int = 1
diff --git a/flo_ai/flo_ai/models/exception.py b/flo_ai/flo_ai/models/exception.py
deleted file mode 100644
index 9477254a..00000000
--- a/flo_ai/flo_ai/models/exception.py
+++ /dev/null
@@ -1,8 +0,0 @@
-class FloValidationException(Exception):
- def __init__(self, *args: object) -> None:
- super().__init__(*args)
-
-
-class FloIllegalStateException(Exception):
- def __init__(self, *args: object) -> None:
- super().__init__(*args)
diff --git a/flo_ai/flo_ai/models/flo_agent.py b/flo_ai/flo_ai/models/flo_agent.py
deleted file mode 100644
index 5daebe2d..00000000
--- a/flo_ai/flo_ai/models/flo_agent.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from langchain_core.tools import BaseTool
-from langchain.agents import AgentExecutor
-from langchain.agents import create_tool_calling_agent
-from langchain_core.runnables import Runnable
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.state.flo_session import FloSession
-from typing import Union, Optional, Callable
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.parsers.flo_parser import FloParser
-from flo_ai.models.flo_base_agent import FloBaseAgent
-
-
-class FloAgent(FloBaseAgent):
- def __init__(
- self,
- name: str,
- agent: Runnable,
- executor: AgentExecutor,
- model_name: str,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- super().__init__(
- name,
- executor,
- ExecutableType.agentic,
- model_name,
- data_collector=data_collector,
- )
- self.agent: Runnable = agent
- self.executor: AgentExecutor = executor
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- job: str,
- tools: list[BaseTool],
- role: Optional[str] = None,
- on_error: Union[str, Callable] = True,
- llm: Union[BaseLanguageModel, None] = None,
- parser: Optional[FloParser] = None,
- data_collector: Optional[FloOutputCollector] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloAgent.Builder(
- session=session,
- name=name,
- job=job,
- tools=tools,
- role=role,
- on_error=on_error,
- llm=llm,
- model_name=model_name,
- parser=parser,
- data_collector=data_collector,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- job: str,
- tools: list[BaseTool],
- role: Optional[str] = None,
- verbose: bool = False,
- llm: Union[BaseLanguageModel, None] = None,
- on_error: Union[str, Callable] = True,
- model_name: Union[str, None] = 'default',
- parser: Optional[FloParser] = None,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- prompt: Union[ChatPromptTemplate, str] = job
- self.name: str = name
- self.model_name = model_name
- self.llm = llm if llm is not None else session.llm
- system_prompts = (
- [('system', 'You are a {}, {}'.format(role, prompt))]
- if role is not None
- else [('system', prompt)]
- )
- if parser is not None:
- system_prompts.append('\n{format_instructions}')
- system_prompts.append(MessagesPlaceholder(variable_name='messages'))
- system_prompts.append(MessagesPlaceholder(variable_name='agent_scratchpad'))
- self.prompt: ChatPromptTemplate = (
- ChatPromptTemplate.from_messages(system_prompts)
- if isinstance(prompt, str)
- else prompt
- )
- if parser is not None:
- self.prompt = self.prompt.partial(
- format_instructions=parser.get_format_instructions()
- )
- self.tools: list[BaseTool] = tools
- self.verbose = verbose
- self.on_error = on_error
- self.data_collector = data_collector
-
- def build(self) -> AgentExecutor:
- agent = create_tool_calling_agent(self.llm, self.tools, self.prompt)
- executor = AgentExecutor(
- agent=agent,
- tools=self.tools,
- verbose=self.verbose,
- return_intermediate_steps=True,
- handle_parsing_errors=self.on_error,
- )
- return FloAgent(
- self.name,
- agent,
- executor,
- model_name=self.model_name,
- data_collector=self.data_collector,
- )
diff --git a/flo_ai/flo_ai/models/flo_base_agent.py b/flo_ai/flo_ai/models/flo_base_agent.py
deleted file mode 100644
index 2b4b064c..00000000
--- a/flo_ai/flo_ai/models/flo_base_agent.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from typing import Optional
-from langchain_core.runnables import Runnable
-from flo_ai.models.flo_executable import ExecutableFlo, ExecutableType
-from flo_ai.state.flo_output_collector import FloOutputCollector
-
-
-class FloBaseAgent(ExecutableFlo):
- """Base class for all Flo agents containing common properties and initialization."""
-
- def __init__(
- self,
- name: str,
- executor: Runnable,
- executable_type: ExecutableType,
- model_name: str,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- """Initialize the base agent with common properties.
-
- Args:
- name: Name of the agent
- executor: The runnable executor for the agent
- executable_type: Type of the executable
- model_name: Name of the model being used
- data_collector: Optional collector for output data
- """
- super().__init__(name, executor, executable_type)
- self.executor: Runnable = executor
- self.model_name: str = model_name
- self.members = []
- self.data_collector: Optional[FloOutputCollector] = data_collector
diff --git a/flo_ai/flo_ai/models/flo_delegation_agent.py b/flo_ai/flo_ai/models/flo_delegation_agent.py
deleted file mode 100644
index 9ee5466b..00000000
--- a/flo_ai/flo_ai/models/flo_delegation_agent.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from typing import Optional
-from langchain_core.runnables import Runnable
-from flo_ai.state.flo_session import FloSession
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.models.delegate import Delegate
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from langchain_core.language_models import BaseLanguageModel
-from pydantic import BaseModel, Field
-from langchain_core.output_parsers import JsonOutputParser
-from flo_ai.models.flo_base_agent import FloBaseAgent
-
-
-# TODO probably use messages to relay information
-class NextAgent(BaseModel):
- next: str = Field(description='Name of the next member to be called')
- message: str = Field(description='Input to the next agent')
-
-
-class FloDelegatorAgent(FloBaseAgent):
- def __init__(
- self,
- session: FloSession,
- executor: Runnable,
- delegate: Delegate,
- name: str,
- model_name: str,
- ) -> None:
- super().__init__(name, executor, ExecutableType.delegator, model_name)
- self.session = session
- self.delegate = delegate
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- job: str,
- to: Delegate,
- llm: Optional[BaseLanguageModel] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloDelegatorAgent.Builder(
- session=session,
- name=name,
- job=job,
- delegate=to,
- llm=llm,
- model_name=model_name,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- job: str,
- delegate: Delegate,
- llm: Optional[BaseLanguageModel] = None,
- model_name: str = None,
- ) -> None:
- self.session = session
- self.name = name
- self.to = delegate
- delegator_base_system_message = (
- 'You are a delegator tasked with routing a conversation between the'
- ' following {member_type}: {members}. Given the following rules,'
- ' respond with the worker to act next. The output should be in strict JSON format. No non-JSON character should be in the output '
- )
- self.model_name = model_name
- self.llm = session.llm if llm is None else llm
- self.options = delegate.to
- self.parser = JsonOutputParser(pydantic_object=NextAgent)
- self.llm_router_prompt = ChatPromptTemplate.from_messages(
- [
- (
- 'system',
- delegator_base_system_message
- + '\n'
- + 'Rules: {delegator_rules}'
- + '\n'
- + 'Given the conversation above, who should act next?'
- + 'Select one of: {options} \n {format_instructions}',
- ),
- MessagesPlaceholder(variable_name='messages'),
- ]
- ).partial(
- options=str(self.options),
- members=', '.join(self.options),
- member_type='agents',
- delegator_rules=job,
- format_instructions=self.parser.get_format_instructions(),
- )
-
- def build(self):
- chain = self.llm_router_prompt | self.llm | self.parser
-
- return FloDelegatorAgent(
- session=self.session,
- name=self.name,
- delegate=self.to,
- executor=chain,
- model_name=self.model_name,
- )
diff --git a/flo_ai/flo_ai/models/flo_executable.py b/flo_ai/flo_ai/models/flo_executable.py
deleted file mode 100644
index 07e143c0..00000000
--- a/flo_ai/flo_ai/models/flo_executable.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from enum import Enum
-from flo_ai.models.flo_member import FloMember
-from langchain_core.runnables import Runnable
-from langchain_core.messages import HumanMessage
-from flo_ai.state.flo_state import STATE_NAME_MESSAGES
-
-
-class ExecutableType(Enum):
- agentic = 'agentic'
- llm = 'llm'
- tool = 'tool'
- reflection = 'reflection'
- delegator = 'delegator'
- team = 'team'
- router = 'router'
-
- @staticmethod
- def isAgent(type: 'ExecutableType'):
- return type in [ExecutableType.agentic, ExecutableType.llm, ExecutableType.tool]
-
-
-class ExecutableFlo(FloMember):
- def __init__(
- self, name: str, runnable: Runnable, type: str = ExecutableType.team
- ) -> None:
- super().__init__(name, type)
- self.runnable = runnable
-
- def stream(self, work, config=None):
- return self.runnable.stream(
- {STATE_NAME_MESSAGES: [HumanMessage(content=work)]}, config
- )
-
- def astream(self, work, config=None):
- return self.runnable.astream(
- {STATE_NAME_MESSAGES: [HumanMessage(content=work)]}, config
- )
-
- def invoke(self, work, config=None):
- return self.runnable.invoke(
- {
- STATE_NAME_MESSAGES: ([HumanMessage(content=work)]),
- },
- config,
- )
-
- def ainvoke(self, work, config=None):
- return self.runnable.ainvoke(
- {
- STATE_NAME_MESSAGES: [HumanMessage(content=work)],
- },
- config,
- )
-
- def draw(self, xray=True):
- return self.runnable.get_graph().draw_mermaid_png()
diff --git a/flo_ai/flo_ai/models/flo_llm_agent.py b/flo_ai/flo_ai/models/flo_llm_agent.py
deleted file mode 100644
index da8d4d6f..00000000
--- a/flo_ai/flo_ai/models/flo_llm_agent.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from langchain_core.runnables import Runnable
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from flo_ai.state.flo_session import FloSession
-from typing import Union, Optional
-from langchain_core.output_parsers import StrOutputParser
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.parsers.flo_parser import FloParser
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.models.flo_base_agent import FloBaseAgent
-
-
-class FloLLMAgent(FloBaseAgent):
- def __init__(
- self,
- name: str,
- executor: Runnable,
- model_name: str,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- super().__init__(
- name,
- executor,
- ExecutableType.llm,
- model_name,
- data_collector=data_collector,
- )
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- job: str,
- role: Optional[str] = None,
- llm: Union[BaseLanguageModel, None] = None,
- parser: Optional[FloParser] = None,
- data_collector: Optional[FloOutputCollector] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloLLMAgent.Builder(
- session=session,
- name=name,
- job=job,
- role=role,
- llm=llm,
- model_name=model_name,
- parser=parser,
- data_collector=data_collector,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- job: str,
- role: Optional[str] = None,
- llm: Union[BaseLanguageModel, None] = None,
- model_name: str = None,
- parser: Optional[FloParser] = None,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- self.model_name = model_name
- prompt: Union[ChatPromptTemplate, str] = job
-
- self.name: str = name
- self.llm = llm if llm is not None else session.llm
- system_prompts = (
- [('system', 'You are a {}, {}'.format(role, prompt))]
- if role is not None
- else [('system', prompt)]
- )
- if parser is not None:
- system_prompts.append('\n{format_instructions}')
- system_prompts.append(MessagesPlaceholder(variable_name='messages'))
- self.prompt: ChatPromptTemplate = (
- ChatPromptTemplate.from_messages(system_prompts)
- if isinstance(prompt, str)
- else prompt
- )
- if parser is not None:
- self.prompt = self.prompt.partial(
- format_instructions=parser.get_format_instructions()
- )
- self.data_collector = data_collector
-
- def build(self) -> Runnable:
- executor = self.prompt | self.llm | StrOutputParser()
- return FloLLMAgent(
- self.name, executor, self.model_name, data_collector=self.data_collector
- )
diff --git a/flo_ai/flo_ai/models/flo_member.py b/flo_ai/flo_ai/models/flo_member.py
deleted file mode 100644
index cfd39ad4..00000000
--- a/flo_ai/flo_ai/models/flo_member.py
+++ /dev/null
@@ -1,4 +0,0 @@
-class FloMember:
- def __init__(self, name: str, type: str) -> None:
- self.name = name
- self.type = type
diff --git a/flo_ai/flo_ai/models/flo_node.py b/flo_ai/flo_ai/models/flo_node.py
deleted file mode 100644
index a36e6d60..00000000
--- a/flo_ai/flo_ai/models/flo_node.py
+++ /dev/null
@@ -1,330 +0,0 @@
-import functools
-from flo_ai.models.flo_agent import FloAgent
-from flo_ai.models.flo_reflection_agent import FloReflectionAgent
-from flo_ai.models.flo_routed_team import FloRoutedTeam
-from flo_ai.models.delegate import Delegate
-from langchain.agents import AgentExecutor
-from flo_ai.state.flo_state import TeamFloAgentState, STATE_NAME_MESSAGES
-from langchain_core.messages import AIMessage, HumanMessage
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.state.flo_session import FloSession
-from typing import Optional, Type, List
-from flo_ai.callbacks.flo_callbacks import (
- FloAgentCallback,
- FloRouterCallback,
- FloCallback,
-)
-from flo_ai.common.flo_logger import get_logger
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.helpers.utils import rotate_array
-
-
-class FloNode:
- def __init__(
- self,
- func: functools.partial,
- name: str,
- kind: ExecutableType,
- delegate: Optional[Delegate] = None,
- async_func: functools.partial = None,
- agent_executable=None,
- ) -> None:
- self.name = name
- self.func = func
- self.kind: ExecutableType = kind
- self.delegate = delegate
- self.async_func = async_func
- self.agent_executable = agent_executable
-
- def invoke(self, query, config):
- return self.func(
- {STATE_NAME_MESSAGES: [HumanMessage(content=query)]}, config=config
- )
-
- async def ainvoke(self, query, config):
- return await self.async_func(
- {STATE_NAME_MESSAGES: [HumanMessage(content=query)]}, config=config
- )
-
- def draw(
- self,
- xray=True,
- ):
- return (
- self.agent_executable.get_graph().draw_mermaid_png()
- if self.agent_executable is not None
- else None
- )
-
- class Builder:
- def __init__(self, session: FloSession) -> None:
- self.session = session
-
- def build_from_agent(self, flo_agent: FloAgent) -> 'FloNode':
- agent_func = functools.partial(
- FloNode.Builder.__teamflo_agent_node,
- agent=flo_agent.runnable,
- name=flo_agent.name,
- session=self.session,
- model_name=flo_agent.model_name,
- data_collector=flo_agent.data_collector,
- )
- agent_func_async = functools.partial(
- FloNode.Builder.__async_teamflo_agent_node,
- agent=flo_agent.runnable,
- name=flo_agent.name,
- session=self.session,
- model_name=flo_agent.model_name,
- data_collector=flo_agent.data_collector,
- )
- return FloNode(
- agent_func,
- flo_agent.name,
- flo_agent.type,
- async_func=agent_func_async,
- agent_executable=flo_agent.runnable,
- )
-
- def build_from_reflection(self, flo_agent: FloReflectionAgent) -> 'FloNode':
- agent_func = functools.partial(
- FloNode.Builder.__teamflo_agent_node,
- agent=flo_agent.runnable,
- name=flo_agent.name,
- session=self.session,
- model_name=flo_agent.model_name,
- )
- return FloNode(
- agent_func,
- flo_agent.name,
- flo_agent.type,
- delegate=flo_agent.delegate,
- agent_executable=flo_agent.runnable,
- )
-
- def build_from_team(self, flo_team: FloRoutedTeam) -> 'FloNode':
- team_chain = (
- functools.partial(
- FloNode.Builder.__teamflo_team_node, members=flo_team.runnable.nodes
- )
- | flo_team.runnable
- )
- return FloNode(
- (
- FloNode.Builder.__get_last_message
- | team_chain
- | FloNode.Builder.__join_graph
- ),
- flo_team.name,
- flo_team.type,
- agent_executable=flo_team.runnable,
- )
-
- def build_from_router(self, flo_router) -> 'FloNode':
- router_func = functools.partial(
- FloNode.Builder.__teamflo_router_node,
- agent=flo_router.executor,
- name=flo_router.name,
- session=self.session,
- model_name=flo_router.model_name,
- )
- return FloNode(
- router_func,
- flo_router.name,
- flo_router.type,
- agent_executable=flo_router.executor,
- )
-
- def build_from_delegator(self, flo_router) -> 'FloNode':
- router_func = functools.partial(
- FloNode.Builder.__teamflo_router_node,
- agent=flo_router.executor,
- name=flo_router.name,
- session=self.session,
- model_name=flo_router.model_name,
- )
- return FloNode(
- router_func,
- flo_router.name,
- flo_router.type,
- delegate=flo_router.delegate,
- agent_executable=flo_router.executor,
- )
-
- @staticmethod
- def __teamflo_agent_node(
- state: TeamFloAgentState,
- agent: AgentExecutor,
- name: str,
- session: FloSession,
- model_name: str,
- config=None,
- data_collector: Optional[FloOutputCollector] = None,
- ):
- agent_cbs: List[FloAgentCallback] = FloNode.Builder.__filter_callbacks(
- session, FloAgentCallback
- )
- flo_cbs: List[FloCallback] = FloNode.Builder.__filter_callbacks(
- session, FloCallback
- )
- [
- callback.on_agent_start(name, model_name, state['messages'], **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_start(name, model_name, state['messages'], **{})
- for callback in flo_cbs
- ]
- try:
- if isinstance(state['messages'][-1], AIMessage):
- # This was done as part of a fix for using llama 3.1 8b
- # When the last message was from AI, it was forgetting the actual task if was meant to do
- state['messages'] = rotate_array(state['messages'])
-
- result = agent.invoke(state, config=config)
- output = result if isinstance(result, str) else result['output']
- if data_collector is not None:
- get_logger().info(
- 'appending output to data collector', session=session
- )
- data_collector.append(output)
- except Exception as e:
- [
- callback.on_agent_error(name, model_name, e, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_error(name, model_name, e, **{})
- for callback in flo_cbs
- ]
- raise e
- [
- callback.on_agent_end(name, model_name, output, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_start(name, model_name, output, **{})
- for callback in flo_cbs
- ]
- # Only human message working for Cloude models
- # TODO maybe handle Cloude seperately
- return {STATE_NAME_MESSAGES: [HumanMessage(content=output, name=name)]}
-
- @staticmethod
- async def __async_teamflo_agent_node(
- state: TeamFloAgentState,
- agent: AgentExecutor,
- name: str,
- session: FloSession,
- model_name: str,
- config: dict = None,
- data_collector: Optional[FloOutputCollector] = None,
- ):
- agent_cbs: List[FloAgentCallback] = FloNode.Builder.__filter_callbacks(
- session, FloAgentCallback
- )
- flo_cbs: List[FloCallback] = FloNode.Builder.__filter_callbacks(
- session, FloCallback
- )
- [
- callback.on_agent_start(name, model_name, state['messages'], **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_start(name, model_name, state['messages'], **{})
- for callback in flo_cbs
- ]
- try:
- result = await agent.ainvoke(state, config=config)
- output = result if isinstance(result, str) else result['output']
- if data_collector is not None:
- get_logger().info(
- 'appending output to data collector', session=session
- )
- data_collector.append(output)
- except Exception as e:
- [
- callback.on_agent_error(name, model_name, e, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_error(name, model_name, e, **{})
- for callback in flo_cbs
- ]
- raise e
- [
- callback.on_agent_end(name, model_name, output, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_agent_start(name, model_name, output, **{})
- for callback in flo_cbs
- ]
- return {STATE_NAME_MESSAGES: [AIMessage(content=output, name=name)]}
-
- @staticmethod
- def __filter_callbacks(session: FloSession, type: Type):
- cbs = session.callbacks
- return list(filter(lambda callback: isinstance(callback, type), cbs))
-
- @staticmethod
- def __teamflo_router_node(
- state: TeamFloAgentState,
- agent: AgentExecutor,
- name: str,
- session: FloSession,
- model_name: str,
- config: dict = None,
- ):
- agent_cbs: List[FloRouterCallback] = FloNode.Builder.__filter_callbacks(
- session, FloRouterCallback
- )
- flo_cbs: List[FloCallback] = FloNode.Builder.__filter_callbacks(
- session, FloCallback
- )
- [
- callback.on_router_start(name, model_name, state['messages'], **{})
- for callback in agent_cbs
- ]
- [
- callback.on_router_start(name, model_name, state['messages'], **{})
- for callback in flo_cbs
- ]
- try:
- result = agent.invoke(state, config=config)
- nextNode = result if isinstance(result, str) else result['next']
- messages = [] if isinstance(result, str) else [result['message']]
- except Exception as e:
- [
- callback.on_router_error(name, model_name, e, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_router_error(name, model_name, e, **{})
- for callback in flo_cbs
- ]
- raise e
- [
- callback.on_router_end(name, model_name, nextNode, **{})
- for callback in agent_cbs
- ]
- [
- callback.on_router_start(name, model_name, nextNode, **{})
- for callback in flo_cbs
- ]
- return {'next': nextNode, STATE_NAME_MESSAGES: messages}
-
- @staticmethod
- def __get_last_message(state: TeamFloAgentState) -> str:
- return state[STATE_NAME_MESSAGES][-1].content
-
- @staticmethod
- def __join_graph(response: dict):
- return {STATE_NAME_MESSAGES: [response[STATE_NAME_MESSAGES][-1]]}
-
- @staticmethod
- def __teamflo_team_node(message: str, members: list[str]):
- results = {
- STATE_NAME_MESSAGES: [HumanMessage(content=message)],
- 'team_members': ', '.join(members),
- }
- return results
diff --git a/flo_ai/flo_ai/models/flo_reflection_agent.py b/flo_ai/flo_ai/models/flo_reflection_agent.py
deleted file mode 100644
index 23edc6be..00000000
--- a/flo_ai/flo_ai/models/flo_reflection_agent.py
+++ /dev/null
@@ -1,75 +0,0 @@
-from typing import Union, Optional
-from langchain_core.runnables import Runnable
-from flo_ai.state.flo_session import FloSession
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from flo_ai.models.flo_executable import ExecutableType
-from langchain_core.output_parsers import StrOutputParser
-from flo_ai.models.delegate import Delegate
-from flo_ai.models.flo_base_agent import FloBaseAgent
-
-
-class FloReflectionAgent(FloBaseAgent):
- def __init__(
- self, name: str, executor: Runnable, model_name: str, delegate: Delegate
- ) -> None:
- super().__init__(name, executor, ExecutableType.reflection, model_name)
- self.delegate = delegate
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- job: str,
- to: Delegate,
- role: Optional[str] = None,
- llm: Optional[BaseLanguageModel] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloReflectionAgent.Builder(
- session=session,
- name=name,
- job=job,
- to=to,
- role=role,
- llm=llm,
- model_name=model_name,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- job: str,
- to: Delegate,
- role: Optional[str] = None,
- llm: Union[BaseLanguageModel, None] = None,
- model_name: str = None,
- ) -> None:
- prompt_message: Union[ChatPromptTemplate, str] = job
- self.name: str = name
- self.llm = llm if llm is not None else session.llm
- self.model_name = model_name
- self.delegate = to
-
- system_prompts = (
- [
- ('system', 'You are a {}'.format(role)),
- ('system', prompt_message),
- ]
- if role is not None
- else [('system', prompt_message)]
- )
- system_prompts.append(MessagesPlaceholder(variable_name='messages'))
- self.prompt: ChatPromptTemplate = (
- ChatPromptTemplate.from_messages(system_prompts)
- if isinstance(prompt_message, str)
- else prompt_message
- )
-
- def build(self):
- executor = self.prompt | self.llm | StrOutputParser()
- return FloReflectionAgent(
- self.name, executor, self.model_name, delegate=self.delegate
- )
diff --git a/flo_ai/flo_ai/models/flo_routed_team.py b/flo_ai/flo_ai/models/flo_routed_team.py
deleted file mode 100644
index cc118af3..00000000
--- a/flo_ai/flo_ai/models/flo_routed_team.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from flo_ai.models.flo_executable import ExecutableFlo
-from langgraph.graph.graph import CompiledGraph
-
-
-class FloRoutedTeam(ExecutableFlo):
- def __init__(self, name: str, graph: CompiledGraph) -> None:
- super().__init__(name, graph)
-
- # Overridden for xray use, doesnt work in base class
- def draw(self, xray=True):
- return self.runnable.get_graph(xray=xray).draw_mermaid_png()
diff --git a/flo_ai/flo_ai/models/flo_team.py b/flo_ai/flo_ai/models/flo_team.py
deleted file mode 100644
index 66fd926b..00000000
--- a/flo_ai/flo_ai/models/flo_team.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from flo_ai.models.flo_member import FloMember
-from flo_ai.state.flo_session import FloSession
-
-
-class FloTeam:
- def __init__(
- self, session: FloSession, name: str, members: list[FloMember]
- ) -> None:
- self.name = name
- self.members = members
- self.session = session
-
- @staticmethod
- def create(session: FloSession, name: str, members: list[FloMember]):
- return FloTeam.Builder(session=session, name=name, members=members).build()
-
- class Builder:
- def __init__(
- self, session: FloSession, name: str, members: list[FloMember]
- ) -> None:
- from flo_ai import Flo
-
- self.name = name
- self.session = session
- self.members = list(
- map(lambda x: x.runnable if isinstance(x, Flo) else x, members)
- )
- self.member_names = list(map(lambda x: x.name, self.members))
-
- def build(self):
- return FloTeam(name=self.name, session=self.session, members=self.members)
diff --git a/flo_ai/flo_ai/models/flo_tool_agent.py b/flo_ai/flo_ai/models/flo_tool_agent.py
deleted file mode 100644
index 432540e6..00000000
--- a/flo_ai/flo_ai/models/flo_tool_agent.py
+++ /dev/null
@@ -1,59 +0,0 @@
-from typing import Optional
-from langchain_core.runnables import Runnable
-from flo_ai.state.flo_session import FloSession
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.models.flo_base_agent import FloBaseAgent
-
-
-class FloToolAgent(FloBaseAgent):
- def __init__(
- self,
- name: str,
- executor: Runnable,
- model_name: str,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- super().__init__(
- name,
- executor,
- ExecutableType.tool,
- model_name,
- data_collector=data_collector,
- )
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- tool: Runnable,
- ):
- model_name = 'default'
- return FloToolAgent.Builder(
- session=session,
- name=name,
- tool_runnable=tool,
- model_name=model_name,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- tool_runnable: Runnable,
- model_name: str,
- data_collector: Optional[FloOutputCollector] = None,
- ) -> None:
- self.name: str = name
- self.runnable = tool_runnable
- self.model_name = model_name
- self.data_collector = data_collector
-
- def build(self) -> Runnable:
- return FloToolAgent(
- self.name,
- self.runnable,
- self.model_name,
- data_collector=self.data_collector,
- )
diff --git a/flo_ai/flo_ai/parsers/__init__.py b/flo_ai/flo_ai/parsers/__init__.py
deleted file mode 100644
index 969dca8b..00000000
--- a/flo_ai/flo_ai/parsers/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from flo_ai.parsers.flo_parser import FloParser
-from flo_ai.parsers.flo_json_parser import FloJsonParser
-from flo_ai.parsers.flo_pydantic_parser import FloPydanticParser
-
-__all__ = ['FloParser', 'FloJsonParser', 'FloPydanticParser']
diff --git a/flo_ai/flo_ai/parsers/flo_parser.py b/flo_ai/flo_ai/parsers/flo_parser.py
deleted file mode 100644
index ad2b32e3..00000000
--- a/flo_ai/flo_ai/parsers/flo_parser.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from abc import ABC, abstractmethod
-from pydantic import BaseModel
-
-
-class FloParser(ABC):
- @abstractmethod
- def get_format_instructions(self):
- pass
-
- @abstractmethod
- def get_format(self) -> BaseModel:
- pass
diff --git a/flo_ai/flo_ai/parsers/flo_pydantic_parser.py b/flo_ai/flo_ai/parsers/flo_pydantic_parser.py
deleted file mode 100644
index b7bca7b9..00000000
--- a/flo_ai/flo_ai/parsers/flo_pydantic_parser.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from flo_ai.parsers.flo_parser import FloParser
-from pydantic import BaseModel
-from langchain_core.output_parsers import PydanticOutputParser
-
-
-class FloPydanticParser(FloParser):
- def __init__(self, output_model: BaseModel):
- self.model = output_model
- super().__init__()
-
- def get_format_instructions(self):
- return PydanticOutputParser(
- pydantic_object=self.model
- ).get_format_instructions()
-
- def get_format(self):
- return self.model
-
- def create(output_model: BaseModel):
- return FloPydanticParser.Builder(output_model).build()
-
- class Builder:
- def __init__(self, output_model: BaseModel):
- self.model = output_model
-
- def build(self):
- return FloPydanticParser(self.model)
diff --git a/flo_ai/flo_ai/retrievers/__init__.py b/flo_ai/flo_ai/retrievers/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flo_ai/flo_ai/retrievers/flo_compression_pipeline.py b/flo_ai/flo_ai/retrievers/flo_compression_pipeline.py
deleted file mode 100644
index 81338d44..00000000
--- a/flo_ai/flo_ai/retrievers/flo_compression_pipeline.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from langchain_core.embeddings import Embeddings
-from langchain_text_splitters import CharacterTextSplitter
-from langchain_community.document_transformers import EmbeddingsRedundantFilter
-from langchain.retrievers.document_compressors import EmbeddingsFilter
-
-
-class FloCompressionPipeline:
- def __init__(self, embeddings: Embeddings) -> None:
- self.__embeddings = embeddings
- self.__pipeline = []
-
- def add_chuncking(self, chunk_size=300, chunk_overlap=0):
- splitter = CharacterTextSplitter(
- chunk_size=chunk_size, chunk_overlap=chunk_overlap, separator='. '
- )
- self.__pipeline.append(splitter)
-
- def add_embedding_reduntant_filter(self):
- redundant_filter = EmbeddingsRedundantFilter(embeddings=self.__embeddings)
- self.__pipeline.append(redundant_filter)
-
- def add_embedding_relevant_filter(self, threshold: float = 0.50):
- relevant_filter = EmbeddingsFilter(
- embeddings=self.__embeddings, similarity_threshold=threshold
- )
- self.__pipeline.append(relevant_filter)
-
- def add_flashrank_reranking(self, model_name='ms-marco-MultiBERT-L-12'):
- from langchain.retrievers.document_compressors.flashrank_rerank import (
- FlashrankRerank,
- )
-
- compressor = FlashrankRerank(model=model_name)
- self.__pipeline.append(compressor)
-
- def add_cohere_reranking(self, model_name='rerank-english-v3.0'):
- from langchain.retrievers.document_compressors.cohere_rerank import CohereRerank
-
- compressor = CohereRerank(model=model_name)
- self.__pipeline.append(compressor)
-
- def get(self):
- return self.__pipeline
diff --git a/flo_ai/flo_ai/retrievers/flo_multi_query.py b/flo_ai/flo_ai/retrievers/flo_multi_query.py
deleted file mode 100644
index ac56dd70..00000000
--- a/flo_ai/flo_ai/retrievers/flo_multi_query.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from typing import List, Union
-from langchain_core.vectorstores import VectorStoreRetriever
-from langchain.output_parsers import PydanticOutputParser
-from langchain_core.prompts import PromptTemplate
-from pydantic import BaseModel, Field
-from flo_ai.state.flo_session import FloSession
-from langchain.retrievers.multi_query import MultiQueryRetriever
-
-
-class LineList(BaseModel):
- lines: List[str] = Field(description='Lines of text')
-
-
-class LineListOutputParser(PydanticOutputParser):
- def __init__(self) -> None:
- super().__init__(pydantic_object=LineList)
-
- def parse(self, text: str) -> LineList:
- lines = text.strip().split('\n')
- return LineList(lines=lines)
-
-
-class FloMultiQueryRetriever:
- def __init__(self, retriever) -> None:
- self.retriever = retriever
-
-
-class FloMultiQueryRetriverBuilder:
- def __init__(
- self,
- session: FloSession,
- retriver: VectorStoreRetriever,
- query_prompt: Union[str, None] = None,
- ) -> None:
- self.session = session
- self.retriver = retriver
- self.output_parser = LineListOutputParser()
-
- self.prompt = PromptTemplate(
- input_variables=['question'],
- template="""You are an AI language model assistant. Your task is to generate three
- different versions of the given user question to retrieve relevant documents from a vector
- database. By generating multiple perspectives on the user question, your goal is to help
- the user overcome some of the limitations of the distance-based similarity search.
- Provide these alternative questions separated by newlines.
- Original question: {question}"""
- if query_prompt is None
- else query_prompt,
- )
-
- def build(self):
- multi_query_retriever = MultiQueryRetriever.from_llm(
- retriever=self.retriver, llm=self.session.llm, prompt=self.prompt
- )
- return FloMultiQueryRetriever(multi_query_retriever)
diff --git a/flo_ai/flo_ai/retrievers/flo_retriever.py b/flo_ai/flo_ai/retrievers/flo_retriever.py
deleted file mode 100644
index b095a354..00000000
--- a/flo_ai/flo_ai/retrievers/flo_retriever.py
+++ /dev/null
@@ -1,229 +0,0 @@
-from langchain_core.vectorstores import VectorStoreRetriever
-from langchain_core.runnables import RunnableParallel, Runnable
-from flo_ai.state.flo_session import FloSession
-from langchain.schema.output_parser import StrOutputParser
-from langchain.schema.runnable import RunnablePassthrough
-from flo_ai.retrievers.flo_multi_query import FloMultiQueryRetriverBuilder
-from langchain.retrievers import ContextualCompressionRetriever
-from langchain.retrievers.document_compressors import DocumentCompressorPipeline
-from flo_ai.retrievers.flo_compression_pipeline import FloCompressionPipeline
-from functools import partial
-from pydantic import BaseModel, Field
-from langchain_core.tools import Tool
-from typing import Optional
-from langchain_core.callbacks import Callbacks
-from langchain_core.prompts import (
- BasePromptTemplate,
- PromptTemplate,
- aformat_document,
- format_document,
- ChatPromptTemplate,
- MessagesPlaceholder,
-)
-from typing import List
-
-
-class FloRagBaseMessage(BaseModel):
- content: str
-
-
-class FloRagToolInput(BaseModel):
- messages: List[FloRagBaseMessage] = Field(
- description='query to look up in the vector store'
- )
-
-
-def _get_relevant_documents(
- messages: List[FloRagBaseMessage],
- retriever: VectorStoreRetriever,
- document_prompt: BasePromptTemplate,
- document_separator: str,
- callbacks: Callbacks = None,
-) -> str:
- docs = retriever.invoke(messages[-1].content, config={'callbacks': callbacks})
- return document_separator.join(
- format_document(doc, document_prompt) for doc in docs
- )
-
-
-async def _aget_relevant_documents(
- messages: List[str],
- retriever: VectorStoreRetriever,
- document_prompt: BasePromptTemplate,
- document_separator: str,
- callbacks: Callbacks = None,
-) -> str:
- docs = await retriever.ainvoke(
- messages[-1].content, config={'callbacks': callbacks}
- )
- return document_separator.join(
- [await aformat_document(doc, document_prompt) for doc in docs]
- )
-
-
-class FloRagBuilder:
- def __init__(self, session: FloSession, retriever: VectorStoreRetriever) -> None:
- self.session = session
- self.retriever = retriever
- self.default_prompt = ChatPromptTemplate.from_messages(
- [
- (
- 'system',
- """You are an assistant for question-answering tasks.
- Use the following pieces of retrieved context to answer the question.
- If you don't know the answer, just say that you don't know.
- Use three sentences maximum and keep the answer concise.
-
- Here is the context:
- {context}
-
- """,
- ),
- MessagesPlaceholder(variable_name='chat_history'),
- ('human', '{question}'),
- ]
- )
- self.history_aware_retriever = self.__create_history_aware_retriever()
-
- def with_prompt(self, prompt: ChatPromptTemplate):
- self.default_prompt = prompt
- return self
-
- def with_multi_query(self, prompt=None):
- builder = FloMultiQueryRetriverBuilder(
- session=self.session, retriver=self.retriever, query_prompt=prompt
- )
- multi_query_retriever = builder.build()
- self.retriever = multi_query_retriever.retriever
- return self
-
- def with_compression(self, pipeline: FloCompressionPipeline):
- pipeline_compressor = DocumentCompressorPipeline(transformers=pipeline.get())
- compression_retriever = ContextualCompressionRetriever(
- base_compressor=pipeline_compressor, base_retriever=self.retriever
- )
- self.retriever = compression_retriever
- return self
-
- def __create_history_aware_retriever(self):
- contextualize_q_system_prompt = """Given a chat history and the latest user question \
- which might reference context in the chat history, formulate a standalone question \
- which can be understood without the chat history. Do NOT answer the question, \
- just reformulate it if needed and otherwise return it as is."""
-
- contextualize_q_prompt = ChatPromptTemplate.from_messages(
- [
- ('system', contextualize_q_system_prompt),
- MessagesPlaceholder('chat_history'),
- ('human', '{question}'),
- ]
- )
- self.history_aware_retriever = (
- contextualize_q_prompt | self.session.llm | StrOutputParser()
- )
- return self.history_aware_retriever
-
- def __get_retriever(self):
- def __precontext_retriver(input_prompt: dict):
- if input_prompt.get('chat_history'):
- return self.history_aware_retriever
- else:
- return input_prompt['question']
-
- return __precontext_retriver | self.retriever
-
- def __format_docs(self, docs):
- return '\n\n'.join(doc.page_content for doc in docs)
-
- def __get_optional_chat_history(self, x):
- return x['chat_history'] if 'chat_history' in x else []
-
- def __build_history_aware_rag(self):
- rag_chain = (
- RunnablePassthrough.assign(
- context=(lambda x: x['context']),
- )
- | self.default_prompt
- | self.session.llm
- )
-
- rag_chain_with_source = RunnableParallel(
- {
- 'context': self.__get_retriever() | self.__format_docs,
- 'question': RunnablePassthrough(),
- 'chat_history': lambda x: self.__get_optional_chat_history(x),
- }
- ).assign(answer=rag_chain)
- return rag_chain_with_source
-
- def build_rag(self):
- return self.__build_history_aware_rag()
-
- def build_retriever_tool(self, name, description):
- return self.__create_retriever_tool(self.retriever, name, description)
-
- @staticmethod
- def __get_rag_answer(messages: List[FloRagBaseMessage], runnable: Runnable):
- question = messages[-1].content
- chat_history = messages[:-1]
- result = runnable.invoke({'question': question, 'chat_history': chat_history})
- return result['answer'].content
-
- @staticmethod
- async def __aget_rag_answer(messages: List[FloRagBaseMessage], runnable: Runnable):
- question = messages[-1].content
- chat_history = messages[:-1]
- result = await runnable.ainvoke(
- {'question': question, 'chat_history': chat_history}
- )
- return result['answer'].content
-
- def __create_retriever_tool(
- self,
- retriever: VectorStoreRetriever,
- name: str,
- description: str,
- *,
- document_prompt: Optional[BasePromptTemplate] = None,
- document_separator: str = '\n',
- ) -> Tool:
- document_prompt = document_prompt or PromptTemplate.from_template(
- '{page_content}'
- )
- func = partial(
- _get_relevant_documents,
- retriever=retriever,
- document_prompt=document_prompt,
- document_separator=document_separator,
- )
- afunc = partial(
- _aget_relevant_documents,
- retriever=retriever,
- document_prompt=document_prompt,
- document_separator=document_separator,
- )
- return Tool(
- name=name,
- description=description,
- func=func,
- coroutine=afunc,
- args_schema=FloRagToolInput,
- )
-
- @staticmethod
- def __create_rag_tool(runnable_rag: Runnable, name: str, description: str) -> Tool:
- func = partial(FloRagBuilder.__get_rag_answer, runnable=runnable_rag)
-
- afunc = partial(FloRagBuilder.__aget_rag_answer, runnable=runnable_rag)
-
- return Tool(
- name=name,
- description=description,
- func=func,
- coroutine=afunc,
- args_schema=FloRagToolInput,
- )
-
- def build_rag_tool(self, name, description) -> Tool:
- rag = self.__build_history_aware_rag()
- return FloRagBuilder.__create_rag_tool(rag, name, description)
diff --git a/flo_ai/flo_ai/router/__init__.py b/flo_ai/flo_ai/router/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/flo_ai/flo_ai/router/flo_agent_router.py b/flo_ai/flo_ai/router/flo_agent_router.py
deleted file mode 100644
index f4098e69..00000000
--- a/flo_ai/flo_ai/router/flo_agent_router.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from flo_ai.router.flo_router import FloRouter
-from langgraph.graph import StateGraph, END, START
-from flo_ai.state.flo_state import TeamFloAgentState
-from flo_ai.models.flo_routed_team import FloRoutedTeam
-from flo_ai.models.flo_base_agent import FloBaseAgent
-from flo_ai.state.flo_session import FloSession
-
-
-class FloAgentRouter(FloRouter):
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_agent: FloBaseAgent,
- ):
- super().__init__(
- session=session,
- name=name,
- flo_team=flo_agent,
- executor=None,
- model_name=None,
- )
-
- def build_graph(self):
- flo_agent_node = self.build_node(self.flo_team)
- workflow = StateGraph(TeamFloAgentState)
- workflow.add_node(self.name, flo_agent_node.func)
- workflow.add_edge(START, self.name)
- workflow.add_edge(self.name, END)
-
- workflow_graph = workflow.compile()
- return FloRoutedTeam(self.flo_team.name, workflow_graph)
-
- @staticmethod
- def create(session: FloSession, name: str, agent: FloBaseAgent):
- return FloAgentRouter.Builder(
- session=session, name=name, flo_agent=agent
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_agent: FloBaseAgent,
- ) -> None:
- self.name = name
- self.session = session
- self.agent = flo_agent
-
- def build(self):
- return FloAgentRouter(self.session, self.name, self.agent)
diff --git a/flo_ai/flo_ai/router/flo_linear.py b/flo_ai/flo_ai/router/flo_linear.py
deleted file mode 100644
index 7bb050e6..00000000
--- a/flo_ai/flo_ai/router/flo_linear.py
+++ /dev/null
@@ -1,86 +0,0 @@
-from flo_ai.router.flo_router import FloRouter
-from langgraph.graph import StateGraph, END, START
-from flo_ai.state.flo_state import TeamFloAgentState
-from flo_ai.models.flo_routed_team import FloRoutedTeam
-from flo_ai.models.flo_team import FloTeam
-from flo_ai.state.flo_session import FloSession
-from flo_ai.models.flo_executable import ExecutableType
-
-
-class FloLinear(FloRouter):
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_team: FloTeam,
- ):
- super().__init__(
- session=session,
- name=name,
- flo_team=flo_team,
- executor=None,
- model_name=None,
- )
-
- def build_graph(self):
- flo_agent_nodes = [self.build_node(member) for member in self.members]
- workflow = StateGraph(TeamFloAgentState)
-
- for flo_node in flo_agent_nodes:
- agent_name = flo_node.name
- workflow.add_node(agent_name, flo_node.func)
-
- start_node = flo_agent_nodes[0]
- end_node = flo_agent_nodes[-1]
- if start_node.kind == ExecutableType.delegator:
- next_node = (
- flo_agent_nodes[0 + 2] if (0 + 2) < len(flo_agent_nodes) else END
- )
- self.add_delegation_edge(workflow, START, start_node, next_node)
- else:
- workflow.add_edge(START, start_node.name)
- for i in range(len(flo_agent_nodes) - 1):
- parent_node = flo_agent_nodes[i]
- child_node = flo_agent_nodes[i + 1]
- next_node = (
- flo_agent_nodes[i + 2] if (i + 2) < len(flo_agent_nodes) else END
- )
- if parent_node.kind == ExecutableType.reflection:
- self.add_reflection_edge(workflow, parent_node, child_node)
- continue
- if child_node.kind == ExecutableType.delegator:
- self.add_delegation_edge(workflow, parent_node, child_node, next_node)
- continue
-
- if (
- child_node.kind != ExecutableType.reflection
- and parent_node.kind != ExecutableType.delegator
- ):
- workflow.add_edge(parent_node.name, child_node.name)
-
- if end_node.kind == ExecutableType.reflection:
- self.add_reflection_edge(workflow, end_node, END)
- elif end_node.kind != ExecutableType.delegator:
- workflow.add_edge(end_node.name, END)
-
- workflow_graph = workflow.compile()
-
- return FloRoutedTeam(self.flo_team.name, workflow_graph)
-
- @staticmethod
- def create(session: FloSession, name: str, team: FloTeam):
- return FloLinear.Builder(session=session, name=name, flo_team=team).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_team: FloTeam,
- ) -> None:
- self.name = name
- self.session = session
- self.team = flo_team
-
- def build(self):
- return FloLinear(self.session, self.name, self.team)
diff --git a/flo_ai/flo_ai/router/flo_llm_router.py b/flo_ai/flo_ai/router/flo_llm_router.py
deleted file mode 100644
index 38b8a5b1..00000000
--- a/flo_ai/flo_ai/router/flo_llm_router.py
+++ /dev/null
@@ -1,126 +0,0 @@
-from typing import Union
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from langchain_core.runnables import Runnable
-from flo_ai.state.flo_session import FloSession
-from flo_ai.constants.prompt_constants import FLO_FINISH
-from flo_ai.router.flo_router import FloRouter
-from flo_ai.models.flo_team import FloTeam
-from flo_ai.models.flo_routed_team import FloRoutedTeam
-from langgraph.graph import StateGraph
-from flo_ai.state.flo_state import TeamFloAgentState
-from langchain_core.output_parsers import JsonOutputParser
-from pydantic import BaseModel, Field
-
-
-class NextAgent(BaseModel):
- next: str = Field(description='Name of the next member to be called')
-
-
-class FloLLMRouter(FloRouter):
- def __init__(
- self,
- session: FloSession,
- executor: Runnable,
- flo_team: FloTeam,
- name: str,
- model_name: str = 'default',
- ) -> None:
- super().__init__(
- session=session,
- name=name,
- flo_team=flo_team,
- executor=executor,
- model_name=model_name,
- )
-
- def build_graph(self):
- flo_agent_nodes = [self.build_node(flo_agent) for flo_agent in self.members]
- workflow = StateGraph(TeamFloAgentState)
- for flo_agent_node in flo_agent_nodes:
- workflow.add_node(flo_agent_node.name, flo_agent_node.func)
-
- workflow.add_node(self.name, self.build_node(self).func)
- for member in self.member_names:
- workflow.add_edge(member, self.name)
- workflow.add_conditional_edges(self.name, self.router_fn)
- workflow.set_entry_point(self.name)
- workflow_graph = workflow.compile()
- return FloRoutedTeam(self.flo_team.name, workflow_graph)
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- team: FloTeam,
- router_prompt: str = None,
- llm: Union[BaseLanguageModel, None] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloLLMRouter.Builder(
- session=session,
- name=name,
- flo_team=team,
- router_prompt=router_prompt,
- llm=llm,
- model_nick_name=model_name,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_team: FloTeam,
- router_prompt: str = None,
- llm: Union[BaseLanguageModel, None] = None,
- model_nick_name: str = 'default',
- ) -> None:
- self.name = name
- self.session = session
- self.llm = llm if llm is not None else session.llm
- self.flo_team = flo_team
- self.agents = flo_team.members
- self.members = [agent.name for agent in flo_team.members]
- self.model_name = model_nick_name
- self.options = self.members + [FLO_FINISH]
- member_type = (
- 'workers' if flo_team.members[0].type == 'agent' else 'team members'
- )
-
- router_base_system_message = (
- 'You are a supervisor tasked with managing a conversation between the'
- ' following {member_type}: {members}. Given the following rules,'
- ' respond with the worker to act next '
- )
-
- self.parser = JsonOutputParser(pydantic_object=NextAgent)
- self.llm_router_prompt = ChatPromptTemplate.from_messages(
- [
- ('system', router_base_system_message),
- MessagesPlaceholder(variable_name='messages'),
- ('system', 'Rules: {router_prompt}'),
- (
- 'system',
- 'Given the conversation above, who should act next?'
- ' Or should we FINISH if the task is already answered ? Select one of: {options} \n {format_instructions}',
- ),
- ]
- ).partial(
- options=str(self.options),
- members=', '.join(self.members),
- member_type=member_type,
- router_prompt=router_prompt,
- format_instructions=self.parser.get_format_instructions(),
- )
-
- def build(self):
- chain = self.llm_router_prompt | self.llm | self.parser
-
- return FloLLMRouter(
- executor=chain,
- flo_team=self.flo_team,
- name=self.name,
- session=self.session,
- model_name=self.model_name,
- )
diff --git a/flo_ai/flo_ai/router/flo_router.py b/flo_ai/flo_ai/router/flo_router.py
deleted file mode 100644
index 2df2b035..00000000
--- a/flo_ai/flo_ai/router/flo_router.py
+++ /dev/null
@@ -1,174 +0,0 @@
-import functools
-from typing import Union
-from abc import ABC, abstractmethod
-from langgraph.graph import END, StateGraph
-from flo_ai.models.flo_node import FloNode
-from flo_ai.state.flo_session import FloSession
-from flo_ai.models.flo_team import FloTeam
-from flo_ai.models.flo_member import FloMember
-from flo_ai.models.flo_routed_team import FloRoutedTeam
-from flo_ai.constants.prompt_constants import FLO_FINISH
-from flo_ai.models.flo_executable import ExecutableType
-from flo_ai.state.flo_state import (
- TeamFloAgentState,
- STATE_NAME_LOOP_CONTROLLER,
- STATE_NAME_NEXT,
-)
-from flo_ai.constants.flo_node_contants import (
- INTERNAL_NODE_REFLECTION_MANAGER,
- INTERNAL_NODE_DELEGATION_MANAGER,
-)
-
-
-class FloRouter(ABC):
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_team: FloTeam,
- executor,
- model_name: Union[str, None] = 'default',
- ):
- self.name = name
- self.session: FloSession = session
- self.flo_team: FloTeam = flo_team
- self.members = flo_team.members
- self.member_names = [x.name for x in flo_team.members]
- self.type: ExecutableType = ExecutableType.router
- self.executor = executor
- self.model_name = model_name
-
- def build_routed_team(self) -> FloRoutedTeam:
- return self.build_graph()
-
- @abstractmethod
- def build_graph():
- pass
-
- def build_node(self, flo_member: FloMember) -> FloNode:
- node_builder = FloNode.Builder(self.session)
- if flo_member.type == ExecutableType.router:
- return node_builder.build_from_router(flo_member)
- if flo_member.type == ExecutableType.team:
- return node_builder.build_from_team(flo_member)
- if flo_member.type == ExecutableType.delegator:
- return node_builder.build_from_delegator(flo_member)
- if flo_member.type == ExecutableType.reflection:
- return node_builder.build_from_reflection(flo_member)
- return node_builder.build_from_agent(flo_member)
-
- def router_fn(self, state: TeamFloAgentState):
- next = state['next']
- conditional_map = {k: k for k in self.member_names}
- conditional_map[FLO_FINISH] = END
- self.session.append(node=next)
- if self.session.is_looping(node=next):
- return conditional_map[FLO_FINISH]
- return conditional_map[next]
-
- def update_reflection_state(
- self, state: TeamFloAgentState, reflection_agent_name: str
- ):
- tracker = state.get(STATE_NAME_LOOP_CONTROLLER) or {}
- tracker[reflection_agent_name] = tracker.get(reflection_agent_name, 0) + 1
- return {STATE_NAME_LOOP_CONTROLLER: tracker}
-
- def add_delegation_edge(
- self,
- workflow: StateGraph,
- parent: FloNode,
- delegation_node: FloNode,
- nextNode: Union[FloNode, str],
- ):
- to_agent_names = delegation_node.delegate.to
- delegation_node_name = delegation_node.name
- next_node_name = nextNode if isinstance(nextNode, str) else nextNode.name
-
- retry = delegation_node.delegate.retry or 0
-
- conditional_map = {}
- for agent_name in to_agent_names:
- conditional_map[agent_name] = agent_name
- conditional_map[next_node_name] = next_node_name
-
- parent_name = parent if isinstance(parent, str) else parent.name
- if retry == 0:
- # no need to track loops when the retry is zero
- workflow.add_node(
- INTERNAL_NODE_DELEGATION_MANAGER,
- functools.partial(
- self.update_reflection_state,
- reflection_agent_name=delegation_node_name,
- ),
- )
- workflow.add_edge(parent_name, INTERNAL_NODE_DELEGATION_MANAGER)
- workflow.add_conditional_edges(
- INTERNAL_NODE_DELEGATION_MANAGER,
- self.__get_refelection_routing_fn(
- retry, delegation_node_name, next_node_name
- ),
- {
- delegation_node_name: delegation_node_name,
- next_node_name: next_node_name,
- },
- )
- else:
- workflow.add_edge(parent_name, delegation_node_name)
-
- workflow.add_conditional_edges(
- delegation_node_name,
- FloRouter.__get_delegation_router_fn(next_node_name),
- conditional_map,
- )
-
- @staticmethod
- def __get_delegation_router_fn(nextNode: str):
- def delegation_router(state: TeamFloAgentState):
- if STATE_NAME_NEXT not in state:
- return nextNode
- return state[STATE_NAME_NEXT]
-
- return delegation_router
-
- def add_reflection_edge(
- self,
- workflow: StateGraph,
- reflection_node: FloNode,
- nextNode: Union[FloNode, str],
- ):
- to_agent_name = reflection_node.delegate.to[0]
- retry = reflection_node.delegate.retry or 1
- reflection_agent_name = reflection_node.name
- next = nextNode if isinstance(nextNode, str) else nextNode.name
-
- workflow.add_node(
- INTERNAL_NODE_REFLECTION_MANAGER,
- functools.partial(
- self.update_reflection_state,
- reflection_agent_name=reflection_agent_name,
- ),
- )
-
- workflow.add_edge(to_agent_name, INTERNAL_NODE_REFLECTION_MANAGER)
- workflow.add_conditional_edges(
- INTERNAL_NODE_REFLECTION_MANAGER,
- self.__get_refelection_routing_fn(retry, reflection_agent_name, next),
- {reflection_agent_name: reflection_agent_name, next: next},
- )
- workflow.add_edge(reflection_agent_name, to_agent_name)
-
- @staticmethod
- def __get_refelection_routing_fn(
- retries: int, reflection_agent_name, next_node_name
- ):
- def reflection_routing_fn(state: TeamFloAgentState):
- tracker = state[STATE_NAME_LOOP_CONTROLLER]
- if (
- tracker is not None
- and reflection_agent_name in tracker
- and tracker[reflection_agent_name] > retries
- ):
- return next_node_name
- return reflection_agent_name
-
- return reflection_routing_fn
diff --git a/flo_ai/flo_ai/router/flo_router_factory.py b/flo_ai/flo_ai/router/flo_router_factory.py
deleted file mode 100644
index 38ad7cf4..00000000
--- a/flo_ai/flo_ai/router/flo_router_factory.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from typing import Optional
-from flo_ai.state.flo_session import FloSession
-from flo_ai.router.flo_supervisor import FloSupervisor
-from flo_ai.router.flo_llm_router import FloLLMRouter
-from flo_ai.router.flo_linear import FloLinear
-from flo_ai.yaml.config import TeamConfig
-from flo_ai.models.flo_team import FloTeam
-from flo_ai.router.flo_router import FloRouter
-from flo_ai.router.flo_agent_router import FloAgentRouter
-from flo_ai.error.flo_exception import FloException
-from flo_ai.constants.common_constants import DOCUMENTATION_ROUTER_ANCHOR
-
-
-class FloRouterFactory:
- @staticmethod
- def create(
- session: FloSession,
- router_kind: str,
- team_config: TeamConfig,
- flo_team: FloTeam,
- ) -> FloRouter:
- if router_kind == 'supervisor':
- router_model = FloRouterFactory.__resolve_model(
- session, team_config.router.model
- )
- return FloSupervisor.Builder(
- session,
- team_config.name,
- flo_team,
- llm=router_model,
- model_nick_name=team_config.router.model,
- ).build()
- elif router_kind == 'linear':
- return FloLinear.Builder(session, team_config.name, flo_team).build()
- elif router_kind == 'llm':
- router_model = FloRouterFactory.__resolve_model(
- session, team_config.router.model
- )
- return FloLLMRouter.Builder(
- session,
- team_config.router.name,
- flo_team,
- llm=router_model,
- model_nick_name=team_config.router.model,
- ).build()
- elif router_kind == 'agent':
- return FloAgentRouter.Builder(
- session,
- team_config.name,
- flo_agent=flo_team,
- ).build()
- else:
- raise Exception(f"""Unknown router type: {router_kind}.
- The supported types are supervisor, linear and llm.
- Check the documentation @ {DOCUMENTATION_ROUTER_ANCHOR}""")
-
- @staticmethod
- def __resolve_model(session: FloSession, model_name: Optional[str] = None):
- if model_name is None:
- return session.llm
- if model_name not in session.models:
- raise FloException(
- f"""Model not found: {model_name}.
- The model you would like to use should be registered to the session using session.register_model api,
- and the same model name should be used here instead of `{model_name}`"""
- )
- return session.models[model_name]
diff --git a/flo_ai/flo_ai/router/flo_supervisor.py b/flo_ai/flo_ai/router/flo_supervisor.py
deleted file mode 100644
index 96a5628d..00000000
--- a/flo_ai/flo_ai/router/flo_supervisor.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
-from typing import Union
-from langchain_core.runnables import Runnable
-from flo_ai.state.flo_session import FloSession
-from flo_ai.constants.prompt_constants import FLO_FINISH
-from flo_ai.router.flo_llm_router import FloLLMRouter
-from flo_ai.models.flo_team import FloTeam
-from langchain_core.output_parsers import JsonOutputParser
-from flo_ai.router.flo_llm_router import NextAgent
-
-supervisor_system_message = (
- 'You are a supervisor tasked with managing a conversation between the'
- ' following {member_type}: {members}. Given the following user request,'
- ' respond with the worker to act next. Each worker will perform a'
- ' task and respond with their results and status. When the users question is answered or the assigned task is finished,'
- ' respond with FINISH. '
-)
-
-
-class FloSupervisor(FloLLMRouter):
- def __init__(
- self,
- session: FloSession,
- executor: Runnable,
- flo_team: FloTeam,
- name: str,
- model_name: str = 'default',
- ) -> None:
- super().__init__(
- session=session,
- name=name,
- flo_team=flo_team,
- executor=executor,
- model_name=model_name,
- )
-
- @staticmethod
- def create(
- session: FloSession,
- name: str,
- team: FloTeam,
- llm: Union[BaseLanguageModel, None] = None,
- ):
- model_name = 'default' if llm is None else llm.name
- return FloSupervisor.Builder(
- session=session,
- name=name,
- flo_team=team,
- llm=llm,
- model_nick_name=model_name,
- ).build()
-
- class Builder:
- def __init__(
- self,
- session: FloSession,
- name: str,
- flo_team: FloTeam,
- llm: Union[BaseLanguageModel, None] = None,
- model_nick_name: str = 'default',
- ) -> None:
- self.name = name
- self.session = session
- self.llm = llm if llm is not None else session.llm
- self.model_name = model_nick_name
- self.flo_team = flo_team
- self.agents = flo_team.members
- self.members = [agent.name for agent in flo_team.members]
- self.options = self.members + [FLO_FINISH]
- member_type = (
- 'workers' if flo_team.members[0].type == 'agent' else 'team members'
- )
- self.parser = JsonOutputParser(pydantic_object=NextAgent)
- self.supervisor_prompt = ChatPromptTemplate.from_messages(
- [
- ('system', supervisor_system_message),
- MessagesPlaceholder(variable_name='messages'),
- (
- 'system',
- 'Given the conversation above, who should act next?'
- ' Or should we FINISH if the task is already answered, Select one of: {options} \n {format_instructions}',
- ),
- ]
- ).partial(
- options=str(self.options),
- members=', '.join(self.members),
- member_type=member_type,
- format_instructions=self.parser.get_format_instructions(),
- )
-
- def build(self):
- chain = self.supervisor_prompt | self.llm | self.parser
-
- return FloSupervisor(
- executor=chain,
- flo_team=self.flo_team,
- name=self.name,
- session=self.session,
- model_name=self.model_name,
- )
diff --git a/flo_ai/flo_ai/state/__init__.py b/flo_ai/flo_ai/state/__init__.py
deleted file mode 100644
index 9830a9d0..00000000
--- a/flo_ai/flo_ai/state/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from flo_ai.state.flo_json_output_collector import FloJsonOutputCollector
-from flo_ai.state.flo_output_collector import FloOutputCollector, CollectionStatus
-
-__all__ = ['FloJsonOutputCollector', 'FloOutputCollector', 'CollectionStatus']
diff --git a/flo_ai/flo_ai/state/flo_json_output_collector.py b/flo_ai/flo_ai/state/flo_json_output_collector.py
deleted file mode 100644
index 9df81bf7..00000000
--- a/flo_ai/flo_ai/state/flo_json_output_collector.py
+++ /dev/null
@@ -1,198 +0,0 @@
-import json
-import re
-from typing import Dict, List, Any, Callable, Optional
-
-from flo_ai.error.flo_exception import FloException
-from flo_ai.common.flo_logger import get_logger
-from flo_ai.state.flo_output_collector import FloOutputCollector, CollectionStatus
-
-
-class FloJsonOutputCollector(FloOutputCollector):
- """
- FloJsonOutputCollector โ collects JSON payloads from LLM/agent outputs,
- gracefully handles comments, and offers โFloโ Q-promise looping.
- Key Features:
- - Strips out // and /*โฆ*/ comments before parsing
- - Uses recursive regex to find balanced { โฆ } blocks
- - Strict mode: raises exception if no JSON found
- - peek, pop, fetch to manage collected data
- - rewind(): recursive promise-then replay, newest-first
- - iter_q(): whileโfor hybrid iterator over memory steps
- """
-
- def __init__(self, strict: bool = False):
- super().__init__()
- self.strict = strict # Enforce JSON presence?
- self.status = CollectionStatus.success # success, partial, or error
- self.data: List[Dict[str, Any]] = [] # Stored JSON dictionaries
-
- def append(self, agent_output: str) -> None:
- """Extracts JSON from `agent_output` and appends the resulting dict."""
- self.data.append(self.__extract_jsons(agent_output))
-
- def __strip_comments(self, json_str: str) -> str:
- """Remove JS-style comments (// and /*โฆ*/) so json.loads() will succeed."""
- cleaned = []
- i = 0
- length = len(json_str)
-
- while i < length:
- char = json_str[i]
-
- if char not in '"/*':
- cleaned.append(char)
- i += 1
- continue
-
- if char == '"':
- cleaned.append(char)
- i += 1
- while i < length:
- char = json_str[i]
- cleaned.append(char)
- i += 1
- if char == '"' and (i < 2 or json_str[i - 2] != '\\'):
- break
- continue
-
- if char == '/' and i + 1 < length:
- next_char = json_str[i + 1]
- if next_char == '/':
- i += 2
- while i < length and json_str[i] != '\n':
- i += 1
- continue
- elif next_char == '*':
- i += 2
- while i + 1 < length:
- if json_str[i] == '*' and json_str[i + 1] == '/':
- i += 2
- break
- i += 1
- continue
-
- cleaned.append(char)
- i += 1
-
- return ''.join(cleaned)
-
- def __extract_jsons(self, llm_response: str) -> Dict[str, Any]:
- """
- 1) Find all balanced `{ โฆ }` blocks via recursive regex
- 2) Strip comments and json.loads() each
- 3) Merge into one dict (later keys override earlier)
- 4) On strict mode, raise FloException if no JSON found
- """
- pattern = r'\{(?:[^{}]|(?R))*\}'
- matches = re.findall(pattern, llm_response)
- merged: Dict[str, Any] = {}
-
- for json_str in matches:
- try:
- cleaned = self.__strip_comments(json_str)
- obj = json.loads(cleaned)
- merged.update(obj)
- except json.JSONDecodeError as e:
- self.status = CollectionStatus.partial
- get_logger().error(f'Invalid JSON in response: {json_str}, {e}')
-
- if self.strict and not matches:
- self.status = CollectionStatus.error
- get_logger().error(f'No JSON found in strict mode: {llm_response}')
- raise FloException(
- 'JSON response expected in collector model: strict', error_code=1099
- )
-
- return merged
-
- # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- # Standard Data Management
- # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- def pop(self) -> Dict[str, Any]:
- """Remove and return the last collected JSON dict."""
- return self.data.pop()
-
- def peek(self) -> Optional[Dict[str, Any]]:
- """View the last collected JSON dict without removing it."""
- return self.data[-1] if self.data else None
-
- def fetch(self) -> Dict[str, Any]:
- """Merge all collected dicts into one and return it."""
- return self.__merge_data()
-
- def __merge_data(self) -> Dict[str, Any]:
- """Helper method to merge all collected dicts."""
- merged = {}
- for d in self.data:
- merged.update(d)
- return merged
-
- # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- # Flo Q-Promise Looping Methods
- # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-
- def rewind(
- self,
- then_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
- depth: Optional[int] = None
- ) -> None:
- """
- Recursively replay memory entries newestโoldest, invoking `then_callback` per step.
- Mirrors JS Promise.then chaining in reverse order.
- :param then_callback: function to handle each entry
- :param depth: max number of entries to process
- """
- if not self.data:
- get_logger().warn("No memory to rewind.")
- return
-
- entries = self.data[::-1] # Reverse: newest first
- if depth:
- entries = entries[:depth]
-
- def _recursive(idx: int) -> None:
- if idx >= len(entries):
- return
- entry = entries[idx]
- if then_callback:
- then_callback(entry)
- _recursive(idx + 1)
-
- _recursive(0)
-
- def iter_q(self, depth: Optional[int] = None) -> "FloIterator":
- """
- Return a FloIterator that yields one-item lists of entries,
- enabling a whileโfor hybrid loop over memory steps.
- """
- return FloIterator(self, depth)
-
-
-class FloIterator:
- """
- Hybrid whileโfor iterator over FloJsonOutputCollector data.
- Newest entries first, depth-limited.
- """
-
- def __init__(self, collector: FloJsonOutputCollector, depth: Optional[int] = None):
- self.entries = collector.data[::-1]
- self.limit = min(depth, len(self.entries)) if depth is not None else len(self.entries)
- self.index = 0
-
- def has_next(self) -> bool:
- """True if more entries remain."""
- return self.index < self.limit
-
- def next(self) -> List[Dict[str, Any]]:
- """
- Return the next โbatchโ of entries (here, a single-item list).
- Returns [] when exhausted.
- """
- if not self.has_next():
- return []
- entry = self.entries[self.index]
- self.index += 1
- return [entry]
-
-
diff --git a/flo_ai/flo_ai/state/flo_output_collector.py b/flo_ai/flo_ai/state/flo_output_collector.py
deleted file mode 100644
index 5416acf9..00000000
--- a/flo_ai/flo_ai/state/flo_output_collector.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from enum import Enum
-from abc import ABC, abstractmethod
-
-
-class CollectionStatus(Enum):
- success = 'success'
- partial = 'partial'
- error = 'error'
-
-
-class FloOutputCollector(ABC):
- @abstractmethod
- def append():
- pass
-
- @abstractmethod
- def fetch():
- pass
-
- @abstractmethod
- def peek():
- pass
-
- @abstractmethod
- def pop():
- pass
diff --git a/flo_ai/flo_ai/state/flo_session.py b/flo_ai/flo_ai/state/flo_session.py
deleted file mode 100644
index 72d40971..00000000
--- a/flo_ai/flo_ai/state/flo_session.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import warnings
-from typing import Union, Dict
-from langchain_core.language_models import BaseLanguageModel
-from langchain_core.tools import BaseTool
-from flo_ai.common.flo_logger import get_logger
-from flo_ai.common.flo_langchain_logger import FloLangchainLogger
-from flo_ai.helpers.utils import random_str
-from flo_ai.callbacks.flo_callbacks import (
- FloToolCallback,
- FloAgentCallback,
- FloRouterCallback,
-)
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.state.flo_json_output_collector import FloJsonOutputCollector
-from flo_ai.parsers.flo_parser import FloParser
-from typing import Optional
-
-
-def _handle_agent_error(error) -> str:
- error_message = str(error)[:50]
- return f"""
- Following error happened while aogent execution, please retry with the fix for the same:
- {error_message}
- """
-
-
-class FloSession:
- def __init__(
- self,
- default_llm: BaseLanguageModel = None,
- loop_size: int = 2,
- max_loop: int = 3,
- llm: BaseLanguageModel = None,
- log_level: Optional[str] = None,
- on_agent_error=_handle_agent_error,
- ) -> None:
- if log_level:
- warnings.warn(
- '`log_level` is deprecated and will be removed in a future version. '
- 'Please use `Flo.set_log_level()` instead.',
- DeprecationWarning,
- stacklevel=2,
- )
-
- self.session_id = str(random_str(16))
- self.llm = self.resolve_llm(default_llm, llm)
- self.tools = dict()
- self.models: Dict[str, BaseLanguageModel] = dict()
- self.tools: Dict[str, BaseTool] = dict()
- # TODO maybe create a default if not provided
- self.data_collectors: Dict[str, FloOutputCollector] = dict()
- self.parsers: Dict[str, FloParser] = dict()
- self.counter = dict()
- self.navigation: list[str] = list()
- self.pattern_series = dict()
- self.loop_size: int = loop_size
- self.max_loop: int = max_loop
- self.on_agent_error = on_agent_error
- self.langchain_logger = FloLangchainLogger(self.session_id)
- self.callbacks: list = []
- get_logger().info('New session created ...', self)
-
- def resolve_llm(
- self, default_llm: BaseLanguageModel = None, llm: BaseLanguageModel = None
- ):
- if default_llm is not None:
- return default_llm
- if llm:
- warnings.warn(
- '`llm` is deprecated and will be removed in a future version. '
- 'Please use `default_llm` instead.',
- DeprecationWarning,
- stacklevel=2,
- )
- return llm
-
- def register_tool(self, name: str, tool: BaseTool):
- self.tools[name] = tool
- get_logger().info(f"Tool '{name}' registered for session {self.session_id}")
- return self
-
- def register_model(self, name: str, model: BaseLanguageModel):
- self.models[name] = model
- get_logger().info(f"Model '{name}' registered for session {self.session_id}")
- return self
-
- def register_parser(self, name: str, parser: FloParser):
- self.parsers[name] = parser
- get_logger().info(f"Parser '{name}' registered for session {self.session_id}")
- return self
-
- def register_output_collector(
- self,
- name: str = '__default',
- collector: FloOutputCollector = FloJsonOutputCollector(),
- ):
- self.data_collectors[name] = collector
- get_logger().info(
- f"Data Collection '{name}' registered for session {self.session_id}"
- )
- return self
-
- def register_callback(
- self, callback: Union[FloRouterCallback, FloAgentCallback, FloToolCallback]
- ):
- self.callbacks.append(callback)
- tool_callbacks = list(
- filter(lambda x: isinstance(x, FloToolCallback), self.callbacks)
- )
- self.langchain_logger = FloLangchainLogger(self.session_id, tool_callbacks)
- self.callbacks.append(self.langchain_logger)
- return self
-
- def prepare_config(self, config=None):
- get_logger().info(f'Binding all callbacks ... {len(self.callbacks)}')
- config = {} if config is None else config
- existing_cbs = config['callbacks'] if 'callbacks' in config else []
- config['callbacks'] = self.callbacks + existing_cbs
- return config
-
- def append(self, node: str) -> int:
- get_logger().debug(f'Appending node: {node}')
- self.counter[node] = self.counter.get(node, 0) + 1
- if node in self.navigation:
- last_known_index = (
- len(self.navigation) - 1 - self.navigation[::-1].index(node)
- )
- pattern_array = self.navigation[last_known_index : len(self.navigation)]
- if len(pattern_array) + 1 >= self.loop_size:
- pattern = '|'.join(pattern_array) + '|' + node
- if node in self.pattern_series:
- self.pattern_series[node].append(pattern)
- else:
- self.pattern_series[node] = [pattern]
- self.navigation.append(node)
-
- def is_looping(self, node) -> bool:
- get_logger().debug(f'Checking if node {node} is looping')
- patterns = self.pattern_series[node] if node in self.pattern_series else []
- if len(patterns) < self.max_loop:
- return False
- return patterns[-(self.max_loop) :] == [patterns[-1]] * self.max_loop
-
- def stringify(self):
- return str(self.counter)
diff --git a/flo_ai/flo_ai/state/flo_state.py b/flo_ai/flo_ai/state/flo_state.py
deleted file mode 100644
index 485e1aee..00000000
--- a/flo_ai/flo_ai/state/flo_state.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import Annotated, List, Sequence, TypedDict, Tuple
-from langchain_core.messages import BaseMessage
-
-
-import operator
-
-STATE_NAME_LOOP_CONTROLLER = 'loop_tracker'
-STATE_NAME_NEXT = 'next'
-STATE_NAME_MESSAGES = 'messages'
-
-
-# The agent state is the input to each node in the graph
-class TeamFloAgentState(TypedDict):
- # The annotation tells the graph that new messages will always
- # be added to the current states
- messages: Annotated[Sequence[BaseMessage], operator.add]
- # The 'next' field indicates where to route to next
- next: str
- # used for reflection agents
- loop_tracker: dict
-
-
-class TeamFloAgentStateWithPlan(TypedDict):
- input: str
- plan: List[str]
- past_steps: Annotated[List[Tuple], operator.add]
- response: str
diff --git a/flo_ai/flo_ai/storage/data_collector.py b/flo_ai/flo_ai/storage/data_collector.py
deleted file mode 100644
index 14d72163..00000000
--- a/flo_ai/flo_ai/storage/data_collector.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from abc import ABC, abstractmethod
-from typing import Any, Dict
-from pathlib import Path
-import json
-
-
-class DataCollector(ABC):
- @abstractmethod
- def store_log(self, entry: Dict[str, Any]) -> None:
- pass
-
- @abstractmethod
- def store_tool_log(self, entry: Dict[str, Any]) -> None:
- pass
-
- @abstractmethod
- def close(self) -> None:
- pass
-
-
-class JSONLFileCollector(DataCollector):
- def __init__(self, folder_path: str):
- self.log_file_path = Path(f'{folder_path}/logs/logs.jsonl')
- self.tool_file_path = Path(f'{folder_path}/tools/tools.jsonl')
-
- self.log_file_path.parent.mkdir(parents=True, exist_ok=True)
- self.tool_file_path.parent.mkdir(parents=True, exist_ok=True)
-
- def store_log(self, entry: Dict[str, Any]) -> None:
- with open(self.log_file_path, 'a') as f:
- json.dump(entry, f)
- f.write('\n')
-
- def store_tool_log(self, entry: Dict[str, Any]) -> None:
- with open(self.tool_file_path, 'a') as f:
- json.dump(entry, f)
- f.write('\n')
-
- def close(self) -> None:
- pass
diff --git a/flo_ai/flo_ai/tool/__init__.py b/flo_ai/flo_ai/tool/__init__.py
new file mode 100644
index 00000000..9d985ea5
--- /dev/null
+++ b/flo_ai/flo_ai/tool/__init__.py
@@ -0,0 +1,4 @@
+from .base_tool import Tool, ToolExecutionError
+from .flo_tool import flo_tool, create_tool_from_function
+
+__all__ = ['Tool', 'ToolExecutionError', 'flo_tool', 'create_tool_from_function']
diff --git a/flo_ai/flo_ai/tool/base_tool.py b/flo_ai/flo_ai/tool/base_tool.py
new file mode 100644
index 00000000..a2b338d7
--- /dev/null
+++ b/flo_ai/flo_ai/tool/base_tool.py
@@ -0,0 +1,42 @@
+from typing import Dict, Any, Callable
+from flo_ai.models.agent_error import AgentError
+from flo_ai.utils.logger import logger
+
+
+class ToolExecutionError(AgentError):
+ """Error during tool execution"""
+
+ pass
+
+
+class Tool:
+ def __init__(
+ self,
+ name: str,
+ description: str,
+ function: Callable,
+ parameters: Dict[str, Dict[str, Any]],
+ ):
+ self.name = name
+ self.description = description
+ self.function = function
+
+ # Ensure parameters have required field
+ self.parameters = {}
+ for param_name, param_info in parameters.items():
+ self.parameters[param_name] = {
+ **param_info,
+ 'required': param_info.get('required', True),
+ }
+
+ async def execute(self, **kwargs) -> Any:
+ """Execute the tool with error handling"""
+ try:
+ logger.info(f'Executing tool {self.name} with kwargs: {kwargs}')
+ tool_result = await self.function(**kwargs)
+ logger.info(f'Tool {self.name} returned: {tool_result}')
+ return tool_result
+ except Exception as e:
+ raise ToolExecutionError(
+ f'Error executing tool {self.name}: {str(e)}', original_error=e
+ )
diff --git a/flo_ai/flo_ai/tool/flo_tool.py b/flo_ai/flo_ai/tool/flo_tool.py
new file mode 100644
index 00000000..accd9aa7
--- /dev/null
+++ b/flo_ai/flo_ai/tool/flo_tool.py
@@ -0,0 +1,213 @@
+import inspect
+import asyncio
+from typing import Dict, Any, Callable, Optional, Union
+from functools import wraps
+from .base_tool import Tool
+
+
+def flo_tool(
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ parameter_descriptions: Optional[Dict[str, str]] = None,
+):
+ """
+ Decorator to automatically convert a function into a Tool object.
+
+ Args:
+ name: Optional custom name for the tool. If not provided, uses function name.
+ description: Optional description for the tool. If not provided, uses function docstring.
+ parameter_descriptions: Optional dict mapping parameter names to their descriptions.
+ If not provided, will try to extract from docstring or use defaults.
+
+ Example:
+ @flo_tool(
+ description="Calculate mathematical operations",
+ parameter_descriptions={
+ "operation": "The operation to perform (add, subtract, multiply, divide)",
+ "x": "First number",
+ "y": "Second number"
+ }
+ )
+ async def calculate(operation: str, x: float, y: float) -> float:
+ # function implementation
+ pass
+
+ # The function can be used normally
+ result = await calculate("add", 5, 3)
+
+ # And you can get the Tool object
+ tool = calculate.tool
+ """
+
+ def decorator(func: Callable) -> Callable:
+ # Create the Tool object
+ tool = _create_tool_from_function(
+ func, name, description, parameter_descriptions
+ )
+
+ # Attach the tool to the function
+ func.tool = tool
+
+ # Return the original function (wrapped to preserve async behavior)
+ @wraps(func)
+ async def async_wrapper(*args, **kwargs):
+ return await func(*args, **kwargs)
+
+ @wraps(func)
+ def sync_wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+
+ # Return appropriate wrapper based on whether function is async
+ if asyncio.iscoroutinefunction(func):
+ async_wrapper.tool = tool
+ return async_wrapper
+ else:
+ sync_wrapper.tool = tool
+ return sync_wrapper
+
+ return decorator
+
+
+def _create_tool_from_function(
+ func: Callable,
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ parameter_descriptions: Optional[Dict[str, str]] = None,
+) -> Tool:
+ """Create a Tool object from a function."""
+ # Get function signature
+ sig = inspect.signature(func)
+
+ # Determine tool name
+ tool_name = name or func.__name__
+
+ # Determine tool description
+ tool_description = description or func.__doc__ or f'Tool for {func.__name__}'
+
+ # Extract parameters
+ parameters = {}
+ for param_name, param in sig.parameters.items():
+ # Skip self parameter for methods
+ if param_name == 'self':
+ continue
+
+ param_type = param.annotation
+ param_default = param.default
+
+ # Determine if parameter is required
+ is_required = param.default == inspect.Parameter.empty
+
+ # Get parameter description
+ param_desc = None
+ if parameter_descriptions and param_name in parameter_descriptions:
+ param_desc = parameter_descriptions[param_name]
+ else:
+ # Try to extract from docstring
+ param_desc = _extract_param_description_from_docstring(func, param_name)
+
+ # Default description if none found
+ if not param_desc:
+ param_desc = f'Parameter {param_name}'
+
+ # Determine JSON schema type
+ json_type = _get_json_type(param_type)
+
+ parameters[param_name] = {
+ 'type': json_type,
+ 'description': param_desc,
+ 'required': is_required,
+ }
+
+ # Add default value if present
+ if not is_required:
+ parameters[param_name]['default'] = param_default
+
+ # Create the tool
+ return Tool(
+ name=tool_name,
+ description=tool_description,
+ function=func,
+ parameters=parameters,
+ )
+
+
+def _extract_param_description_from_docstring(
+ func: Callable, param_name: str
+) -> Optional[str]:
+ """Extract parameter description from function docstring."""
+ if not func.__doc__:
+ return None
+
+ doc_lines = func.__doc__.split('\n')
+ for line in doc_lines:
+ line = line.strip()
+ if line.startswith(f':param {param_name}:'):
+ return line.split(':', 2)[2].strip()
+ elif line.startswith('Args:') and f'{param_name}:' in line:
+ # Handle Google-style docstrings
+ parts = line.split(f'{param_name}:', 1)
+ if len(parts) > 1:
+ return parts[1].strip()
+
+ return None
+
+
+def _get_json_type(python_type: Any) -> str:
+ """Convert Python type to JSON schema type."""
+ if python_type == inspect.Parameter.empty:
+ return 'string' # Default to string if no type annotation
+
+ # Handle Union types (e.g., Optional[str] -> str)
+ if hasattr(python_type, '__origin__') and python_type.__origin__ is Union:
+ # For Optional types, get the first non-None type
+ args = python_type.__args__
+ non_none_types = [arg for arg in args if arg is not type(None)]
+ if non_none_types:
+ python_type = non_none_types[0]
+
+ # Handle basic types
+ type_mapping = {
+ str: 'string',
+ int: 'integer',
+ float: 'number',
+ bool: 'boolean',
+ list: 'array',
+ dict: 'object',
+ }
+
+ # Check for exact type matches
+ if python_type in type_mapping:
+ return type_mapping[python_type]
+
+ # Check for isinstance relationships
+ for py_type, json_type in type_mapping.items():
+ try:
+ if issubclass(python_type, py_type):
+ return json_type
+ except TypeError:
+ continue
+
+ # Default to string for unknown types
+ return 'string'
+
+
+# Convenience function for creating tools from existing functions
+def create_tool_from_function(
+ func: Callable,
+ name: Optional[str] = None,
+ description: Optional[str] = None,
+ parameter_descriptions: Optional[Dict[str, str]] = None,
+) -> Tool:
+ """
+ Create a Tool from an existing function without using the decorator.
+
+ Args:
+ func: The function to convert to a tool
+ name: Optional custom name for the tool
+ description: Optional description for the tool
+ parameter_descriptions: Optional parameter descriptions
+
+ Returns:
+ Tool: The created tool object
+ """
+ return _create_tool_from_function(func, name, description, parameter_descriptions)
diff --git a/flo_ai/flo_ai/tools/__init__.py b/flo_ai/flo_ai/tools/__init__.py
deleted file mode 100644
index f7c55444..00000000
--- a/flo_ai/flo_ai/tools/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from flo_ai.tools.flo_tool import flotool
-
-__all__ = ['flotool']
diff --git a/flo_ai/flo_ai/tools/flo_tool.py b/flo_ai/flo_ai/tools/flo_tool.py
deleted file mode 100644
index da6bbd7a..00000000
--- a/flo_ai/flo_ai/tools/flo_tool.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import asyncio
-from langchain.tools import tool
-from functools import wraps
-from typing import Optional
-
-
-def flotool(
- name: str,
- description: Optional[str] = None,
- argument_contract: Optional[type] = None,
- unsafe: bool = False,
-):
- def decorator(func):
- func.__doc__ = func.__doc__ or description
-
- @tool(name, args_schema=argument_contract)
- @wraps(func)
- def wrapper(*args, **kwargs):
- try:
- return func(*args, **kwargs)
- except Exception as e:
- if unsafe:
- raise e
- return f'An error occurred while executing the tool: {str(e)}, please retry with the corresponding fix'
-
- @tool(name, args_schema=argument_contract)
- @wraps(func)
- async def async_wrapper(*args, **kwargs):
- try:
- return await func(*args, **kwargs)
- except Exception as e:
- if unsafe:
- raise e
- return f'An error occurred while executing the tool: {str(e)}, please retry with the corresponding fix'
-
- return async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
-
- return decorator
diff --git a/flo_ai/flo_ai/utils/logger.py b/flo_ai/flo_ai/utils/logger.py
new file mode 100644
index 00000000..a6ebfbc4
--- /dev/null
+++ b/flo_ai/flo_ai/utils/logger.py
@@ -0,0 +1,16 @@
+import logging
+import os
+
+log_level = os.environ.get('LOG_LEVEL', 'INFO')
+logging.getLogger('uvicorn').setLevel(log_level)
+log_format = (
+ '%(asctime)s | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s'
+)
+
+logging.basicConfig(
+ level=log_level,
+ format=log_format,
+ datefmt='%Y-%m-%d %H:%M:%S',
+)
+
+logger = logging.getLogger('floware')
diff --git a/flo_ai/flo_ai/yaml/config.py b/flo_ai/flo_ai/yaml/config.py
deleted file mode 100644
index cb4bff19..00000000
--- a/flo_ai/flo_ai/yaml/config.py
+++ /dev/null
@@ -1,124 +0,0 @@
-from pydantic import BaseModel
-from typing import List, Union, Dict, Any
-import yaml
-import re
-from typing import Optional
-from flo_ai.models.exception import FloValidationException
-
-
-KIND_SUPERVISED_TEAM = 'FloRoutedTeam'
-KIND_FLO_AGENT = 'FloAgent'
-
-yaml_kinds = [KIND_SUPERVISED_TEAM, KIND_FLO_AGENT]
-
-
-class KeyValueArgs(BaseModel):
- name: str
- value: str
-
-
-class FilterArgs(BaseModel):
- name: str
- description: str
- type: str
-
-
-class ToolConfig(BaseModel):
- name: str
- args: Optional[List[KeyValueArgs]] = None
- properties: Optional[List[KeyValueArgs]] = None
- filters: Optional[List[FilterArgs]] = None
-
-
-class MemberKey(BaseModel):
- name: str
-
-
-class Parser(BaseModel):
- name: str
- fields: Optional[List[Dict[str, Any]]] = None
-
-
-class AgentConfig(BaseModel):
- name: str
- role: Optional[str] = None
- kind: Optional[str] = None
- job: Optional[str] = None
- tools: List[ToolConfig] = []
- to: Optional[List[MemberKey]] = None
- retry: Optional[int] = 1
- model: Optional[str] = None
- parser: Union[Parser, str] = None
- data_collector: Optional[str] = None
-
-
-class EdgeConfig(BaseModel):
- edge: List[str]
- type: Optional[str] = None
- rule: Optional[str] = None
-
-
-class RouterConfig(BaseModel):
- name: str
- kind: str
- model: Optional[str] = None
- job: Optional[str] = None
- start_node: Optional[str] = None
- end_node: Union[Optional[str], List[str]] = None
- edges: Optional[List[EdgeConfig]] = None
-
-
-class PlannerConfig(BaseModel):
- name: str
-
-
-class TeamConfig(BaseModel):
- name: str
- kind: Optional[str] = None
- agents: Optional[List[AgentConfig]] = None
- subteams: Optional[List['TeamConfig']] = None
- router: Optional[RouterConfig] = None
- planner: Optional[PlannerConfig] = None
-
-
-class FloRoutedTeamConfig(BaseModel):
- apiVersion: str
- kind: str
- name: str
- team: TeamConfig
-
-
-class FloAgentConfig(BaseModel):
- apiVersion: str
- kind: Optional[str] = None
- name: str
- agent: AgentConfig
-
-
-def to_supervised_team(yaml_str: str) -> FloRoutedTeamConfig:
- parsed_data = yaml.safe_load(yaml_str)
- kind = parsed_data['kind']
- if kind == KIND_SUPERVISED_TEAM:
- flo_supervised_team = FloRoutedTeamConfig(**parsed_data)
- validate_sup_team_config(flo_supervised_team)
- return flo_supervised_team
- elif kind == KIND_FLO_AGENT:
- flo_agent = FloAgentConfig(**parsed_data)
- validate_sup_team_config(flo_agent)
- return flo_agent
- else:
- raise FloValidationException('Unknown kind: {}'.format(kind))
-
-
-def validate_sup_team_config(flo: FloRoutedTeamConfig):
- if flo.kind == KIND_FLO_AGENT:
- return
- if flo.name is None or not is_valid_name(flo.name):
- raise FloValidationException(
- 'Invalid agent name while creating the flow, expected: [^[a-z][a-z0-9_-]*$]'
- )
-
-
-def is_valid_name(s: str) -> bool:
- pattern = r'^[a-z][a-z0-9_-]*$'
- return bool(re.match(pattern, s))
diff --git a/flo_ai/flo_ai/yaml/validators.py b/flo_ai/flo_ai/yaml/validators.py
deleted file mode 100644
index 288a87b2..00000000
--- a/flo_ai/flo_ai/yaml/validators.py
+++ /dev/null
@@ -1,11 +0,0 @@
-import re
-from flo_ai.error.flo_exception import FloException
-
-name_regex = r'^[a-zA-Z0-9-_]+$'
-
-
-def raise_for_name_error(string):
- if not re.match(name_regex, string):
- raise FloException(
- 'Name must contain only alphanumeric characters and hyphens.'
- )
diff --git a/flo_ai/poetry.lock b/flo_ai/poetry.lock
index 9c700e96..04bc8ad5 100644
--- a/flo_ai/poetry.lock
+++ b/flo_ai/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiohappyeyeballs"
@@ -6,6 +6,7 @@ version = "2.6.1"
description = "Happy Eyeballs for asyncio"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"},
{file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"},
@@ -17,6 +18,7 @@ version = "3.11.16"
description = "Async http client/server framework (asyncio)"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa"},
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955"},
@@ -112,7 +114,7 @@ propcache = ">=0.2.0"
yarl = ">=1.17.0,<2.0"
[package.extras]
-speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
+speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.2.0) ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
[[package]]
name = "aiosignal"
@@ -120,6 +122,7 @@ version = "1.3.2"
description = "aiosignal: a list of registered asynchronous callbacks"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
@@ -134,6 +137,7 @@ version = "5.5.0"
description = "Vega-Altair: A declarative statistical visualization library for Python."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c"},
{file = "altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d"},
@@ -158,6 +162,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -169,6 +174,7 @@ version = "0.49.0"
description = "The official Python library for the anthropic API"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375"},
{file = "anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398"},
@@ -193,6 +199,7 @@ version = "1.2.1"
description = "A Pythonic Helper for DBAPI-2.0 SQL Access"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "antiorm-1.2.1.tar.gz", hash = "sha256:96eb1841ce5163db4cf1dc13f4499ec2d7cffc190cf724b78ffdd3e6b7c4ff93"},
]
@@ -203,6 +210,7 @@ version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
@@ -216,7 +224,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
-test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
+test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
@@ -225,6 +233,8 @@ version = "0.1.4"
description = "Disable App Nap on macOS >= 10.9"
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
+markers = "platform_system == \"Darwin\""
files = [
{file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"},
{file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"},
@@ -236,6 +246,7 @@ version = "1.5.1"
description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"},
{file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
@@ -247,6 +258,7 @@ version = "3.0.0"
description = "Annotate AST trees with source code positions"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
@@ -262,6 +274,8 @@ version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
+markers = "python_version == \"3.10\""
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@@ -273,18 +287,19 @@ version = "25.3.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
[package.extras]
-benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
-tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "beautifulsoup4"
@@ -292,6 +307,7 @@ version = "4.13.3"
description = "Screen-scraping library"
optional = false
python-versions = ">=3.7.0"
+groups = ["dev"]
files = [
{file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"},
{file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"},
@@ -314,6 +330,7 @@ version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
@@ -325,6 +342,7 @@ version = "1.36.1"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "boto3-1.36.1-py3-none-any.whl", hash = "sha256:eb21380d73fec6645439c0d802210f72a0cdb3295b02953f246ff53f512faa8f"},
{file = "boto3-1.36.1.tar.gz", hash = "sha256:258ab77225a81d3cf3029c9afe9920cd9dec317689dfadec6f6f0a23130bb60a"},
@@ -344,6 +362,7 @@ version = "1.36.1"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "botocore-1.36.1-py3-none-any.whl", hash = "sha256:dec513b4eb8a847d79bbefdcdd07040ed9d44c20b0001136f0890a03d595705a"},
{file = "botocore-1.36.1.tar.gz", hash = "sha256:f789a6f272b5b3d8f8756495019785e33868e5e00dd9662a3ee7959ac939bb12"},
@@ -363,6 +382,7 @@ version = "5.5.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"},
{file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"},
@@ -374,6 +394,7 @@ version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
+groups = ["main", "dev"]
files = [
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
@@ -385,6 +406,8 @@ version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "implementation_name == \"pypy\""
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
@@ -464,6 +487,7 @@ version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
@@ -475,6 +499,7 @@ version = "3.4.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
@@ -576,6 +601,7 @@ version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
@@ -590,10 +616,12 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main", "dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
+markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@@ -601,6 +629,7 @@ version = "0.2.2"
description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"},
{file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"},
@@ -618,6 +647,7 @@ version = "0.6.7"
description = "Easily serialize dataclasses to and from JSON."
optional = false
python-versions = "<4.0,>=3.7"
+groups = ["main"]
files = [
{file = "dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a"},
{file = "dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0"},
@@ -633,6 +663,7 @@ version = "0.1.1"
description = "Databases for Humans"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "db-0.1.1.tar.gz", hash = "sha256:980e772f15c1161d3b287ffec4f144e40961b0b3e6d5102809577870bf6c5808"},
]
@@ -646,6 +677,7 @@ version = "0.0.1"
description = "sqlite3 driver for db"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "db-sqlite3-0.0.1.tar.gz", hash = "sha256:4dd410aa28a2c5b66de477f9dc62f523133217dd31c7ed7eb085214bea148db9"},
]
@@ -659,6 +691,7 @@ version = "1.8.13"
description = "An implementation of the Debug Adapter Protocol for Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "debugpy-1.8.13-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:06859f68e817966723ffe046b896b1bd75c665996a77313370336ee9e1de3e90"},
{file = "debugpy-1.8.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c2db69fb8df3168bc857d7b7d2494fed295dfdbde9a45f27b4b152f37520"},
@@ -694,6 +727,7 @@ version = "5.2.1"
description = "Decorators for Humans"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
@@ -705,6 +739,7 @@ version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+groups = ["dev"]
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
@@ -716,6 +751,7 @@ version = "0.3.9"
description = "Distribution utilities"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
@@ -727,6 +763,7 @@ version = "1.9.0"
description = "Distro - an OS platform information API"
optional = false
python-versions = ">=3.6"
+groups = ["main", "dev"]
files = [
{file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"},
{file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"},
@@ -738,6 +775,7 @@ version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
@@ -758,6 +796,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
+markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -772,13 +812,14 @@ version = "2.2.0"
description = "Get the currently executing AST node of a frame, and other information"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"},
{file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"},
]
[package.extras]
-tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
+tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""]
[[package]]
name = "filelock"
@@ -786,6 +827,7 @@ version = "3.18.0"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"},
{file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"},
@@ -794,7 +836,7 @@ files = [
[package.extras]
docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"]
-typing = ["typing-extensions (>=4.12.2)"]
+typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""]
[[package]]
name = "flo-ai-tools"
@@ -802,6 +844,7 @@ version = "0.0.1"
description = "Some good tool implementations for flo-ai"
optional = false
python-versions = ">=3.9,<4.0"
+groups = ["dev"]
files = []
develop = true
@@ -818,6 +861,7 @@ version = "1.5.0"
description = "A list-like structure which implements collections.abc.MutableSequence"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"},
{file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"},
@@ -919,6 +963,7 @@ version = "4.0.12"
description = "Git Object Database"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"},
{file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"},
@@ -933,6 +978,7 @@ version = "3.1.44"
description = "GitPython is a Python library used to interact with Git repositories"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"},
{file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"},
@@ -943,7 +989,7 @@ gitdb = ">=4.0.1,<5"
[package.extras]
doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"]
-test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"]
+test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""]
[[package]]
name = "greenlet"
@@ -951,6 +997,8 @@ version = "3.1.1"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
+markers = "(platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version < \"3.14\""
files = [
{file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"},
{file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"},
@@ -1037,6 +1085,7 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -1048,6 +1097,7 @@ version = "1.0.7"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
@@ -1069,6 +1119,7 @@ version = "0.27.0"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
@@ -1082,7 +1133,7 @@ idna = "*"
sniffio = "*"
[package.extras]
-brotli = ["brotli", "brotlicffi"]
+brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@@ -1093,6 +1144,7 @@ version = "2.6.9"
description = "File identification library for Python"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"},
{file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"},
@@ -1107,6 +1159,7 @@ version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
+groups = ["main", "dev"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
@@ -1121,6 +1174,7 @@ version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
@@ -1132,6 +1186,7 @@ version = "6.29.5"
description = "IPython Kernel for Jupyter"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"},
{file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"},
@@ -1165,6 +1220,7 @@ version = "8.35.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.10"
+groups = ["dev"]
files = [
{file = "ipython-8.35.0-py3-none-any.whl", hash = "sha256:e6b7470468ba6f1f0a7b116bb688a3ece2f13e2f94138e508201fad677a788ba"},
{file = "ipython-8.35.0.tar.gz", hash = "sha256:d200b7d93c3f5883fc36ab9ce28a18249c7706e51347681f80a0aef9895f2520"},
@@ -1186,7 +1242,7 @@ typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
[package.extras]
all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"]
black = ["black"]
-doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"]
+doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli ; python_version < \"3.11\"", "typing_extensions"]
kernel = ["ipykernel"]
matplotlib = ["matplotlib"]
nbconvert = ["nbconvert"]
@@ -1203,6 +1259,7 @@ version = "0.19.2"
description = "An autocompletion tool for Python that can be used for text editors."
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
@@ -1222,6 +1279,7 @@ version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
@@ -1239,6 +1297,7 @@ version = "0.9.0"
description = "Fast iterable JSON parser."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "jiter-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:816ec9b60fdfd1fec87da1d7ed46c66c44ffec37ab2ef7de5b147b2fce3fd5ad"},
{file = "jiter-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b1d3086f8a3ee0194ecf2008cf81286a5c3e540d977fa038ff23576c023c0ea"},
@@ -1324,6 +1383,7 @@ version = "1.0.1"
description = "JSON Matching Expressions"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"},
{file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"},
@@ -1335,6 +1395,7 @@ version = "1.33"
description = "Apply JSON-Patches (RFC 6902)"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
+groups = ["main", "dev"]
files = [
{file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"},
{file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"},
@@ -1349,6 +1410,7 @@ version = "3.0.0"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"},
{file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"},
@@ -1360,6 +1422,7 @@ version = "4.23.0"
description = "An implementation of JSON Schema validation for Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"},
{file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"},
@@ -1381,6 +1444,7 @@ version = "2024.10.1"
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"},
{file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"},
@@ -1395,6 +1459,7 @@ version = "8.6.3"
description = "Jupyter protocol implementation and client libraries"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"},
{file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"},
@@ -1409,7 +1474,7 @@ traitlets = ">=5.3"
[package.extras]
docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"]
-test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
+test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"]
[[package]]
name = "jupyter-core"
@@ -1417,6 +1482,7 @@ version = "5.7.2"
description = "Jupyter core package. A base package on which Jupyter projects rely."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"},
{file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"},
@@ -1437,6 +1503,7 @@ version = "0.3.23"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["main"]
files = [
{file = "langchain-0.3.23-py3-none-any.whl", hash = "sha256:084f05ee7e80b7c3f378ebadd7309f2a37868ce2906fa0ae64365a67843ade3d"},
{file = "langchain-0.3.23.tar.gz", hash = "sha256:d95004afe8abebb52d51d6026270248da3f4b53d93e9bf699f76005e0c83ad34"},
@@ -1477,6 +1544,7 @@ version = "0.2.4"
description = "An integration package connecting AnthropicMessages and LangChain"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["dev"]
files = [
{file = "langchain_anthropic-0.2.4-py3-none-any.whl", hash = "sha256:bcb6c2d0df4a67aff52816621079d6e743b260911caccf313a72b33b7edece6f"},
{file = "langchain_anthropic-0.2.4.tar.gz", hash = "sha256:0382d4c7b5236839b703f7b72b3e06de4bb5be99104b193f719adbe34c49562b"},
@@ -1494,6 +1562,7 @@ version = "0.2.13"
description = "An integration package connecting AWS and LangChain"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["dev"]
files = [
{file = "langchain_aws-0.2.13-py3-none-any.whl", hash = "sha256:f914c046d8e92b7e721f18916b2fe3dc1a1715c2abe9f60ecd41f844942a123b"},
{file = "langchain_aws-0.2.13.tar.gz", hash = "sha256:2dc41928ff35f0e37cf521e976f6e87f8263b0a27cfb5007394241f56d1d6644"},
@@ -1514,6 +1583,7 @@ version = "0.3.2"
description = "Community contributed LangChain integrations."
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["main"]
files = [
{file = "langchain_community-0.3.2-py3-none-any.whl", hash = "sha256:fffcd484c7674e81ceaa72a809962338bfb17ec8f9e0377ce4e9d884e6fe8ca5"},
{file = "langchain_community-0.3.2.tar.gz", hash = "sha256:469bf5357a08c915cebc4c506dca4617eec737d82a9b6e340df5f3b814dc89bc"},
@@ -1541,6 +1611,7 @@ version = "0.3.51"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["main", "dev"]
files = [
{file = "langchain_core-0.3.51-py3-none-any.whl", hash = "sha256:4bd71e8acd45362aa428953f2a91d8162318014544a2216e4b769463caf68e13"},
{file = "langchain_core-0.3.51.tar.gz", hash = "sha256:db76b9cc331411602cb40ba0469a161febe7a0663fbcaddbc9056046ac2d22f4"},
@@ -1564,6 +1635,7 @@ version = "0.2.0"
description = "An integration package connecting MongoDB and LangChain"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["dev"]
files = [
{file = "langchain_mongodb-0.2.0-py3-none-any.whl", hash = "sha256:c18139f799e5593f204d8d3d294a7ade5ff4ec2d0fa35a12c93c82b7ba50d533"},
{file = "langchain_mongodb-0.2.0.tar.gz", hash = "sha256:e5daf08edf56e0a86630cdba87c7e6c3305b6c38fd6e63e71fce86b1396ec65a"},
@@ -1583,6 +1655,7 @@ version = "0.2.14"
description = "An integration package connecting OpenAI and LangChain"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["dev"]
files = [
{file = "langchain_openai-0.2.14-py3-none-any.whl", hash = "sha256:d232496662f79ece9a11caf7d798ba863e559c771bc366814f7688e0fe664fe8"},
{file = "langchain_openai-0.2.14.tar.gz", hash = "sha256:7a514f309e356b182a337c0ed36ab3fbe34d9834a235a3b85cb7f91ae775d978"},
@@ -1599,6 +1672,7 @@ version = "0.3.8"
description = "LangChain text splitting utilities"
optional = false
python-versions = "<4.0,>=3.9"
+groups = ["main"]
files = [
{file = "langchain_text_splitters-0.3.8-py3-none-any.whl", hash = "sha256:e75cc0f4ae58dcf07d9f18776400cf8ade27fadd4ff6d264df6278bb302f6f02"},
{file = "langchain_text_splitters-0.3.8.tar.gz", hash = "sha256:116d4b9f2a22dda357d0b79e30acf005c5518177971c66a9f1ab0edfdb0f912e"},
@@ -1613,6 +1687,7 @@ version = "0.1.21"
description = "The LangChain Hub API client"
optional = false
python-versions = "<4.0,>=3.8.1"
+groups = ["dev"]
files = [
{file = "langchainhub-0.1.21-py3-none-any.whl", hash = "sha256:1cc002dc31e0d132a776afd044361e2b698743df5202618cf2bad399246b895f"},
{file = "langchainhub-0.1.21.tar.gz", hash = "sha256:723383b3964a47dbaea6ad5d0ef728accefbc9d2c07480e800bdec43510a8c10"},
@@ -1629,6 +1704,7 @@ version = "0.2.38"
description = "Building stateful, multi-actor applications with LLMs"
optional = false
python-versions = "<4.0,>=3.9.0"
+groups = ["main"]
files = [
{file = "langgraph-0.2.38-py3-none-any.whl", hash = "sha256:71db9cf41d7f60e2af9a8f972848be87c79040d88665815d1cf42b1e332ce702"},
{file = "langgraph-0.2.38.tar.gz", hash = "sha256:7a6b104d3951cbe05551678eea916bd74be98752a4281a5bf72562de05239e78"},
@@ -1645,6 +1721,7 @@ version = "2.0.24"
description = "Library with base interfaces for LangGraph checkpoint savers."
optional = false
python-versions = "<4.0.0,>=3.9.0"
+groups = ["main"]
files = [
{file = "langgraph_checkpoint-2.0.24-py3-none-any.whl", hash = "sha256:3836e2909ef2387d1fa8d04ee3e2a353f980d519fd6c649af352676dc73d66b8"},
{file = "langgraph_checkpoint-2.0.24.tar.gz", hash = "sha256:9596dad332344e7e871257be464df8a07c2e9bac66143081b11b9422b0167e5b"},
@@ -1660,6 +1737,7 @@ version = "0.1.61"
description = "SDK for interacting with LangGraph API"
optional = false
python-versions = "<4.0.0,>=3.9.0"
+groups = ["main"]
files = [
{file = "langgraph_sdk-0.1.61-py3-none-any.whl", hash = "sha256:f2d774b12497c428862993090622d51e0dbc3f53e0cee3d74a13c7495d835cc6"},
{file = "langgraph_sdk-0.1.61.tar.gz", hash = "sha256:87dd1f07ab82da8875ac343268ece8bf5414632017ebc9d1cef4b523962fd601"},
@@ -1675,6 +1753,7 @@ version = "0.1.147"
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
optional = false
python-versions = "<4.0,>=3.8.1"
+groups = ["main", "dev"]
files = [
{file = "langsmith-0.1.147-py3-none-any.whl", hash = "sha256:7166fc23b965ccf839d64945a78e9f1157757add228b086141eb03a60d699a15"},
{file = "langsmith-0.1.147.tar.gz", hash = "sha256:2e933220318a4e73034657103b3b1a3a6109cc5db3566a7e8e03be8d6d7def7a"},
@@ -1699,6 +1778,7 @@ version = "5.3.2"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "lxml-5.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c4b84d6b580a9625dfa47269bf1fd7fbba7ad69e08b16366a46acb005959c395"},
{file = "lxml-5.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4c08ecb26e4270a62f81f81899dfff91623d349e433b126931c9c4577169666"},
@@ -1853,6 +1933,7 @@ version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
@@ -1923,6 +2004,7 @@ version = "3.26.1"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"},
{file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"},
@@ -1942,6 +2024,7 @@ version = "0.1.7"
description = "Inline Matplotlib backend for Jupyter"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
@@ -1956,6 +2039,7 @@ version = "6.3.2"
description = "multidict implementation"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "multidict-6.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3dc0eec9304fa04d84a51ea13b0ec170bace5b7ddeaac748149efd316f1504"},
{file = "multidict-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9534f3d84addd3b6018fa83f97c9d4247aaa94ac917d1ed7b2523306f99f5c16"},
@@ -2060,6 +2144,7 @@ version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
+groups = ["main"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
@@ -2071,6 +2156,7 @@ version = "1.34.0"
description = "Extremely lightweight compatibility layer between dataframe libraries"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "narwhals-1.34.0-py3-none-any.whl", hash = "sha256:9502b9aa5dfe125c090a3a0bbca95becfa1fac2cd67f8b80d12b1dc2ed751865"},
{file = "narwhals-1.34.0.tar.gz", hash = "sha256:bdd3fa60bea1f1e8b698e483be18dd43af13290da12dba69ea16dc1f3edbb8f7"},
@@ -2094,6 +2180,7 @@ version = "1.6.0"
description = "Patch asyncio to allow nested event loops"
optional = false
python-versions = ">=3.5"
+groups = ["dev"]
files = [
{file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"},
{file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"},
@@ -2105,6 +2192,7 @@ version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["dev"]
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
@@ -2116,6 +2204,7 @@ version = "1.26.4"
description = "Fundamental package for array computing in Python"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"},
{file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"},
@@ -2157,13 +2246,14 @@ files = [
[[package]]
name = "openai"
-version = "1.71.0"
+version = "1.77.0"
description = "The official Python library for the openai API"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
- {file = "openai-1.71.0-py3-none-any.whl", hash = "sha256:e1c643738f1fff1af52bce6ef06a7716c95d089281e7011777179614f32937aa"},
- {file = "openai-1.71.0.tar.gz", hash = "sha256:52b20bb990a1780f9b0b8ccebac93416343ebd3e4e714e3eff730336833ca207"},
+ {file = "openai-1.77.0-py3-none-any.whl", hash = "sha256:07706e91eb71631234996989a8ea991d5ee56f0744ef694c961e0824d4f39218"},
+ {file = "openai-1.77.0.tar.gz", hash = "sha256:897969f927f0068b8091b4b041d1f8175bcf124f7ea31bab418bf720971223bc"},
]
[package.dependencies]
@@ -2187,6 +2277,7 @@ version = "3.10.16"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"},
@@ -2257,6 +2348,7 @@ files = [
{file = "orjson-3.10.16-cp39-cp39-win_amd64.whl", hash = "sha256:c338dc2296d1ed0d5c5c27dfb22d00b330555cb706c2e0be1e1c3940a0895905"},
{file = "orjson-3.10.16.tar.gz", hash = "sha256:d2aaa5c495e11d17b9b93205f5fa196737ee3202f000aaebf028dc9a73750f10"},
]
+markers = {dev = "platform_python_implementation != \"PyPy\""}
[[package]]
name = "ormsgpack"
@@ -2264,6 +2356,7 @@ version = "1.9.1"
description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "ormsgpack-1.9.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f1f804fd9c0fd84213a6022c34172f82323b34afa7052a4af18797582cf56365"},
{file = "ormsgpack-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eab5cec99c46276b37071d570aab98603f3d0309b3818da3247eb64bb95e5cfc"},
@@ -2314,6 +2407,7 @@ version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
@@ -2325,6 +2419,7 @@ version = "2.2.3"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
{file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
@@ -2411,6 +2506,7 @@ version = "0.8.4"
description = "A Python Parser"
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"},
{file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"},
@@ -2426,6 +2522,7 @@ version = "3.17.9"
description = "a little orm"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "peewee-3.17.9.tar.gz", hash = "sha256:fe15cd001758e324c8e3ca8c8ed900e7397c2907291789e1efc383e66b9bc7a8"},
]
@@ -2436,6 +2533,8 @@ version = "4.9.0"
description = "Pexpect allows easy control of interactive console applications."
optional = false
python-versions = "*"
+groups = ["dev"]
+markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
files = [
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
@@ -2450,6 +2549,7 @@ version = "10.4.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
{file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
@@ -2538,7 +2638,7 @@ docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
-typing = ["typing-extensions"]
+typing = ["typing-extensions ; python_version < \"3.10\""]
xmp = ["defusedxml"]
[[package]]
@@ -2547,6 +2647,7 @@ version = "4.3.7"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"},
{file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"},
@@ -2563,6 +2664,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -2578,6 +2680,7 @@ version = "4.2.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"},
{file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"},
@@ -2596,6 +2699,7 @@ version = "3.0.50"
description = "Library for building powerful interactive command lines in Python"
optional = false
python-versions = ">=3.8.0"
+groups = ["dev"]
files = [
{file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"},
{file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"},
@@ -2610,6 +2714,7 @@ version = "0.3.1"
description = "Accelerated property cache"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"},
{file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"},
@@ -2717,6 +2822,7 @@ version = "5.29.4"
description = ""
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"},
{file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"},
@@ -2737,6 +2843,7 @@ version = "7.0.0"
description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7."
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"},
{file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"},
@@ -2760,6 +2867,8 @@ version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = false
python-versions = "*"
+groups = ["dev"]
+markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
@@ -2771,6 +2880,7 @@ version = "0.2.3"
description = "Safely evaluate AST nodes without side effects"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
@@ -2785,6 +2895,7 @@ version = "19.0.1"
description = "Python library for Apache Arrow"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69"},
{file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec"},
@@ -2839,6 +2950,8 @@ version = "2.22"
description = "C parser in Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "implementation_name == \"pypy\""
files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
@@ -2850,6 +2963,7 @@ version = "2.11.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7"},
{file = "pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e"},
@@ -2863,7 +2977,7 @@ typing-inspection = ">=0.4.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
@@ -2871,6 +2985,7 @@ version = "2.33.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"},
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"},
@@ -2982,6 +3097,7 @@ version = "2.8.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"},
{file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"},
@@ -3002,6 +3118,7 @@ version = "0.9.1"
description = "Widget for deck.gl maps"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038"},
{file = "pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605"},
@@ -3013,7 +3130,7 @@ numpy = ">=1.16.4"
[package.extras]
carto = ["pydeck-carto"]
-jupyter = ["ipykernel (>=5.1.2)", "ipython (>=5.8.0)", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"]
+jupyter = ["ipykernel (>=5.1.2) ; python_version >= \"3.4\"", "ipython (>=5.8.0) ; python_version < \"3.4\"", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"]
[[package]]
name = "pygments"
@@ -3021,6 +3138,7 @@ version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
@@ -3035,6 +3153,7 @@ version = "4.11.3"
description = "Python driver for MongoDB "
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "pymongo-4.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78f19598246dd61ba2a4fc4dddfa6a4f9af704fff7d81cb4fe0d02c7b17b1f68"},
{file = "pymongo-4.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c9cbe81184ec81ad8c76ccedbf5b743639448008d68f51f9a3c8a9abe6d9a46"},
@@ -3101,9 +3220,9 @@ dnspython = ">=1.16.0,<3.0.0"
[package.extras]
aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"]
docs = ["furo (==2024.8.6)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<9)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<4)", "sphinxcontrib-shellcheck (>=1,<2)"]
-encryption = ["certifi", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.12.0,<2.0.0)"]
-gssapi = ["pykerberos", "winkerberos (>=0.5.0)"]
-ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
+encryption = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.12.0,<2.0.0)"]
+gssapi = ["pykerberos ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""]
+ocsp = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"]
snappy = ["python-snappy"]
test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"]
zstd = ["zstandard"]
@@ -3114,6 +3233,7 @@ version = "4.3.1"
description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files"
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "pypdf-4.3.1-py3-none-any.whl", hash = "sha256:64b31da97eda0771ef22edb1bfecd5deee4b72c3d1736b7df2689805076d6418"},
{file = "pypdf-4.3.1.tar.gz", hash = "sha256:b2f37fe9a3030aa97ca86067a56ba3f9d3565f9a791b305c7355d8392c30d91b"},
@@ -3123,10 +3243,10 @@ files = [
typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
-crypto = ["PyCryptodome", "cryptography"]
+crypto = ["PyCryptodome ; python_version == \"3.6\"", "cryptography ; python_version >= \"3.7\""]
dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"]
docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"]
-full = ["Pillow (>=8.0.0)", "PyCryptodome", "cryptography"]
+full = ["Pillow (>=8.0.0)", "PyCryptodome ; python_version == \"3.6\"", "cryptography ; python_version >= \"3.7\""]
image = ["Pillow (>=8.0.0)"]
[[package]]
@@ -3135,6 +3255,7 @@ version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
@@ -3157,6 +3278,7 @@ version = "0.24.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
@@ -3175,6 +3297,7 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -3189,6 +3312,7 @@ version = "1.1.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
@@ -3203,6 +3327,7 @@ version = "2025.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"},
{file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"},
@@ -3214,6 +3339,8 @@ version = "310"
description = "Python for Window Extensions"
optional = false
python-versions = "*"
+groups = ["dev"]
+markers = "platform_python_implementation != \"PyPy\" and sys_platform == \"win32\""
files = [
{file = "pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1"},
{file = "pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d"},
@@ -3239,6 +3366,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
@@ -3301,6 +3429,7 @@ version = "26.4.0"
description = "Python bindings for 0MQ"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pyzmq-26.4.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:0329bdf83e170ac133f44a233fc651f6ed66ef8e66693b5af7d54f45d1ef5918"},
{file = "pyzmq-26.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:398a825d2dea96227cf6460ce0a174cf7657d6f6827807d4d1ae9d0f9ae64315"},
@@ -3406,6 +3535,7 @@ version = "2.1.5"
description = "Redshift interface library"
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "redshift_connector-2.1.5-py3-none-any.whl", hash = "sha256:a90e5644a1d8f58f9d6d62c6ee000bb7788dcbfb9c9b3b4e114d66ccbfc82478"},
]
@@ -3430,6 +3560,7 @@ version = "0.36.2"
description = "JSON Referencing + Python"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"},
{file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"},
@@ -3446,6 +3577,7 @@ version = "2024.11.6"
description = "Alternative regular expression module, to replace re."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"},
{file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"},
@@ -3549,6 +3681,7 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -3570,6 +3703,7 @@ version = "1.0.0"
description = "A utility belt for advanced users of python-requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+groups = ["main", "dev"]
files = [
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
@@ -3584,6 +3718,7 @@ version = "0.24.0"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"},
{file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"},
@@ -3707,6 +3842,7 @@ version = "0.11.3"
description = "An Amazon S3 Transfer Manager"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "s3transfer-0.11.3-py3-none-any.whl", hash = "sha256:ca855bdeb885174b5ffa95b9913622459d4ad8e331fc98eb01e6d5eb6a30655d"},
{file = "s3transfer-0.11.3.tar.gz", hash = "sha256:edae4977e3a122445660c7c114bba949f9d191bae3b34a096f18a1c8c354527a"},
@@ -3724,6 +3860,7 @@ version = "1.4.5"
description = "An implementation of the SCRAM protocol."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7"},
{file = "scramp-1.4.5.tar.gz", hash = "sha256:be3fbe774ca577a7a658117dca014e5d254d158cecae3dd60332dfe33ce6d78e"},
@@ -3738,19 +3875,20 @@ version = "78.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"},
{file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"},
]
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
-core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
+core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "six"
@@ -3758,6 +3896,7 @@ version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["dev"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
@@ -3769,6 +3908,7 @@ version = "5.0.2"
description = "A pure Python implementation of a sliding window memory map manager"
optional = false
python-versions = ">=3.7"
+groups = ["dev"]
files = [
{file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"},
{file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"},
@@ -3780,6 +3920,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -3791,6 +3932,7 @@ version = "2.6"
description = "A modern CSS selector implementation for Beautiful Soup."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"},
{file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"},
@@ -3802,6 +3944,7 @@ version = "2.0.40"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"},
{file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"},
@@ -3897,6 +4040,7 @@ version = "0.6.3"
description = "Extract data from python stack frames and tracebacks for informative displays"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
@@ -3916,6 +4060,7 @@ version = "1.44.1"
description = "A faster way to build and share data apps"
optional = false
python-versions = "!=3.9.7,>=3.9"
+groups = ["dev"]
files = [
{file = "streamlit-1.44.1-py3-none-any.whl", hash = "sha256:9fe355f58b11f4eb71e74f115ce1f38c4c9eaff2733e6bcffb510ac1298a5990"},
{file = "streamlit-1.44.1.tar.gz", hash = "sha256:c6914ed6d5b76870b461510476806db370f36425ae0e6654d227c988288198d3"},
@@ -3942,7 +4087,7 @@ typing-extensions = ">=4.4.0,<5"
watchdog = {version = ">=2.1.5,<7", markers = "platform_system != \"Darwin\""}
[package.extras]
-snowflake = ["snowflake-connector-python (>=3.3.0)", "snowflake-snowpark-python[modin] (>=1.17.0)"]
+snowflake = ["snowflake-connector-python (>=3.3.0) ; python_version < \"3.12\"", "snowflake-snowpark-python[modin] (>=1.17.0) ; python_version < \"3.12\""]
[[package]]
name = "tenacity"
@@ -3950,6 +4095,7 @@ version = "8.5.0"
description = "Retry code until it succeeds"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"},
{file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"},
@@ -3965,6 +4111,7 @@ version = "0.9.0"
description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"},
{file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"},
@@ -4012,6 +4159,7 @@ version = "0.10.2"
description = "Python Library for Tom's Obvious, Minimal Language"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+groups = ["dev"]
files = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -4023,6 +4171,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -4064,6 +4214,7 @@ version = "6.4.2"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"},
{file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"},
@@ -4084,6 +4235,7 @@ version = "4.67.1"
description = "Fast, Extensible Progress Meter"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"},
{file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"},
@@ -4105,6 +4257,7 @@ version = "5.14.3"
description = "Traitlets Python configuration system"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
@@ -4120,6 +4273,7 @@ version = "2.32.0.20250328"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "types_requests-2.32.0.20250328-py3-none-any.whl", hash = "sha256:72ff80f84b15eb3aa7a8e2625fffb6a93f2ad5a0c20215fc1dcfa61117bcb2a2"},
{file = "types_requests-2.32.0.20250328.tar.gz", hash = "sha256:c9e67228ea103bd811c96984fac36ed2ae8da87a36a633964a21f199d60baf32"},
@@ -4134,6 +4288,7 @@ version = "4.13.1"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"},
{file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"},
@@ -4145,6 +4300,7 @@ version = "0.9.0"
description = "Runtime inspection utilities for typing module."
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"},
{file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"},
@@ -4160,6 +4316,7 @@ version = "0.4.0"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"},
{file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"},
@@ -4174,6 +4331,7 @@ version = "2025.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
+groups = ["dev"]
files = [
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
@@ -4185,13 +4343,14 @@ version = "2.3.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
{file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
@@ -4202,6 +4361,7 @@ version = "20.30.0"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"},
{file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"},
@@ -4214,7 +4374,7 @@ platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
-test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""]
[[package]]
name = "watchdog"
@@ -4222,6 +4382,8 @@ version = "6.0.0"
description = "Filesystem events monitoring"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
+markers = "platform_system != \"Darwin\""
files = [
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"},
{file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"},
@@ -4264,6 +4426,7 @@ version = "0.2.13"
description = "Measures the displayed width of unicode strings in a terminal"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"},
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
@@ -4275,6 +4438,7 @@ version = "1.4.0"
description = "Wikipedia API for Python"
optional = false
python-versions = "*"
+groups = ["dev"]
files = [
{file = "wikipedia-1.4.0.tar.gz", hash = "sha256:db0fad1829fdd441b1852306e9856398204dc0786d2996dd2e0c8bb8e26133b2"},
]
@@ -4289,6 +4453,7 @@ version = "1.19.0"
description = "Yet another URL library"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "yarl-1.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0bae32f8ebd35c04d6528cedb4a26b8bf25339d3616b04613b97347f919b76d3"},
{file = "yarl-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8015a076daf77823e7ebdcba474156587391dab4e70c732822960368c01251e6"},
@@ -4385,6 +4550,6 @@ multidict = ">=4.0"
propcache = ">=0.2.1"
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = ">=3.10,<4.0"
-content-hash = "edced44d0ba7dfabafa474bdb4a90d24569fdd9a8d55514e1908cf23643735e9"
+content-hash = "e1ccfa91a73d3970942a520f574a3072abfef7379a7353fe580ed2bc56575166"
diff --git a/flo_ai/pyproject.toml b/flo_ai/pyproject.toml
index 724de4e7..2ed2424c 100644
--- a/flo_ai/pyproject.toml
+++ b/flo_ai/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "flo_ai"
-version = "0.0.6-rc2"
+version = "0.0.7-rc1"
description = "A easy way to create structured AI agents"
authors = ["rootflo <*@rootflo.ai>"]
license = "MIT"
@@ -13,10 +13,11 @@ packages = [
python = ">=3.10,<4.0"
langchain = ">=0.3.3,<4.0"
langgraph = "0.2.38"
-httpx = "0.27.0"
+httpx = ">=0.27.0"
pillow = "^10.3.0"
pydantic = "^2.9.2"
langchain-community = "0.3.2"
+openai = "^1.77.0"
[tool.poetry.group.dev.dependencies]
diff --git a/flo_ai/setup.py b/flo_ai/setup.py
index 867c59db..ab432d9a 100644
--- a/flo_ai/setup.py
+++ b/flo_ai/setup.py
@@ -5,7 +5,7 @@
setuptools.setup(
name='flo-ai',
- version='0.0.6-rc2',
+ version='0.0.7-rc1',
author='Rootflo',
description='Create composable AI agents',
long_description=long_description,
diff --git a/flo_ai/test/test_flo_tool.py b/flo_ai/test/test_flo_tool.py
new file mode 100644
index 00000000..40914ce9
--- /dev/null
+++ b/flo_ai/test/test_flo_tool.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+"""
+Pytest tests for the @flo_tool decorator.
+"""
+
+import sys
+import os
+import pytest
+
+# Add the flo_ai directory to the path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from flo_ai.tool import flo_tool
+
+
+@flo_tool(
+ description='Add two numbers together',
+ parameter_descriptions={'a': 'First number to add', 'b': 'Second number to add'},
+)
+async def add_numbers(a: float, b: float) -> float:
+ """Add two numbers and return the result."""
+ return a + b
+
+
+@flo_tool()
+async def multiply_numbers(x: int, y: int) -> int:
+ """
+ Multiply two integers.
+
+ Args:
+ x: First integer
+ y: Second integer
+ """
+ return x * y
+
+
+class TestFloToolDecorator:
+ """Test class for @flo_tool decorator functionality."""
+
+ @pytest.mark.asyncio
+ async def test_function_calls_work_normally(self):
+ """Test that decorated functions can be called normally."""
+ result1 = await add_numbers(5, 3)
+ assert result1 == 8.0
+
+ result2 = await multiply_numbers(4, 7)
+ assert result2 == 28
+
+ @pytest.mark.asyncio
+ async def test_tool_objects_are_accessible(self):
+ """Test that tool objects are properly attached to decorated functions."""
+ # Test add_numbers tool
+ assert hasattr(add_numbers, 'tool')
+ assert add_numbers.tool.name == 'add_numbers'
+ assert add_numbers.tool.description == 'Add two numbers together'
+ assert 'a' in add_numbers.tool.parameters
+ assert 'b' in add_numbers.tool.parameters
+
+ # Test multiply_numbers tool
+ assert hasattr(multiply_numbers, 'tool')
+ assert multiply_numbers.tool.name == 'multiply_numbers'
+ # Should use docstring as description when none provided
+ assert 'Multiply two integers' in multiply_numbers.tool.description
+ assert 'x' in multiply_numbers.tool.parameters
+ assert 'y' in multiply_numbers.tool.parameters
+
+ @pytest.mark.asyncio
+ async def test_tool_execution_works(self):
+ """Test that tool.execute() method works correctly."""
+ tool_result1 = await add_numbers.tool.execute(a=10, b=20)
+ assert tool_result1 == 30.0
+
+ tool_result2 = await multiply_numbers.tool.execute(x=6, y=8)
+ assert tool_result2 == 48
+
+ @pytest.mark.asyncio
+ async def test_parameter_types_are_preserved(self):
+ """Test that parameter types are correctly preserved in tool metadata."""
+ # Check add_numbers parameters
+ a_param = add_numbers.tool.parameters.get('a', {})
+ assert a_param.get('type') == 'number' # float should be mapped to number
+
+ b_param = add_numbers.tool.parameters.get('b', {})
+ assert b_param.get('type') == 'number'
+
+ # Check multiply_numbers parameters
+ x_param = multiply_numbers.tool.parameters.get('x', {})
+ assert x_param.get('type') == 'integer'
+
+ y_param = multiply_numbers.tool.parameters.get('y', {})
+ assert y_param.get('type') == 'integer'
+
+ @pytest.mark.asyncio
+ async def test_parameter_descriptions_are_set(self):
+ """Test that parameter descriptions are correctly set."""
+ # add_numbers has explicit parameter descriptions
+ a_param = add_numbers.tool.parameters.get('a', {})
+ assert a_param.get('description') == 'First number to add'
+
+ b_param = add_numbers.tool.parameters.get('b', {})
+ assert b_param.get('description') == 'Second number to add'
+
+ # multiply_numbers should have default descriptions or be inferred
+ multiply_numbers.tool.parameters.get('x', {})
+ multiply_numbers.tool.parameters.get('y', {})
+ # At minimum, parameters should exist even if no explicit descriptions
+ assert 'x' in multiply_numbers.tool.parameters
+ assert 'y' in multiply_numbers.tool.parameters
+
+
+class TestFloToolEdgeCases:
+ """Test edge cases and error conditions."""
+
+ @pytest.mark.asyncio
+ async def test_zero_values(self):
+ """Test functions with zero values."""
+ result1 = await add_numbers(0, 0)
+ assert result1 == 0.0
+
+ result2 = await multiply_numbers(0, 5)
+ assert result2 == 0
+
+ @pytest.mark.asyncio
+ async def test_negative_values(self):
+ """Test functions with negative values."""
+ result1 = await add_numbers(-5, 3)
+ assert result1 == -2.0
+
+ result2 = await multiply_numbers(-4, -7)
+ assert result2 == 28
+
+ @pytest.mark.asyncio
+ async def test_large_values(self):
+ """Test functions with large values."""
+ result1 = await add_numbers(1000000, 2000000)
+ assert result1 == 3000000.0
+
+ result2 = await multiply_numbers(1000, 2000)
+ assert result2 == 2000000
diff --git a/flo_ai/tests/test.yaml b/flo_ai/tests/test.yaml
deleted file mode 100644
index c58e8c9f..00000000
--- a/flo_ai/tests/test.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: flo/alpha-v1
-kind: FloAgent
-name: weather-assistant
-agent:
- name: WeatherAssistant
- kind: agentic
- job: >
- Given the city name, you are capable of answering the latest weather this time of the year
- by searching the internet.
- tools:
- - name: adder
diff --git a/flo_ai/tests/test_component_name.py b/flo_ai/tests/test_component_name.py
deleted file mode 100644
index a1deff4c..00000000
--- a/flo_ai/tests/test_component_name.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import pytest
-from flo_ai.yaml.validators import raise_for_name_error
-
-
-@pytest.mark.parametrize(
- 'flo_name, validity',
- [
- ('CorrectName', True),
- ('Wrong Name', False),
- ('correct_name', True),
- ('correct-name', True),
- ('wrong/name', False),
- ],
-)
-def test_flo_component_names(flo_name, validity):
- isException = False
- try:
- raise_for_name_error(flo_name)
- except Exception:
- isException = True
- assert isException != validity
diff --git a/flo_ai/tests/test_flotool.py b/flo_ai/tests/test_flotool.py
deleted file mode 100644
index e814ad9f..00000000
--- a/flo_ai/tests/test_flotool.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import pytest
-import asyncio
-from typing import List
-from flo_ai import Flo
-from flo_ai.error.flo_exception import FloException
-from langchain_openai import ChatOpenAI
-from flo_ai.tools.flo_tool import flotool
-from flo_ai.state.flo_session import FloSession
-from flo_ai.constants.common_constants import DOCUMENTATION_WEBSITE
-
-
-@flotool(name='AdditionTool', description='Tool to add numbers')
-async def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The sum is {result}'
-
-
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
-)
-def mul_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- return f'The product is {result}'
-
-
-def test_flotool_async():
- built_tool = asyncio.iscoroutinefunction(addition_tool.coroutine)
- assert built_tool
-
-
-def test_flotool_sync():
- built_tool = asyncio.iscoroutinefunction(mul_tool.coroutine)
- assert not built_tool
-
-
-@pytest.mark.asyncio
-async def test_flotool_invoke_with_async_tool():
- result = await addition_tool.ainvoke({'numbers': [1, 32, 2]})
- assert 'The sum is 35' == result
-
-
-def test_flotool_invoke_with_sync_tool():
- result = mul_tool.invoke({'numbers': [1, 32, 2]})
- assert 'The product is 35' == result
-
-
-def test_session_registration_and_invoke():
- llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini', api_key='TEST_KEY')
- session = FloSession(llm)
- session.register_tool('adder', addition_tool)
- session.register_tool('muller', mul_tool)
-
- mock_agent_yaml = mock_agent_yaml = """
- apiVersion: flo/alpha-v1
- kind: FloAgent
- name: weather-assistant
- agent:
- name: WeatherAssistant
- kind: agentic
- job: >
- Given the city name you are capable of answering the latest whether this time of the year by searching the internet
- tools:
- - name: adder
- """
-
- flo = Flo.build(session, mock_agent_yaml)
- try:
- flo.invoke('What the whether in berlin')
- except FloException as e:
- assert (
- str(e)
- == f'[Error -1] You seem to have atleast one async tool registered in this session. Please use flo.async_invoke or flo.async_stream. Checkout {DOCUMENTATION_WEBSITE}'
- )
diff --git a/flo_ai/tests/test_json_output_collection.py b/flo_ai/tests/test_json_output_collection.py
deleted file mode 100644
index 11325671..00000000
--- a/flo_ai/tests/test_json_output_collection.py
+++ /dev/null
@@ -1,138 +0,0 @@
-import pytest
-from flo_ai.error.flo_exception import FloException
-from flo_ai.state.flo_output_collector import FloOutputCollector
-from flo_ai.state.flo_json_output_collector import FloJsonOutputCollector
-
-
-class TestFloJsonOutputCollector:
- @pytest.fixture
- def collector(self):
- return FloJsonOutputCollector(strict=False)
-
- @pytest.fixture
- def strict_collector(self):
- return FloJsonOutputCollector(strict=True)
-
- def test_initialization(self, collector):
- assert isinstance(collector, FloOutputCollector)
- assert collector.strict is False
- assert collector.data == []
-
- def test_append_single_json(self, collector):
- test_input = '{"key": "value"}'
- collector.append(test_input)
- assert collector.data == [{'key': 'value'}]
-
- def test_append_multiple_jsons(self, collector):
- test_input = '{"key1": "value1"} Some text {"key2": "value2"}'
- collector.append(test_input)
- assert collector.data == [{'key1': 'value1', 'key2': 'value2'}]
-
- def test_append_nested_json(self, collector):
- test_input = '{"outer": {"inner": "value"}}'
- collector.append(test_input)
- assert collector.data == [{'outer': {'inner': 'value'}}]
-
- def test_strip_comments(self, collector):
- test_input = """
- {
- // Single line comment
- "key1": "value1",
- /* Multi-line
- comment */
- "key2": "value2"
- }
- """
- collector.append(test_input)
- assert collector.data == [{'key1': 'value1', 'key2': 'value2'}]
-
- def test_string_with_comment_chars(self, collector):
- test_input = '{"key": "This // is not a comment", "url": "http://example.com"}'
- collector.append(test_input)
- assert collector.data == [
- {'key': 'This // is not a comment', 'url': 'http://example.com'}
- ]
-
- def test_strict_mode_no_json(self, strict_collector):
- with pytest.raises(FloException) as exc_info:
- strict_collector.append('No JSON here')
- assert exc_info.value.error_code == 1099
-
- def test_strict_mode_with_json(self, strict_collector):
- test_input = '{"key": "value"}'
- strict_collector.append(test_input)
- assert strict_collector.data == [{'key': 'value'}]
-
- def test_pop_operation(self, collector: FloJsonOutputCollector):
- test_input1 = '{"key1": "value1"}'
- test_input2 = '{"key2": "value2"}'
- collector.append(test_input1)
- collector.append(test_input2)
-
- popped = collector.pop()
- assert popped == {'key2': 'value2'}
- assert len(collector.data) == 1
-
- def test_peek_operation(self, collector: FloJsonOutputCollector):
- test_input = '{"key": "value"}'
- collector.append(test_input)
-
- peeked = collector.peek()
- assert peeked == {'key': 'value'}
- assert len(collector.data) == 1
-
- def test_peek_empty_collector(self, collector):
- assert collector.peek() is None
-
- def test_fetch_operation(self, collector: FloJsonOutputCollector):
- test_input1 = '{"key1": "value1"}'
- test_input2 = '{"key2": "value2"}'
- collector.append(test_input1)
- collector.append(test_input2)
-
- result = collector.fetch()
- assert result == {'key1': 'value1', 'key2': 'value2'}
-
- def test_fetch_with_overlapping_keys(self, collector: FloJsonOutputCollector):
- test_input1 = '{"key": "value1"}'
- test_input2 = '{"key": "value2"}'
- collector.append(test_input1)
- collector.append(test_input2)
-
- result = collector.fetch()
- assert result == {'key': 'value2'} # Later values should override earlier ones
-
- def test_complex_nested_structure(self, collector: FloJsonOutputCollector):
- test_input = """
- {
- "array": [1, 2, 3],
- "nested": {
- "deep": {
- "deeper": "value"
- }
- },
- "mixed": [{"key": "value"}, 42, "string"]
- }
- """
- collector.append(test_input)
- expected = {
- 'array': [1, 2, 3],
- 'nested': {'deep': {'deeper': 'value'}},
- 'mixed': [{'key': 'value'}, 42, 'string'],
- }
- assert collector.data == [expected]
-
- @pytest.mark.parametrize(
- 'test_input,expected',
- [
- ('{"a": 1}', [{'a': 1}]),
- ('{"a": 1, "b": 2}', [{'a': 1, 'b': 2}]),
- ('{"a": 1} {"b": 2}', [{'a': 1, 'b': 2}]),
- ('No JSON', [{}]),
- ],
- )
- def test_various_inputs(
- self, collector: FloJsonOutputCollector, test_input, expected
- ):
- collector.append(test_input)
- assert collector.data == expected
diff --git a/flo_ai/tests/test_yaml_file_path.py b/flo_ai/tests/test_yaml_file_path.py
deleted file mode 100644
index 637503ed..00000000
--- a/flo_ai/tests/test_yaml_file_path.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from flo_ai import Flo, FloSession
-from langchain_openai import ChatOpenAI
-from flo_ai.tools.flo_tool import flotool
-from flo_ai.error.flo_exception import FloException
-from typing import List
-import asyncio
-import pytest
-
-
-@pytest.fixture
-def initialize_session():
- llm = ChatOpenAI(temperature=0, model_name='gpt-4o-mini', api_key='TEST_KEY')
- session = FloSession(llm)
- session.register_tool('adder', addition_tool)
- session.register_tool('muller', mul_tool)
- return session
-
-
-@flotool(name='AdditionTool', description='Tool to add numbers')
-async def addition_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- await asyncio.sleep(1)
- return f'The sum is {result}'
-
-
-@flotool(
- name='MultiplicationTool',
- description='Tool to multiply numbers to get product of numbers',
-)
-def mul_tool(numbers: List[int]) -> str:
- result = sum(numbers)
- return f'The product is {result}'
-
-
-def test_valid_path(initialize_session):
- Flo.build(initialize_session, yaml_path='tests/test.yaml')
-
-
-def test_invalid_path(initialize_session):
- try:
- yaml_path = 'test/test.yaml'
- Flo.build(initialize_session, yaml_path=yaml_path)
- except FloException as e:
- assert str(e) == f'[Error -1] YAML file at path {yaml_path} not found.'
-
-
-def test_both_yaml(initialize_session):
- try:
- yaml_path = 'test/test.yaml'
- Flo.build(initialize_session, yaml='', yaml_path=yaml_path)
- except FloException as e:
- assert (
- str(e)
- == '[Error -1] Cannot specify both `yaml` and `yaml_path`. Use only one.'
- )
diff --git a/generate_training_data.py b/generate_training_data.py
deleted file mode 100644
index 8d4760b0..00000000
--- a/generate_training_data.py
+++ /dev/null
@@ -1,157 +0,0 @@
-"""
-Generate Training Data Script
-
-This script processes log files and tool descriptions to generate training datasets.
-It handles both tool-based and chain-based data, transforming them into a proper format.
-
-Usage:
- python generate_training_data.py --logger-path PATH --tool-path PATH [--output PATH]
-
-Arguments:
- --logger-path: Path to the logger file containing tool and chain entries
- --tool-path: Path to the tool descriptions file
- --output: path to save the output
-"""
-
-import json
-import argparse
-
-
-def parse_arguments():
- """Parse command line arguments."""
- parser = argparse.ArgumentParser(
- description='Generate training data from logs and tool descriptions'
- )
- parser.add_argument(
- '--logger-path',
- required=True,
- help='Path to the logger file containing tool and chain entries',
- )
- parser.add_argument(
- '--tool-path', required=True, help='Path to the tool descriptions file'
- )
- parser.add_argument('--output-path', required=True, help='path to save the output')
- return parser.parse_args()
-
-
-def read_file(file_path):
- """
- Read and parse JSON lines from a file.
- Returns:
- List of parsed JSON objects
- """
- try:
- datas = []
- with open(file_path, 'r') as file:
- for line in file:
- datas.append(json.loads(line))
- return datas
- except FileNotFoundError:
- raise FileNotFoundError(f'Could not find file: {file_path}')
- except Exception as e:
- raise Exception(e)
-
-
-def extracting_tool_details(tools, toolbox):
- dataset = []
- for tool in tools:
- query = tool.get('query')
- tool_name = tool.get('tool_name')
- for tool_d in toolbox[tool_name]:
- if tool_d.get('tool_name') == tool_name:
- description = tool_d['description']
- args = tool_d['args']
- tool_input = tool['input']
- dataset.append(
- {
- 'query': query,
- 'tool_name': tool_name,
- 'description': description,
- 'args': args,
- 'tool_input': tool_input,
- }
- )
- return dataset
-
-
-def tool_transformation(input_data):
- transform_data = {}
-
- for idx, data in enumerate(input_data):
- tool_name = data['tool_name']
- description = (data['description'],)
- args = data['args']
- tool_input = json.loads(data['tool_input'].replace("'", '"'))
- for _, value in args.items():
- if 'title' in value:
- del value['title']
- transformed_query = {
- 'id': idx,
- 'answers': json.dumps([{'name': tool_name, 'arguments': tool_input}]),
- 'tools': json.dumps(
- [{'name': tool_name, 'description': description, 'parameters': args}]
- ),
- }
- transform_data[tool_name] = transformed_query
- return transform_data
-
-
-def chain_transformation(chains, start_idx):
- chain_dataset = []
-
- for i, chain in enumerate(chains, start_idx):
- if chain.get('inputs'):
- chain_dataset.append(
- {
- 'query': chain['prompt'][0],
- 'id': i,
- 'answer': chain['outputs']['output'],
- }
- )
- return chain_dataset
-
-
-def llm_transformation(llm_logs, tool_set):
- dataset = []
- for i, llm_log in enumerate(llm_logs):
- if llm_log['inputs'] and 'messages' in llm_log['inputs']:
- tools = None
- answer = llm_log['outputs']
- print()
- if (
- 'type' in llm_log['outputs'][0]
- and llm_log['outputs'][0]['type'] == 'AgentAction'
- ):
- tools = tool_set[llm_log['outputs'][0]['tool']]
- answer = tools['answers']
- dataset.append(
- {
- 'query': llm_log['inputs']['messages'],
- 'id': i,
- 'answers': answer,
- 'tools': tools['tools'] if tools is not None else None,
- }
- )
- return dataset
-
-
-if __name__ == '__main__':
- args = parse_arguments()
-
- logger_data = read_file(args.logger_path)
- tool_descriptions = read_file(args.tool_path)
-
- toolbox = {}
- for td in tool_descriptions:
- toolbox[td[0]['tool_name']] = td
-
- tools = [entry for entry in logger_data if entry['type'] == 'tool']
- llms = [entry for entry in logger_data if entry['type'] == 'llm']
-
- tool_extraction = extracting_tool_details(tools, toolbox)
- tool_transformed = tool_transformation(tool_extraction)
- training_data = llm_transformation(llms, tool_transformed)
-
- with open(args.output_path, 'w') as f:
- for data in training_data:
- f.write(json.dumps(data) + '\n')