diff --git a/README.md b/README.md index c96ddec4..7979e145 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,46 @@ Flo AI Studio is a modern, intuitive visual editor that allows you to design com - πŸ”§ **Flexible**: Use pre-built components or create your own - 🀝 **Team-Oriented**: Create and manage teams of AI agents working together - πŸ”„ **Langchain Compatible**: Works with all your favorite Langchain tools +- πŸ“Š **OpenTelemetry Integration**: Built-in observability with automatic instrumentation for LLM calls, agent execution, and workflows + +## πŸ“Š OpenTelemetry Integration + +Flo AI includes comprehensive OpenTelemetry integration for production observability. Monitor your AI applications with automatic instrumentation for: + +- πŸ” **LLM Calls**: Track token usage, latency, and errors across all providers +- πŸ€– **Agent Execution**: Monitor performance, tool calls, and retry attempts +- πŸ”„ **Workflows**: Track Arium workflow execution and node traversals +- πŸ“Š **Metrics**: Export performance data to Jaeger, Prometheus, Grafana, or cloud providers + +### Quick Telemetry Setup + +```python +from flo_ai import configure_telemetry, shutdown_telemetry + +# Configure at startup +configure_telemetry( + service_name="my_ai_app", + service_version="1.0.0", + console_export=True # For debugging +) + +# Your application code here... + +# Shutdown to flush data +shutdown_telemetry() +``` + +### Production Monitoring + +```python +# Export to OTLP collector (Jaeger, Prometheus, etc.) +configure_telemetry( + service_name="my_ai_app", + otlp_endpoint="http://localhost:4317" +) +``` + +**πŸ“– [Complete Telemetry Guide β†’](flo_ai/flo_ai/telemetry/README.md)** ## πŸ“– Table of Contents @@ -104,6 +144,7 @@ Flo AI Studio is a modern, intuitive visual editor that allows you to design com - [Create Your First AI Agent in 30 seconds](#create-your-first-ai-agent-in-30-seconds) - [Create a Tool-Using Agent](#create-a-tool-using-agent) - [Create an Agent with Structured Output](#create-an-agent-with-structured-output) +- [πŸ“Š OpenTelemetry Integration](#-opentelemetry-integration) - [πŸ“ YAML Configuration](#-yaml-configuration) - [πŸ”§ Variables System](#-variables-system) - [πŸ“„ Document Processing](#-document-processing) diff --git a/flo_ai/README.md b/flo_ai/README.md index 5c8e15e3..7979e145 100644 --- a/flo_ai/README.md +++ b/flo_ai/README.md @@ -1 +1,2599 @@ -Please refer to [Main README.md](../README.md)** \ No newline at end of file +

+ Rootflo +

+ +

Composable Agentic AI Workflow

+ +

+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. +

+ +

+ GitHub stars + + GitHub release (latest) + + GitHub commit activity + + License + + Tests Passing + +
+

+ +

+
+ Checkout the docs Β» +
+
+ Github + β€’ + Website + β€’ + Roadmap +

+ +
+ +# Flo AI 🌊 + +> 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. + +## 🎨 Flo AI Studio - Visual Workflow Designer + +**Create AI workflows visually with our powerful React-based studio!** + +

+ Flo AI Studio - Visual Workflow Designer +

+ +Flo AI Studio is a modern, intuitive visual editor that allows you to design complex multi-agent workflows through a drag-and-drop interface. Build sophisticated AI systems without writing code, then export them as production-ready YAML configurations. + +### πŸš€ Studio Features + +- **🎯 Visual Design**: Drag-and-drop interface for creating agent workflows +- **πŸ€– Agent Management**: Configure AI agents with different roles, models, and tools +- **πŸ”€ Smart Routing**: Visual router configuration for intelligent workflow decisions +- **πŸ“€ YAML Export**: Export workflows as Flo AI-compatible YAML configurations +- **πŸ“₯ YAML Import**: Import existing workflows for further editing +- **βœ… Workflow Validation**: Real-time validation and error checking +- **πŸ”§ Tool Integration**: Connect agents to external tools and APIs +- **πŸ“‹ Template System**: Quick start with pre-built agent and router templates + +### πŸƒβ€β™‚οΈ Quick Start with Studio + +1. **Start the Studio**: + ```bash + cd studio + pnpm install + pnpm dev + ``` + +2. **Design Your Workflow**: + - Add agents, routers, and tools to the canvas + - Configure their properties and connections + - Test with the built-in validation + +3. **Export & Run**: + ```bash + # Export YAML from the studio, then run with Flo AI + python -c " + from flo_ai.arium import AriumBuilder + builder = AriumBuilder.from_yaml(yaml_file='your_workflow.yaml') + result = await builder.build_and_run(['Your input here']) + " + ``` + +## ✨ 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 +- 🧠 **LLM-Powered Routing**: Intelligent routing decisions made by LLMs, no code required +- πŸ”§ **Flexible**: Use pre-built components or create your own +- 🀝 **Team-Oriented**: Create and manage teams of AI agents working together +- πŸ”„ **Langchain Compatible**: Works with all your favorite Langchain tools +- πŸ“Š **OpenTelemetry Integration**: Built-in observability with automatic instrumentation for LLM calls, agent execution, and workflows + +## πŸ“Š OpenTelemetry Integration + +Flo AI includes comprehensive OpenTelemetry integration for production observability. Monitor your AI applications with automatic instrumentation for: + +- πŸ” **LLM Calls**: Track token usage, latency, and errors across all providers +- πŸ€– **Agent Execution**: Monitor performance, tool calls, and retry attempts +- πŸ”„ **Workflows**: Track Arium workflow execution and node traversals +- πŸ“Š **Metrics**: Export performance data to Jaeger, Prometheus, Grafana, or cloud providers + +### Quick Telemetry Setup + +```python +from flo_ai import configure_telemetry, shutdown_telemetry + +# Configure at startup +configure_telemetry( + service_name="my_ai_app", + service_version="1.0.0", + console_export=True # For debugging +) + +# Your application code here... + +# Shutdown to flush data +shutdown_telemetry() +``` + +### Production Monitoring + +```python +# Export to OTLP collector (Jaeger, Prometheus, etc.) +configure_telemetry( + service_name="my_ai_app", + otlp_endpoint="http://localhost:4317" +) +``` + +**πŸ“– [Complete Telemetry Guide β†’](flo_ai/flo_ai/telemetry/README.md)** + +## πŸ“– Table of Contents + +- [πŸš€ Quick Start](#-quick-start) + - [Installation](#installation) + - [Create Your First AI Agent in 30 seconds](#create-your-first-ai-agent-in-30-seconds) + - [Create a Tool-Using Agent](#create-a-tool-using-agent) + - [Create an Agent with Structured Output](#create-an-agent-with-structured-output) +- [πŸ“Š OpenTelemetry Integration](#-opentelemetry-integration) +- [πŸ“ YAML Configuration](#-yaml-configuration) +- [πŸ”§ Variables System](#-variables-system) +- [πŸ“„ Document Processing](#-document-processing) +- [πŸ› οΈ Tools](#️-tools) + - [🎯 @flo_tool Decorator](#-flo_tool-decorator) +- [🧠 Reasoning Patterns](#-reasoning-patterns) +- [πŸ”§ LLM Providers](#-llm-providers) + - [OpenAI](#openai) + - [Anthropic Claude](#anthropic-claude) + - [Google Gemini](#google-gemini) + - [Google VertexAI](#google-vertexai) + - [Ollama (Local)](#ollama-local) + - [Streaming Support in LLM](#streaming-support) +- [πŸ“Š Output Formatting](#-output-formatting) +- [πŸ”„ Error Handling](#-error-handling) +- [πŸ“š Examples](#-examples) +- [πŸš€ Advanced Features](#-advanced-features) + - [Custom Tool Creation](#custom-tool-creation) + - [YAML Parser Integration](#yaml-parser-integration) +- [πŸ”„ Agent Orchestration with Arium](#-agent-orchestration-with-arium) + - [🌟 Key Features](#-key-features) + - [Quick Start: Simple Agent Chain](#quick-start-simple-agent-chain) + - [Advanced: Conditional Routing](#advanced-conditional-routing) + - [Agent + Tool Workflows](#agent--tool-workflows) + - [Workflow Visualization](#workflow-visualization) + - [Memory and Context Sharing](#memory-and-context-sharing) + - [πŸ“Š Use Cases for Arium](#-use-cases-for-arium) + - [Builder Pattern Benefits](#builder-pattern-benefits) + - [πŸ“„ YAML-Based Arium Workflows](#-yaml-based-arium-workflows) + - [🧠 LLM-Powered Routers in YAML (NEW!)](#-llm-powered-routers-in-yaml-new) + - [πŸ”„ ReflectionRouter: Structured Reflection Workflows (NEW!)](#-reflectionrouter-structured-reflection-workflows-new) + - [πŸ”„ PlanExecuteRouter: Cursor-Style Plan-and-Execute Workflows (NEW!)](#-planexecuterouter-cursor-style-plan-and-execute-workflows-new) +- [πŸ“– Documentation](#-documentation) +- [🌟 Why Flo AI?](#-why-flo-ai) +- [🎯 Use Cases](#-use-cases) +- [🀝 Contributing](#-contributing) +- [πŸ“œ License](#-license) +- [πŸ™ Acknowledgments](#-acknowledgments) + +## πŸš€ Quick Start + +### Installation + +```bash +pip install flo-ai +# or using poetry +poetry add flo-ai +``` + +### Create Your First AI Agent in 30 seconds + +```python +import asyncio +from typing import Any +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +async def main() -> None: + # Create a simple conversational agent + agent: Agent = ( + AgentBuilder() + .with_name('Math Tutor') + .with_prompt('You are a helpful math tutor.') + .with_llm(OpenAI(model='gpt-4o-mini')) + .build() + ) + + response: Any = await agent.run('What is the formula for the area of a circle?') + print(f'Response: {response}') + +asyncio.run(main()) +``` + +### Create a Tool-Using Agent + +```python +import asyncio +from typing import Any, Dict, List, Union +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.models.agent import Agent +from flo_ai.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 = 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: 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() +) + +response: Any = await agent.run('Calculate 5 plus 3') +print(f'Response: {response}') +``` + +### Create an Agent with Structured Output + +```python +import asyncio +from typing import Any, Dict +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +# Define output schema for structured responses +math_schema: Dict[str, Any] = { + '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: 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: Any = await agent.run('Solve: 2x + 5 = 15') +print(f'Structured Response: {response}') +``` + +## πŸ“ YAML Configuration + +Define your agents using YAML for easy configuration and deployment: + +```yaml +metadata: + name: email-summary-flo + version: 1.0.0 + description: "Agent for analyzing email threads" +agent: + name: EmailSummaryAgent + 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" +``` + +```python +from typing import Any, List +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.models.agent import Agent + +# Create agent from YAML +yaml_config: str = """...""" # Your YAML configuration string +email_thread: List[str] = ["Email thread content..."] + +builder: AgentBuilder = AgentBuilder.from_yaml(yaml_str=yaml_config) +agent: Agent = builder.build() + +# Use the agent +result: Any = await agent.run(email_thread) +``` + +## πŸ”§ Variables System + +Flo AI supports dynamic variable resolution in agent prompts and inputs using `` syntax. Variables are automatically discovered, validated at runtime, and can be shared across multi-agent workflows. + +### ✨ Key Features + +- **πŸ” Automatic Discovery**: Variables are extracted from system prompts and inputs at runtime +- **βœ… Runtime Validation**: Missing variables are reported with detailed error messages +- **🀝 Multi-Agent Support**: Variables can be shared across agent workflows +- **πŸ›‘οΈ JSON-Safe Syntax**: `` format avoids conflicts with JSON content + +### Basic Usage + +```python +import asyncio +from typing import Any, Dict +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +async def main() -> None: + # Create agent with variables in system prompt + agent: Agent = ( + AgentBuilder() + .with_name('Data Analyst') + .with_prompt('Analyze and focus on . Generate insights for .') + .with_llm(OpenAI(model='gpt-4o-mini')) + .build() + ) + + # Define variables at runtime + variables: Dict[str, str] = { + 'dataset_path': '/data/sales_q4_2024.csv', + 'key_metric': 'revenue growth', + 'target_audience': 'executive team' + } + + # Run agent with variable resolution + result: Any = await agent.run( + 'Please provide a comprehensive analysis with actionable recommendations.', + variables=variables + ) + + print(f'Analysis: {result}') + +asyncio.run(main()) +``` + +### Variables in User Input + +Variables can also be used in the user input messages: + +```python +import asyncio +from typing import Any, Dict +from flo_ai.models.agent import Agent +from flo_ai.llm import OpenAI + +async def input_variables_example() -> None: + agent: Agent = Agent( + name='content_creator', + system_prompt='You are a content creator specializing in .', + llm=OpenAI(model='gpt-4o-mini') + ) + + variables: Dict[str, str] = { + 'content_type': 'technical blog posts', + 'topic': 'machine learning fundamentals', + 'word_count': '1500', + 'target_level': 'intermediate' + } + + # Variables in both system prompt and user input + result: Any = await agent.run( + 'Create a -word article about for readers.', + variables=variables + ) + + print(f'Content: {result}') + +asyncio.run(input_variables_example()) +``` + +### Multi-Agent Variable Sharing + +Variables can be shared and passed between agents in workflows: + +```python +import asyncio +from typing import Any, Dict, List +from flo_ai.arium import AriumBuilder +from flo_ai.models.agent import Agent +from flo_ai.llm import OpenAI + +async def multi_agent_variables() -> List[Any]: + llm: OpenAI = OpenAI(model='gpt-4o-mini') + + # Agent 1: Research phase + researcher: Agent = Agent( + name='researcher', + system_prompt='Research and focus on analysis.', + llm=llm + ) + + # Agent 2: Writing phase + writer: Agent = Agent( + name='writer', + system_prompt='Write a based on the research for .', + llm=llm + ) + + # Agent 3: Review phase + reviewer: Agent = Agent( + name='reviewer', + system_prompt='Review the for and provide feedback.', + llm=llm + ) + + # Shared variables across all agents + shared_variables: Dict[str, str] = { + 'research_topic': 'sustainable energy solutions', + 'research_depth': 'comprehensive', + 'document_type': 'white paper', + 'target_audience': 'policy makers', + 'review_criteria': 'accuracy and policy relevance' + } + + # Run multi-agent workflow with shared variables + result: List[Any] = await ( + AriumBuilder() + .add_agents([researcher, writer, reviewer]) + .start_with(researcher) + .connect(researcher, writer) + .connect(writer, reviewer) + .end_with(reviewer) + .build_and_run( + ['Begin comprehensive research and document creation process'], + variables=shared_variables + ) + ) + + return result + +asyncio.run(multi_agent_variables()) +``` + +### YAML Configuration with Variables + +Variables work seamlessly with YAML-based agent configuration: + +```yaml +metadata: + name: personalized-assistant + version: 1.0.0 + description: "Personalized assistant with variable support" +agent: + name: PersonalizedAssistant + kind: llm + role: assistant specialized in + model: + provider: openai + name: gpt-4o-mini + settings: + temperature: 0.3 + max_retries: 2 + reasoning_pattern: DIRECT + job: > + You are a focused on . + Your expertise includes and you should + tailor responses for users. + Always consider in your recommendations. +``` + +```python +import asyncio +from typing import Any, Dict +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.models.agent import Agent + +async def yaml_with_variables() -> None: + yaml_config: str = """...""" # Your YAML configuration + + # Variables for YAML agent + variables: Dict[str, str] = { + 'user_role': 'data scientist', + 'domain_expertise': 'machine learning and statistical analysis', + 'primary_objective': 'deriving actionable insights from data', + 'experience_level': 'senior', + 'priority_constraints': 'computational efficiency and model interpretability' + } + + # Create agent from YAML with variables + builder: AgentBuilder = AgentBuilder.from_yaml(yaml_str=yaml_config) + agent: Agent = builder.build() + + result: Any = await agent.run( + 'Help me design an ML pipeline for with ', + variables={ + **variables, + 'use_case': 'customer churn prediction', + 'data_constraints': 'limited labeled data' + } + ) + + print(f'ML Pipeline Advice: {result}') + +asyncio.run(yaml_with_variables()) +``` + +### Error Handling and Validation + +The variables system provides comprehensive error reporting for missing or invalid variables: + +```python +import asyncio +from typing import Any, Dict +from flo_ai.models.agent import Agent +from flo_ai.llm import OpenAI + +async def variable_validation_example() -> None: + agent: Agent = Agent( + name='validator_example', + system_prompt='Process and for analysis.', + llm=OpenAI(model='gpt-4o-mini') + ) + + # Incomplete variables (missing 'another_param') + incomplete_variables: Dict[str, str] = { + 'required_param': 'dataset.csv' + # 'another_param' is missing + } + + try: + result: Any = await agent.run( + 'Analyze the data in ', + variables=incomplete_variables # Missing 'another_param' and 'data_source' + ) + except ValueError as e: + print(f'Variable validation error: {e}') + # Error will list all missing variables with their locations + +asyncio.run(variable_validation_example()) +``` + +### Best Practices + +1. **Descriptive Variable Names**: Use clear, descriptive names like `` instead of `` +2. **Consistent Naming**: Use consistent variable names across related agents and workflows +3. **Validation**: Always test your variable resolution before production deployment +4. **Documentation**: Document expected variables in your agent configurations + +The variables system makes Flo AI agents highly reusable and configurable, enabling you to create flexible AI workflows that adapt to different contexts and requirements. + +## πŸ“„ Document Processing + +Flo AI provides powerful document processing capabilities that allow agents to analyze and work with various document formats. The framework supports PDF and TXT documents with an extensible architecture for easy addition of new formats. + +### ✨ Key Features + +- **πŸ“„ Multi-Format Support**: Process PDF and TXT documents seamlessly +- **πŸ”„ Multiple Input Methods**: File paths, bytes data, or base64 encoded content +- **🧠 LLM Integration**: Direct document input to AI agents for analysis +- **⚑ Async Processing**: Efficient document handling with async/await support +- **πŸ”§ Extensible Architecture**: Easy to add support for new document types +- **πŸ“Š Rich Metadata**: Extract page counts, processing methods, and document statistics + +### Basic Document Processing + +```python +import asyncio +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.document import DocumentMessage, DocumentType + +async def basic_document_analysis(): + # Create document message from file path + document = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='path/to/your/document.pdf' + ) + + # Create document analysis agent + agent = ( + AgentBuilder() + .with_name('Document Analyzer') + .with_prompt('Analyze the provided document and extract key insights, themes, and important information.') + .with_llm(OpenAI(model='gpt-4o-mini')) + .build() + ) + + # Process document with agent + result = await agent.run([document]) + print(f'Analysis: {result}') + +asyncio.run(basic_document_analysis()) +``` + +### Multiple Input Methods + +Flo AI supports three ways to provide document content: + +#### 1. File Path (Recommended) +```python +document = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='/path/to/document.pdf' +) +``` + +#### 2. Bytes Data +```python +# Read file as bytes +with open('document.pdf', 'rb') as f: + pdf_bytes = f.read() + +document = DocumentMessage( + document_type=DocumentType.PDF, + document_bytes=pdf_bytes, + mime_type='application/pdf' +) +``` + +#### 3. Base64 Encoded +```python +import base64 + +# Encode file to base64 +with open('document.pdf', 'rb') as f: + pdf_base64 = base64.b64encode(f.read()).decode('utf-8') + +document = DocumentMessage( + document_type=DocumentType.PDF, + document_base64=pdf_base64, + mime_type='application/pdf' +) +``` + +### Document Processing in Workflows + +Documents can be seamlessly integrated into Arium workflows: + +```python +import asyncio +from flo_ai.arium import AriumBuilder +from flo_ai.models.document import DocumentMessage, DocumentType + +async def document_workflow(): + # Create document message + document = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='business_report.pdf' + ) + + # Define workflow YAML + workflow_yaml = """ + metadata: + name: document-analysis-workflow + version: 1.0.0 + description: "Multi-agent document analysis pipeline" + + arium: + agents: + - name: intake_agent + role: "Document Intake Specialist" + job: "Process and assess document content for analysis." + model: + provider: openai + name: gpt-4o-mini + + - name: content_analyzer + role: "Content Analyst" + job: "Analyze document content for themes, insights, and key information." + model: + provider: openai + name: gpt-4o-mini + + - name: summary_generator + role: "Summary Writer" + job: "Create comprehensive summaries of analyzed content." + model: + provider: openai + name: gpt-4o-mini + + workflow: + start: intake_agent + edges: + - from: intake_agent + to: [content_analyzer] + - from: content_analyzer + to: [summary_generator] + end: [summary_generator] + """ + + # Run workflow with document + result = await ( + AriumBuilder() + .from_yaml(yaml_str=workflow_yaml) + .build_and_run([document, 'Analyze this business report and provide insights']) + ) + + return result + +asyncio.run(document_workflow()) +``` + +### Advanced Document Processing + +#### Custom Document Metadata +```python +document = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='report.pdf', + metadata={ + 'source': 'quarterly_reports', + 'department': 'finance', + 'priority': 'high', + 'tags': ['financial', 'q4-2024'] + } +) +``` + +#### Processing Different Document Types +```python +# PDF Document +pdf_doc = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='presentation.pdf' +) + +# Text Document +txt_doc = DocumentMessage( + document_type=DocumentType.TXT, + document_file_path='notes.txt' +) + +# Process both with the same agent +agent = AgentBuilder().with_name('Multi-Format Analyzer').build() + +pdf_result = await agent.run([pdf_doc]) +txt_result = await agent.run([txt_doc]) +``` + +### Document Processing Tools + +Create custom tools for document operations: + +```python +from flo_ai.tool import flo_tool +from flo_ai.models.document import DocumentMessage, DocumentType + +@flo_tool(description="Extract key information from documents") +async def extract_document_info(document_path: str, doc_type: str) -> str: + """Extract key information from a document.""" + document_type = DocumentType.PDF if doc_type.lower() == 'pdf' else DocumentType.TXT + + document = DocumentMessage( + document_type=document_type, + document_file_path=document_path + ) + + # Use document processing agent + agent = AgentBuilder().with_name('Info Extractor').build() + result = await agent.run([document]) + + return result + +# Use in agent +agent = ( + AgentBuilder() + .with_name('Document Processor') + .with_tools([extract_document_info.tool]) + .build() +) +``` + +### Error Handling + +```python +from flo_ai.utils.document_processor import DocumentProcessingError + +try: + document = DocumentMessage( + document_type=DocumentType.PDF, + document_file_path='nonexistent.pdf' + ) + result = await agent.run([document]) +except DocumentProcessingError as e: + print(f'Document processing failed: {e}') +except FileNotFoundError: + print('Document file not found') +``` + +### Supported Document Types + +| Type | Extension | Description | Processing Method | +|------|-----------|-------------|-------------------| +| PDF | `.pdf` | Portable Document Format | PyMuPDF4LLM (LLM-optimized) | +| TXT | `.txt` | Plain text files | UTF-8 with encoding detection | + +### Best Practices + +1. **File Validation**: Always check if files exist before processing +2. **Memory Management**: Use file paths for large documents to avoid memory issues +3. **Error Handling**: Implement proper error handling for document processing failures +4. **Metadata**: Add relevant metadata to help agents understand document context +5. **Format Selection**: Choose the most appropriate input method for your use case + +### Use Cases + +- πŸ“Š **Document Analysis**: Extract insights from reports, papers, and documents +- πŸ“ **Content Summarization**: Create summaries of long documents +- πŸ” **Information Extraction**: Pull specific data from structured documents +- πŸ“‹ **Document Classification**: Categorize documents based on content +- πŸ€– **Multi-Agent Workflows**: Process documents through specialized agent pipelines +- πŸ“ˆ **Business Intelligence**: Analyze business documents for insights and trends + +The document processing system makes Flo AI incredibly powerful for real-world applications that need to work with various document formats, enabling sophisticated AI workflows that can understand and process complex document content. + +## πŸ› οΈ Tools + +Create custom tools easily with async support: + +```python +from typing import List +from flo_ai.tool.base_tool import Tool +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +async def weather_lookup(city: str) -> str: + # Your weather API call here + return f"Weather in {city}: Sunny, 25Β°C" + +weather_tool: 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' + } + } +) + +# Add to your agent +agent: Agent = ( + AgentBuilder() + .with_name('Weather Assistant') + .with_llm(OpenAI(model='gpt-4o-mini')) + .with_tools([weather_tool]) + .build() +) +``` + +### 🎯 @flo_tool Decorator + +The `@flo_tool` decorator automatically converts any Python function into a `Tool` object with minimal boilerplate: + +```python +from typing import Any, Dict, Union +from flo_ai.tool import flo_tool +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +@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) -> Union[float, str]: + """Calculate mathematical operations between two numbers.""" + operations: Dict[str, callable] = { + '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: Union[float, str] = await calculate("add", 5, 3) # Returns 8 + +# Tool object is automatically available +agent: Agent = ( + AgentBuilder() + .with_name('Calculator Agent') + .with_llm(OpenAI(model='gpt-4o-mini')) + .with_tools([calculate.tool]) # Access the tool via .tool attribute + .build() +) +``` + +**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 + +**Simple Usage:** +```python +from flo_ai.tool import flo_tool + +@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 + result: float = 0.0 # Your conversion logic here + return f"{value} {from_unit} = {result} {to_unit}" + +# Tool is automatically available as convert_units.tool +``` + +**With Custom Metadata:** +```python +from typing import Optional +from flo_ai.tool import flo_tool + +@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: Optional[str] = None) -> str: + """Get weather information for a specific city.""" + return f"Weather in {city}: sunny" +``` + +> πŸ“– **For detailed documentation on the `@flo_tool` decorator, see [README_flo_tool.md](TOOLS.md)** + +## 🧠 Reasoning Patterns + +Flo AI supports multiple reasoning patterns: + +- **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 + +```python +from flo_ai.models.base_agent import ReasoningPattern +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +agent: Agent = ( + AgentBuilder() + .with_name('Reasoning Agent') + .with_llm(OpenAI(model='gpt-4o')) + .with_reasoning(ReasoningPattern.COT) # or REACT, DIRECT + .build() +) +``` + +## πŸ”§ LLM Providers + +### OpenAI +```python +from flo_ai.llm import OpenAI + +llm: OpenAI = OpenAI( + model='gpt-4o', + temperature=0.7, + api_key='your-api-key' # or set OPENAI_API_KEY env var +) +``` + +### Anthropic Claude +```python +from flo_ai.llm import Anthropic + +llm: Anthropic = Anthropic( + model='claude-3-5-sonnet-20240620', + temperature=0.7, + api_key='your-api-key' # or set ANTHROPIC_API_KEY env var +) +``` + +### Google Gemini +```python +from flo_ai.llm import Gemini + +llm: Gemini = Gemini( + model='gemini-2.5-flash', # or gemini-2.5-pro + temperature=0.7, + api_key='your-api-key' # or set GOOGLE_API_KEY env var +) +``` + +### Google VertexAI +```python +from flo_ai.llm import VertexAI + +llm: VertexAI = VertexAI( + model='gemini-2.5-flash', # or gemini-2.5-pro + temperature=0.7, + project='your-gcp-project-id', # or set GOOGLE_CLOUD_PROJECT env var + location='us-central1' # or set GOOGLE_CLOUD_LOCATION env var +) +``` + +**Prerequisites for VertexAI:** +- Set up Google Cloud project with Vertex AI API enabled +- Configure authentication: `gcloud auth application-default login` +- Set environment variables: `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION` + +### Ollama (Local) +```python +from flo_ai.llm import Ollama + +llm: Ollama = Ollama( + model='llama2', + base_url='http://localhost:11434' +) +``` + +### Streaming Support in LLM +Streaming helps the llm to generate the output (response) piece-by-piece, or token-by-token, +as it is being computed, instead of waiting until the entire response is complete before sending it to the user + +Steaming Support has been added to all the llm providers. Example of streaming function with Gemini is shown below: +```python +from flo_ai.llm import Gemini + +llm: Gemini = Gemini( + model='gemini-2.5-flash', # or gemini-2.5-pro + temperature=0.7, + api_key='your-api-key' # or set GOOGLE_API_KEY env var +) +messages=[{"role": "user", "content": "Stream a short sentence."}] +chunks: List[str] = [] + async for chunk in llm.stream(messages=messages): + text = chunk.get('content', '') + if text: + chunks.append(text) + if len(''.join(chunks)) >= max_chars: + break + return ''.join(chunks) +``` +## πŸ“Š Output Formatting + +Use Pydantic models or JSON schemas for structured outputs: + +```python +from pydantic import BaseModel, Field +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +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: Agent = ( + AgentBuilder() + .with_name('Math Solver') + .with_llm(OpenAI(model='gpt-4o')) + .with_output_schema(MathSolution) + .build() +) +``` + +## πŸ”„ Error Handling + +Built-in retry mechanisms and error recovery: + +```python +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +agent: Agent = ( + AgentBuilder() + .with_name('Robust Agent') + .with_llm(OpenAI(model='gpt-4o')) + .with_retries(3) # Retry up to 3 times on failure + .build() +) +``` + +## πŸ“š Examples + +Check out the `examples/` directory for comprehensive examples: + +- `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 +- `vertexai_agent_example.py` - Google VertexAI integration examples +- `ollama_agent_example.py` - Local Ollama model examples +- `document_processing_example.py` - Document processing with PDF and TXT files + +## πŸš€ Advanced Features + +### Custom Tool Creation +```python +from typing import Dict, Any +from flo_ai.tool.base_tool import Tool + +async def custom_function(param1: str, param2: int) -> Dict[str, str]: + # Your async logic here + return {"result": f"Processed {param1} with {param2}"} + +custom_tool: 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'} + } +) +``` + +### YAML Parser Integration +```python +from typing import Dict, Any +from flo_ai.formatter.yaml_format_parser import FloYamlParser +from flo_ai.builder.agent_builder import AgentBuilder +from flo_ai.llm import OpenAI +from flo_ai.models.agent import Agent + +# Create parser from YAML definition +yaml_config: Dict[str, Any] = {} # Your YAML configuration dict +parser: FloYamlParser = FloYamlParser.create(yaml_dict=yaml_config) +output_schema: Any = parser.get_format() + +agent: Agent = ( + AgentBuilder() + .with_name('YAML Configured Agent') + .with_llm(OpenAI(model='gpt-4o')) + .with_output_schema(output_schema) + .build() +) +``` + +## πŸ”„ Agent Orchestration with Arium + +Arium is Flo AI's powerful workflow orchestration engine that allows you to create complex multi-agent workflows with ease. Think of it as a conductor for your AI agents, coordinating their interactions and data flow. + +### 🌟 Key Features + +- **πŸ”— Multi-Agent Workflows**: Orchestrate multiple agents working together +- **🎯 Flexible Routing**: Route between agents based on context and conditions +- **🧠 LLM Routers**: Intelligent routing powered by LLMs, define routing logic in YAML +- **πŸ’Ύ Shared Memory**: Agents share conversation history and context +- **πŸ“Š Visual Workflows**: Generate flow diagrams of your agent interactions +- **⚑ Builder Pattern**: Fluent API for easy workflow construction +- **πŸ”„ Reusable Workflows**: Build once, run multiple times with different inputs + +### Quick Start: Simple Agent Chain + +```python +import asyncio +from typing import Any, List +from flo_ai.arium import AriumBuilder +from flo_ai.models.agent import Agent +from flo_ai.llm.openai_llm import OpenAI + +async def simple_chain() -> List[Any]: + llm: OpenAI = OpenAI(model='gpt-4o-mini') + + # Create agents + analyst: Agent = Agent( + name='content_analyst', + system_prompt='Analyze the input and extract key insights.', + llm=llm + ) + + summarizer: Agent = Agent( + name='summarizer', + system_prompt='Create a concise summary based on the analysis.', + llm=llm + ) + + # Build and run workflow + result: List[Any] = await ( + AriumBuilder() + .add_agents([analyst, summarizer]) + .start_with(analyst) + .connect(analyst, summarizer) # analyst β†’ summarizer + .end_with(summarizer) + .build_and_run(["Analyze this complex business report..."]) + ) + + return result + +asyncio.run(simple_chain()) +``` + +### Advanced: Conditional Routing + +```python +import asyncio +from typing import Any, List +from flo_ai.arium import AriumBuilder +from flo_ai.models.agent import Agent +from flo_ai.llm.openai_llm import OpenAI +from flo_ai.arium.memory import BaseMemory + +async def conditional_workflow() -> List[Any]: + llm: OpenAI = OpenAI(model='gpt-4o-mini') + + # Create specialized agents + classifier: Agent = Agent( + name='classifier', + system_prompt='Classify the input as either "technical" or "business".', + llm=llm + ) + + tech_specialist: Agent = Agent( + name='tech_specialist', + system_prompt='Provide technical analysis and solutions.', + llm=llm + ) + + business_specialist: Agent = Agent( + name='business_specialist', + system_prompt='Provide business analysis and recommendations.', + llm=llm + ) + + final_agent: Agent = Agent( + name='final_reviewer', + system_prompt='Provide final review and conclusions.', + llm=llm + ) + + # Define routing logic + def route_by_type(memory: BaseMemory) -> str: + """Route based on classification result""" + messages: List[Any] = memory.get() + last_message: str = str(messages[-1]) if messages else "" + + if "technical" in last_message.lower(): + return "tech_specialist" + else: + return "business_specialist" + + # Build workflow with conditional routing + result: List[Any] = await ( + AriumBuilder() + .add_agents([classifier, tech_specialist, business_specialist, final_agent]) + .start_with(classifier) + .add_edge(classifier, [tech_specialist, business_specialist], route_by_type) + .connect(tech_specialist, final_agent) + .connect(business_specialist, final_agent) + .end_with(final_agent) + .build_and_run(["How can we optimize our database performance?"]) + ) + + return result +``` + +### Agent + Tool Workflows + +```python +import asyncio +from typing import Any, List +from flo_ai.tool import flo_tool +from flo_ai.arium import AriumBuilder +from flo_ai.models.agent import Agent +from flo_ai.llm.openai_llm import OpenAI + +@flo_tool(description="Search for relevant information") +async def search_tool(query: str) -> str: + # Your search implementation + return f"Search results for: {query}" + +@flo_tool(description="Perform calculations") +async def calculator(expression: str) -> float: + # Your calculation implementation + return eval(expression) # Note: Use safely in production + +async def agent_tool_workflow() -> List[Any]: + llm: OpenAI = OpenAI(model='gpt-4o-mini') + + research_agent: Agent = Agent( + name='researcher', + system_prompt='Research topics and gather information.', + llm=llm + ) + + analyst_agent: Agent = Agent( + name='analyst', + system_prompt='Analyze data and provide insights.', + llm=llm + ) + + # Mix agents and tools in workflow + result: List[Any] = await ( + AriumBuilder() + .add_agent(research_agent) + .add_tools([search_tool.tool, calculator.tool]) + .add_agent(analyst_agent) + .start_with(research_agent) + .connect(research_agent, search_tool.tool) + .connect(search_tool.tool, calculator.tool) + .connect(calculator.tool, analyst_agent) + .end_with(analyst_agent) + .build_and_run(["Research market trends for Q4 2024"]) + ) + + return result +``` + +### Workflow Visualization + +```python +from typing import Any, List, Callable, Optional +from flo_ai.arium import AriumBuilder +from flo_ai.arium.arium import Arium +from flo_ai.models.agent import Agent +from flo_ai.tool.base_tool import Tool + +# Assume these are defined elsewhere +agent1: Agent = ... # Your agent definitions +agent2: Agent = ... +agent3: Agent = ... +tool1: Tool = ... # Your tool definitions +tool2: Tool = ... +router_function: Callable = ... # Your router function + +# Build workflow and generate visual diagram +arium: Arium = ( + AriumBuilder() + .add_agents([agent1, agent2, agent3]) + .add_tools([tool1, tool2]) + .start_with(agent1) + .connect(agent1, tool1) + .add_edge(tool1, [agent2, agent3], router_function) + .end_with(agent2) + .end_with(agent3) + .visualize("my_workflow.png", "Customer Service Workflow") # Generates PNG + .build() +) + +# Run the workflow +result: List[Any] = await arium.run(["Customer complaint about billing"]) +``` + +### Memory and Context Sharing + +All agents in an Arium workflow share the same memory, enabling them to build on each other's work: + +```python +from typing import Any, List +from flo_ai.arium import AriumBuilder +from flo_ai.arium.memory import MessageMemory +from flo_ai.arium.arium import Arium +from flo_ai.models.agent import Agent + +# Assume these agents are defined elsewhere +agent1: Agent = ... +agent2: Agent = ... +agent3: Agent = ... + +# Custom memory for persistent context +custom_memory: MessageMemory = MessageMemory() + +result: List[Any] = await ( + AriumBuilder() + .with_memory(custom_memory) # Shared across all agents + .add_agents([agent1, agent2, agent3]) + .start_with(agent1) + .connect(agent1, agent2) + .connect(agent2, agent3) + .end_with(agent3) + .build_and_run(["Initial context and instructions"]) +) + +# Build the arium for reuse +arium: Arium = ( + AriumBuilder() + .with_memory(custom_memory) + .add_agents([agent1, agent2, agent3]) + .start_with(agent1) + .connect(agent1, agent2) + .connect(agent2, agent3) + .end_with(agent3) + .build() +) + +# Memory persists and can be reused +result2: List[Any] = await arium.run(["Follow-up question based on previous context"]) +``` + +### πŸ“Š Use Cases for Arium + +- **πŸ“ Content Pipeline**: Research β†’ Writing β†’ Editing β†’ Publishing +- **πŸ” Analysis Workflows**: Data Collection β†’ Processing β†’ Analysis β†’ Reporting +- **🎯 Decision Trees**: Classification β†’ Specialized Processing β†’ Final Decision +- **🀝 Customer Service**: Intent Detection β†’ Specialist Routing β†’ Resolution +- **πŸ§ͺ Research Workflows**: Question Generation β†’ Investigation β†’ Synthesis β†’ Validation +- **πŸ“‹ Document Processing**: Extraction β†’ Validation β†’ Transformation β†’ Storage + +### Builder Pattern Benefits + +The AriumBuilder provides a fluent, intuitive API: + +```python +from typing import Any, List +from flo_ai.arium import AriumBuilder +from flo_ai.arium.arium import Arium +from flo_ai.models.agent import Agent +from flo_ai.tool.base_tool import Tool + +# Assume these are defined elsewhere +agent1: Agent = ... +agent2: Agent = ... +tool1: Tool = ... +inputs: List[str] = ["Your input messages"] + +# All builder methods return self for chaining +workflow: Arium = ( + AriumBuilder() + .add_agent(agent1) # Add components + .add_tool(tool1) + .start_with(agent1) # Define flow + .connect(agent1, tool1) + .end_with(tool1) + .build() # Create Arium instance +) + +# Or build and run in one step +result: List[Any] = await ( + AriumBuilder() + .add_agents([agent1, agent2]) + .start_with(agent1) + .connect(agent1, agent2) + .end_with(agent2) + .build_and_run(inputs) # Build + run together +) +``` + +**Validation Built-in**: The builder automatically validates your workflow: +- βœ… Ensures at least one agent/tool +- βœ… Requires start and end nodes +- βœ… Validates routing functions +- βœ… Checks for unreachable nodes + +### πŸ“„ YAML-Based Arium Workflows + +One of Flo AI's most powerful features is the ability to define entire multi-agent workflows using YAML configuration. This approach makes workflows reproducible, versionable, and easy to modify without changing code. + +#### Simple YAML Workflow + +```yaml +metadata: + name: "content-analysis-workflow" + version: "1.0.0" + description: "Multi-agent content analysis and summarization pipeline" + +arium: + # Define agents inline + agents: + - name: "analyzer" + role: "Content Analyst" + job: "Analyze the input content and extract key insights, themes, and important information." + model: + provider: "openai" + name: "gpt-4o-mini" + settings: + temperature: 0.2 + max_retries: 3 + reasoning_pattern: "COT" + + - name: "summarizer" + role: "Content Summarizer" + job: "Create a concise, actionable summary based on the analysis provided." + model: + provider: "anthropic" + name: "claude-3-5-sonnet-20240620" + settings: + temperature: 0.1 + reasoning_pattern: "DIRECT" + + # Define the workflow + workflow: + start: "analyzer" + edges: + - from: "analyzer" + to: ["summarizer"] + end: ["summarizer"] +``` + +```python +import asyncio +from typing import Any, List +from flo_ai.arium import AriumBuilder + +async def run_yaml_workflow() -> List[Any]: + yaml_config = """...""" # Your YAML configuration + + # Create workflow from YAML + result: List[Any] = await ( + AriumBuilder() + .from_yaml(yaml_config) + .build_and_run(["Analyze this quarterly business report..."]) + ) + + return result + +asyncio.run(run_yaml_workflow()) +``` + +#### Advanced YAML Workflow with Tools and Routing + +```yaml +metadata: + name: "research-workflow" + version: "2.0.0" + description: "Intelligent research workflow with conditional routing" + +arium: + # Define agents with tool references + agents: + - name: "classifier" + role: "Content Classifier" + job: "Classify input as 'research', 'calculation', or 'analysis' task." + model: + provider: "openai" + name: "gpt-4o-mini" + tools: ["web_search"] # Reference tools provided in Python + + - name: "researcher" + role: "Research Specialist" + job: "Conduct thorough research on with analysis." + model: + provider: "anthropic" + name: "claude-3-5-sonnet-20240620" + tools: ["web_search"] + settings: + temperature: 0.3 + reasoning_pattern: "REACT" + + - name: "analyst" + role: "Data Analyst" + job: "Analyze numerical data and provide insights for ." + model: + provider: "openai" + name: "gpt-4o" + tools: ["calculator", "web_search"] + settings: + reasoning_pattern: "COT" + + - name: "synthesizer" + role: "Information Synthesizer" + job: "Combine research and analysis into final recommendations." + model: + provider: "gemini" + name: "gemini-2.5-flash" + + # Complex workflow with conditional routing + workflow: + start: "classifier" + edges: + # Conditional routing based on classification + - from: "classifier" + to: ["researcher", "analyst"] + router: "classification_router" # Router function provided in Python + + # Both specialists feed into synthesizer + - from: "researcher" + to: ["synthesizer"] + + - from: "analyst" + to: ["synthesizer"] + + end: ["synthesizer"] +``` + +```python +import asyncio +from typing import Any, Dict, List, Literal +from flo_ai.arium import AriumBuilder +from flo_ai.tool.base_tool import Tool +from flo_ai.arium.memory import BaseMemory + +# Define tools in Python (cannot be defined in YAML) +async def web_search(query: str) -> str: + # Your search implementation + return f"Search results for: {query}" + +async def calculate(expression: str) -> str: + # Your calculation implementation + try: + result = eval(expression) # Note: Use safely in production + return f"Calculation result: {result}" + except: + return "Invalid expression" + +# Create tool objects +tools: Dict[str, Tool] = { + "web_search": Tool( + name="web_search", + description="Search the web for current information", + function=web_search, + parameters={ + "query": { + "type": "string", + "description": "Search query" + } + } + ), + "calculator": Tool( + name="calculator", + description="Perform mathematical calculations", + function=calculate, + parameters={ + "expression": { + "type": "string", + "description": "Mathematical expression to calculate" + } + } + ) +} + +# Define router functions in Python (cannot be defined in YAML) +def classification_router(memory: BaseMemory) -> Literal["researcher", "analyst"]: + """Route based on task classification""" + content = str(memory.get()[-1]).lower() + if 'research' in content or 'investigate' in content: + return 'researcher' + elif 'calculate' in content or 'analyze data' in content: + return 'analyst' + return 'researcher' # default + +routers: Dict[str, callable] = { + "classification_router": classification_router +} + +async def run_workflow() -> List[Any]: + yaml_config = """...""" # Your YAML configuration from above + + # Create workflow with tools and routers provided as Python objects + result: List[Any] = await ( + AriumBuilder() + .from_yaml( + yaml_str=yaml_config, + tools=tools, # Tools must be provided as Python objects + routers=routers # Routers must be provided as Python functions + ) + .build_and_run(["Research the latest trends in renewable energy"]) + ) + + return result +``` + +#### 🧠 LLM-Powered Routers in YAML (NEW!) + +One of the most powerful new features is the ability to define **intelligent LLM routers directly in YAML**. No more writing router functions - just describe your routing logic and let the LLM handle the decisions! + +```yaml +metadata: + name: "intelligent-content-workflow" + version: "1.0.0" + description: "Content creation with intelligent LLM-based routing" + +arium: + agents: + - name: "content_creator" + role: "Content Creator" + job: "Create initial content based on the request" + model: + provider: "openai" + name: "gpt-4o-mini" + + - name: "technical_writer" + role: "Technical Writer" + job: "Refine content for technical accuracy and clarity" + model: + provider: "openai" + name: "gpt-4o-mini" + + - name: "creative_writer" + role: "Creative Writer" + job: "Enhance content with creativity and storytelling" + model: + provider: "openai" + name: "gpt-4o-mini" + + - name: "marketing_writer" + role: "Marketing Writer" + job: "Optimize content for engagement and conversion" + model: + provider: "openai" + name: "gpt-4o-mini" + + # ✨ LLM Router definitions - No code required! + routers: + - name: "content_type_router" + type: "smart" # Uses LLM to make intelligent routing decisions + routing_options: + technical_writer: "Technical content, documentation, tutorials, how-to guides" + creative_writer: "Creative writing, storytelling, fiction, brand narratives" + marketing_writer: "Marketing copy, sales content, landing pages, ad campaigns" + model: + provider: "openai" + name: "gpt-4o-mini" + settings: + temperature: 0.3 + fallback_strategy: "first" + + - name: "task_classifier" + type: "task_classifier" # Keyword-based classification + task_categories: + math_solver: + description: "Mathematical calculations and problem solving" + keywords: ["calculate", "solve", "equation", "math", "formula"] + examples: ["Calculate 2+2", "Solve x^2 + 5x + 6 = 0"] + code_helper: + description: "Programming and code assistance" + keywords: ["code", "program", "debug", "function", "algorithm"] + examples: ["Write a Python function", "Debug this code"] + model: + provider: "openai" + name: "gpt-4o-mini" + + workflow: + start: "content_creator" + edges: + - from: "content_creator" + to: ["technical_writer", "creative_writer", "marketing_writer"] + router: "content_type_router" # LLM automatically routes based on content type! + end: ["technical_writer", "creative_writer", "marketing_writer"] +``` + +**🎯 LLM Router Types:** + +1. **Smart Router** (`type: smart`): General-purpose routing based on content analysis +2. **Task Classifier** (`type: task_classifier`): Routes based on keywords and examples +3. **Conversation Analysis** (`type: conversation_analysis`): Context-aware routing +4. **Reflection Router** (`type: reflection`): Structured Aβ†’Bβ†’Aβ†’C patterns for reflection workflows +5. **PlanExecute Router** (`type: plan_execute`): Cursor-style plan-and-execute workflows with step tracking + +**✨ Key Benefits:** +- 🚫 **No Code Required**: Define routing logic purely in YAML +- 🎯 **Intelligent Decisions**: LLMs understand context and make smart routing choices +- πŸ“ **Easy Configuration**: Simple, declarative syntax +- πŸ”„ **Version Control**: Track routing changes in YAML files +- πŸŽ›οΈ **Model Flexibility**: Each router can use different LLM models + +```python +# Using LLM routers is incredibly simple! +async def run_intelligent_workflow(): + # No routers dictionary needed - they're defined in YAML! + result = await ( + AriumBuilder() + .from_yaml(yaml_str=intelligent_workflow_yaml) + .build_and_run(["Write a technical tutorial on Docker containers"]) + ) + # The LLM will automatically route to technical_writer! ✨ + return result +``` + +##### πŸ”„ ReflectionRouter: Structured Reflection Workflows (NEW!) + +The **ReflectionRouter** is designed specifically for reflection-based workflows that follow Aβ†’Bβ†’Aβ†’C patterns, commonly used for mainβ†’criticβ†’mainβ†’final agent sequences. This pattern is perfect for iterative improvement workflows where a critic agent provides feedback before final processing. + +**πŸ“‹ Key Features:** +- 🎯 **Pattern Tracking**: Automatically tracks progress through defined reflection sequences +- πŸ”„ **Self-Reference Support**: Allows routing back to the same agent (Aβ†’Bβ†’A patterns) +- πŸ“Š **Visual Progress**: Shows current position with β—‹ pending, βœ“ completed indicators +- πŸ›‘οΈ **Loop Prevention**: Built-in safety mechanisms to prevent infinite loops +- πŸŽ›οΈ **Flexible Patterns**: Supports both 2-agent (Aβ†’Bβ†’A) and 3-agent (Aβ†’Bβ†’Aβ†’C) flows + +**🎯 Supported Patterns:** + +1. **A β†’ B β†’ A** (2 agents): Main β†’ Critic β†’ Main β†’ End +2. **A β†’ B β†’ A β†’ C** (3 agents): Main β†’ Critic β†’ Main β†’ Final + +```yaml +# Simple A β†’ B β†’ A reflection pattern +metadata: + name: "content-reflection-workflow" + version: "1.0.0" + description: "Content creation with critic feedback loop" + +arium: + agents: + - name: "writer" + role: "Content Writer" + job: "Create and improve content based on feedback from critics." + model: + provider: "openai" + name: "gpt-4o-mini" + settings: + temperature: 0.7 + + - name: "critic" + role: "Content Critic" + job: "Review content and provide constructive feedback for improvement." + model: + provider: "openai" + name: "gpt-4o-mini" + settings: + temperature: 0.3 + + # ✨ ReflectionRouter definition + routers: + - name: "reflection_router" + type: "reflection" # Specialized for reflection patterns + flow_pattern: [writer, critic, writer] # A β†’ B β†’ A pattern + model: + provider: "openai" + name: "gpt-4o-mini" + settings: + temperature: 0.2 + allow_early_exit: false # Strict adherence to pattern + + workflow: + start: "writer" + edges: + - from: "writer" + to: [critic, writer] # Can go to critic or self-reference + router: "reflection_router" + - from: "critic" + to: [writer] # Always returns to writer + router: "reflection_router" + end: [writer] # Writer produces final output +``` + +```yaml +# Advanced A β†’ B β†’ A β†’ C reflection pattern +metadata: + name: "advanced-reflection-workflow" + version: "1.0.0" + description: "Full reflection cycle with dedicated final agent" + +arium: + agents: + - name: "researcher" + role: "Research Agent" + job: "Conduct research and gather information on topics." + model: + provider: "openai" + name: "gpt-4o-mini" + + - name: "reviewer" + role: "Research Reviewer" + job: "Review research quality and suggest improvements." + model: + provider: "anthropic" + name: "claude-3-5-sonnet-20240620" + + - name: "synthesizer" + role: "Information Synthesizer" + job: "Create final synthesis and conclusions from research." + model: + provider: "openai" + name: "gpt-4o" + + routers: + - name: "research_reflection_router" + type: "reflection" + flow_pattern: [researcher, reviewer, researcher, synthesizer] # A β†’ B β†’ A β†’ C + settings: + allow_early_exit: true # Allow smart early completion + + workflow: + start: "researcher" + edges: + - from: "researcher" + to: [reviewer, researcher, synthesizer] # All possible destinations + router: "research_reflection_router" + - from: "reviewer" + to: [researcher, reviewer, synthesizer] + router: "research_reflection_router" + - from: "synthesizer" + to: [end] + end: [synthesizer] +``` + +**πŸ”§ ReflectionRouter Configuration Options:** + +```yaml +routers: + - name: "my_reflection_router" + type: "reflection" + flow_pattern: [main_agent, critic, main_agent, final_agent] # Define your pattern + model: # Optional: LLM for routing decisions + provider: "openai" + name: "gpt-4o-mini" + settings: # Optional settings + temperature: 0.2 # Router temperature (lower = more deterministic) + allow_early_exit: false # Allow early completion if LLM determines pattern is done + fallback_strategy: "first" # first, last, random - fallback when LLM fails +``` + +**πŸ—οΈ Programmatic Usage:** + +```python +import asyncio +from flo_ai.arium import AriumBuilder +from flo_ai.models.agent import Agent +from flo_ai.llm import OpenAI +from flo_ai.arium.llm_router import create_main_critic_reflection_router + +async def reflection_workflow_example(): + llm = OpenAI(model='gpt-4o-mini', api_key='your-api-key') + + # Create agents + main_agent = Agent( + name='main_agent', + system_prompt='Create solutions and improve them based on feedback.', + llm=llm + ) + + critic = Agent( + name='critic', + system_prompt='Provide constructive feedback for improvement.', + llm=llm + ) + + final_agent = Agent( + name='final_agent', + system_prompt='Polish and finalize the work.', + llm=llm + ) + + # Create reflection router - A β†’ B β†’ A β†’ C pattern + reflection_router = create_main_critic_reflection_router( + main_agent='main_agent', + critic_agent='critic', + final_agent='final_agent', + allow_early_exit=False, # Strict pattern adherence + llm=llm + ) + + # Build workflow + result = await ( + AriumBuilder() + .add_agents([main_agent, critic, final_agent]) + .start_with(main_agent) + .add_edge(main_agent, [critic, final_agent], reflection_router) + .add_edge(critic, [main_agent, final_agent], reflection_router) + .end_with(final_agent) + .build_and_run(["Create a comprehensive project proposal"]) + ) + + return result + +# Alternative: Direct factory usage +from flo_ai.arium.llm_router import create_llm_router + +reflection_router = create_llm_router( + 'reflection', + flow_pattern=['writer', 'editor', 'writer'], # A β†’ B β†’ A + allow_early_exit=False, + llm=llm +) +``` + +**πŸ’‘ ReflectionRouter Intelligence:** + +The ReflectionRouter automatically: +- **Tracks Progress**: Knows which step in the pattern should execute next +- **Prevents Loops**: Uses execution context to avoid infinite cycles +- **Provides Guidance**: Shows LLM the suggested next step and current progress +- **Handles Self-Reference**: Properly validates flows that return to the same agent +- **Visual Feedback**: Displays pattern progress: `β—‹ writer β†’ βœ“ critic β†’ β—‹ writer` + +**🎯 Perfect Use Cases:** +- πŸ“ **Content Creation**: Writer β†’ Editor β†’ Writer β†’ Publisher +- πŸ”¬ **Research Workflows**: Researcher β†’ Reviewer β†’ Researcher β†’ Synthesizer +- πŸ’Ό **Business Analysis**: Analyst β†’ Critic β†’ Analyst β†’ Decision Maker +- 🎨 **Creative Processes**: Creator β†’ Critic β†’ Creator β†’ Finalizer +- πŸ§ͺ **Iterative Refinement**: Any process requiring feedback and improvement cycles + +**⚑ Quick Start Example:** + +```python +# Minimal A β†’ B β†’ A pattern +yaml_config = """ +arium: + agents: + - name: main_agent + job: "Main work agent" + model: {provider: openai, name: gpt-4o-mini} + - name: critic + job: "Feedback agent" + model: {provider: openai, name: gpt-4o-mini} + + routers: + - name: reflection_router + type: reflection + flow_pattern: [main_agent, critic, main_agent] + + workflow: + start: main_agent + edges: + - from: main_agent + to: [critic, main_agent] + router: reflection_router + - from: critic + to: [main_agent] + router: reflection_router + end: [main_agent] +""" + +result = await AriumBuilder().from_yaml(yaml_str=yaml_config).build_and_run(["Your task"]) +``` + +The ReflectionRouter makes implementing sophisticated feedback loops and iterative improvement workflows incredibly simple, whether you need a 2-agent or 3-agent pattern! πŸš€ + +##### πŸ”„ PlanExecuteRouter: Cursor-Style Plan-and-Execute Workflows (NEW!) + +The **PlanExecuteRouter** implements sophisticated plan-and-execute patterns similar to how Cursor works. It automatically breaks down complex tasks into detailed execution plans and coordinates step-by-step execution with intelligent progress tracking. + +**πŸ“‹ Key Features:** +- 🎯 **Automatic Task Breakdown**: Creates detailed execution plans from high-level tasks +- πŸ“Š **Step Tracking**: Real-time progress monitoring with visual indicators (β—‹ ⏳ βœ… ❌) +- πŸ”„ **Phase Coordination**: Intelligent routing between planning, execution, and review phases +- πŸ›‘οΈ **Dependency Management**: Handles step dependencies and execution order automatically +- πŸ’Ύ **Plan Persistence**: Uses PlanAwareMemory for stateful plan storage and updates +- πŸ”§ **Error Recovery**: Built-in retry logic for failed steps + +**🎯 Perfect for Cursor-Style Workflows:** +- πŸ’» **Software Development**: Requirements β†’ Design β†’ Implementation β†’ Testing β†’ Review +- πŸ“ **Content Creation**: Planning β†’ Writing β†’ Editing β†’ Review β†’ Publishing +- πŸ”¬ **Research Projects**: Plan β†’ Investigate β†’ Analyze β†’ Synthesize β†’ Report +- πŸ“Š **Business Processes**: Any multi-step workflow with dependencies + +**πŸ“„ YAML Configuration:** + +```yaml +# Complete Plan-Execute Workflow +metadata: + name: "development-plan-execute" + version: "1.0.0" + description: "Cursor-style development workflow" + +arium: + agents: + - name: planner + role: Project Planner + job: > + Break down complex development tasks into detailed, sequential execution plans. + Create clear steps with dependencies and agent assignments. + model: + provider: openai + name: gpt-4o-mini + settings: + temperature: 0.3 + + - name: developer + role: Software Developer + job: > + Implement features step by step according to execution plans. + Provide detailed implementation and update step status. + model: + provider: openai + name: gpt-4o-mini + settings: + temperature: 0.5 + + - name: tester + role: QA Engineer + job: > + Test implementations thoroughly and validate functionality. + Create comprehensive test scenarios and report results. + model: + provider: openai + name: gpt-4o-mini + settings: + temperature: 0.2 + + - name: reviewer + role: Senior Reviewer + job: > + Provide final quality assessment and approval. + Review completed work for best practices and requirements. + model: + provider: openai + name: gpt-4o-mini + + # PlanExecuteRouter configuration + routers: + - name: dev_plan_router + type: plan_execute # Router type for plan-execute workflows + agents: # Available agents and their capabilities + planner: "Creates detailed execution plans by breaking down tasks" + developer: "Implements features and code according to plan specifications" + tester: "Tests implementations and validates functionality" + reviewer: "Reviews and approves completed work" + model: # Optional: LLM for routing decisions + provider: openai + name: gpt-4o-mini + settings: # Optional configuration + temperature: 0.2 # Router decision temperature + planner_agent: planner # Agent responsible for creating plans + executor_agent: developer # Default agent for executing steps + reviewer_agent: reviewer # Optional agent for final review + max_retries: 3 # Maximum retries for failed steps + + workflow: + start: planner + edges: + # All agents can route to all others based on plan state + - from: planner + to: [developer, tester, reviewer, planner] + router: dev_plan_router + - from: developer + to: [developer, tester, reviewer, planner] + router: dev_plan_router + - from: tester + to: [developer, tester, reviewer, planner] + router: dev_plan_router + - from: reviewer + to: [end] + end: [reviewer] +``` + +**πŸ—οΈ Programmatic Usage:** + +```python +import asyncio +from flo_ai.arium import AriumBuilder +from flo_ai.arium.memory import PlanAwareMemory +from flo_ai.models.agent import Agent +from flo_ai.llm import OpenAI +from flo_ai.arium.llm_router import create_plan_execute_router + +async def cursor_style_workflow(): + llm = OpenAI(model='gpt-4o-mini', api_key='your-api-key') + + # Create specialized agents + planner = Agent( + name='planner', + system_prompt='Create detailed execution plans by breaking down tasks into sequential steps.', + llm=llm + ) + + developer = Agent( + name='developer', + system_prompt='Implement features step by step according to execution plans.', + llm=llm + ) + + tester = Agent( + name='tester', + system_prompt='Test implementations and validate functionality thoroughly.', + llm=llm + ) + + reviewer = Agent( + name='reviewer', + system_prompt='Review completed work and provide final approval.', + llm=llm + ) + + # Create plan-execute router + plan_router = create_plan_execute_router( + planner_agent='planner', + executor_agent='developer', + reviewer_agent='reviewer', + additional_agents={'tester': 'Tests implementations and validates quality'}, + llm=llm + ) + + # Use PlanAwareMemory for plan state persistence + memory = PlanAwareMemory() + + # Build and run workflow + result = await ( + AriumBuilder() + .with_memory(memory) + .add_agents([planner, developer, tester, reviewer]) + .start_with(planner) + .add_edge(planner, [developer, tester, reviewer, planner], plan_router) + .add_edge(developer, [developer, tester, reviewer, planner], plan_router) + .add_edge(tester, [developer, tester, reviewer, planner], plan_router) + .add_edge(reviewer, [developer, tester, reviewer, planner], plan_router) + .end_with(reviewer) + .build_and_run(["Create a REST API for user authentication with JWT tokens"]) + ) + + return result + +# Alternative: Factory function +from flo_ai.arium.llm_router import create_plan_execute_router + +plan_router = create_plan_execute_router( + planner_agent='planner', + executor_agent='developer', + reviewer_agent='reviewer', + llm=llm +) +``` + +**πŸ’‘ How PlanExecuteRouter Works:** + +The router intelligently coordinates workflow phases: + +1. **Planning Phase**: + - Detects when no execution plan exists + - Routes to planner agent to create detailed plan + - Plan stored as ExecutionPlan object in PlanAwareMemory + +2. **Execution Phase**: + - Analyzes plan state and step dependencies + - Routes to appropriate agents for next ready steps + - Updates step status (pending β†’ in-progress β†’ completed) + - Handles parallel execution of independent steps + +3. **Review Phase**: + - Detects when all steps are completed + - Routes to reviewer agent for final validation + - Manages error recovery for failed steps + +**πŸ“Š Plan Progress Visualization:** + +``` +πŸ“‹ EXECUTION PLAN: User Authentication API +πŸ“Š CURRENT PROGRESS: +βœ… design_schema: Design user database schema β†’ developer +βœ… implement_registration: Create registration endpoint β†’ developer +⏳ implement_login: Add login with JWT β†’ developer (depends: design_schema, implement_registration) +β—‹ add_middleware: Authentication middleware β†’ developer (depends: implement_login) +β—‹ write_tests: Comprehensive testing β†’ tester (depends: add_middleware) +β—‹ final_review: Security and code review β†’ reviewer (depends: write_tests) + +🎯 NEXT ACTION: Execute step 'implement_login' +🎯 SUGGESTED AGENT: developer +``` + +**πŸ”§ Advanced Configuration Options:** + +```yaml +routers: + - name: advanced_plan_router + type: plan_execute + agents: + planner: "Creates execution plans" + frontend_dev: "Frontend implementation" + backend_dev: "Backend implementation" + devops: "Deployment and infrastructure" + qa_tester: "Quality assurance testing" + security_reviewer: "Security review" + product_owner: "Product validation" + model: + provider: openai + name: gpt-4o + settings: + temperature: 0.1 # Lower for more deterministic routing + planner_agent: planner # Plan creation agent + executor_agent: backend_dev # Default execution agent + reviewer_agent: product_owner # Final review agent + max_retries: 5 # Retry attempts for failed steps + allow_parallel_execution: true # Enable parallel step execution + plan_validation: strict # Validate plan completeness +``` + +**⚑ Quick Start Example:** + +```python +# Minimal plan-execute workflow +yaml_config = """ +arium: + agents: + - name: planner + job: "Create execution plans" + model: {provider: openai, name: gpt-4o-mini} + - name: executor + job: "Execute plan steps" + model: {provider: openai, name: gpt-4o-mini} + - name: reviewer + job: "Review final results" + model: {provider: openai, name: gpt-4o-mini} + + routers: + - name: simple_plan_router + type: plan_execute + agents: + planner: "Creates plans" + executor: "Executes steps" + reviewer: "Reviews results" + settings: + planner_agent: planner + executor_agent: executor + reviewer_agent: reviewer + + workflow: + start: planner + edges: + - from: planner + to: [executor, reviewer, planner] + router: simple_plan_router + - from: executor + to: [executor, reviewer, planner] + router: simple_plan_router + - from: reviewer + to: [end] + end: [reviewer] +""" + +result = await AriumBuilder().from_yaml(yaml_str=yaml_config).build_and_run(["Your complex task"]) +``` + +**🎯 Use Cases and Examples:** + +- πŸ“± **App Development**: "Build a todo app with React and Node.js" +- πŸ›’ **E-commerce**: "Create a shopping cart system with payment processing" +- πŸ“Š **Data Pipeline**: "Build ETL pipeline for customer analytics" +- πŸ” **Security**: "Implement OAuth2 authentication system" +- πŸ“ˆ **Analytics**: "Create real-time dashboard with user metrics" + +The PlanExecuteRouter brings Cursor-style intelligent task automation to Flo AI, making it incredibly easy to build sophisticated multi-step workflows that adapt and execute complex tasks automatically! πŸš€ + +#### YAML Workflow with Variables + +```yaml +metadata: + name: "personalized-workflow" + version: "1.0.0" + description: "Workflow that adapts based on input variables" + +arium: + agents: + - name: "specialist" + role: "" + job: "You are a specializing in . Provide for ." + model: + provider: "" + name: "" + settings: + temperature: 0.3 + reasoning_pattern: "" + + - name: "reviewer" + role: "Quality Reviewer" + job: "Review the for and provide feedback." + model: + provider: "openai" + name: "gpt-4o" + + workflow: + start: "specialist" + edges: + - from: "specialist" + to: ["reviewer"] + end: ["reviewer"] +``` + +```python +import asyncio +from typing import Any, Dict, List +from flo_ai.arium import AriumBuilder + +async def run_personalized_workflow() -> List[Any]: + yaml_config = """...""" # Your YAML configuration with variables + + # Define variables for the workflow + variables: Dict[str, str] = { + 'expert_role': 'Data Scientist', + 'domain': 'machine learning and predictive analytics', + 'output_type': 'technical analysis report', + 'target_audience': 'engineering team', + 'preferred_llm_provider': 'anthropic', + 'model_name': 'claude-3-5-sonnet-20240620', + 'reasoning_style': 'COT', + 'quality_criteria': 'technical accuracy and clarity' + } + + result: List[Any] = await ( + AriumBuilder() + .from_yaml(yaml_config) + .build_and_run( + ["Analyze our customer churn prediction model performance"], + variables=variables + ) + ) + + return result +``` + +#### Using Pre-built Agents in YAML Workflows + +```yaml +metadata: + name: "hybrid-workflow" + version: "1.0.0" + description: "Mix of inline agents and pre-built agent references" + +# Import existing agent configurations +imports: + - "agents/content_analyzer.yaml" + - "agents/technical_reviewer.yaml" + +arium: + # Mix of imported and inline agents + agents: + # Reference imported agent + - import: "content_analyzer" + name: "analyzer" # Override name if needed + + # Define new agent inline + - name: "formatter" + role: "Content Formatter" + job: "Format the analysis into a professional report structure." + model: + provider: "openai" + name: "gpt-4o-mini" + + # Reference another imported agent + - import: "technical_reviewer" + name: "reviewer" + + workflow: + start: "analyzer" + edges: + - from: "analyzer" + to: ["formatter"] + - from: "formatter" + to: ["reviewer"] + end: ["reviewer"] +``` + +#### YAML Workflow Best Practices + +1. **Modular Design**: Define reusable agents in YAML, create tools in Python separately +2. **Clear Naming**: Use descriptive names for agents and workflows +3. **Variable Usage**: Leverage variables for environment-specific configurations +4. **Version Control**: Track workflow versions in metadata +5. **Documentation**: Include descriptions for complex workflows +6. **Router Functions**: Keep routing logic simple and provide as Python functions +7. **Tool Management**: Create tools as Python objects and pass them to the builder + +#### What Can Be Defined in YAML vs Python + +**βœ… YAML Configuration Supports:** +- Agent definitions (name, role, job, model settings) +- Workflow structure (start, edges, end nodes) +- Agent-to-agent connections +- Tool and router references (by name) +- Variables and settings +- Model configurations + +**❌ YAML Configuration Does NOT Support:** +- Tool function implementations (must be Python objects) +- Router function code (must be Python functions) +- Custom logic execution +- Direct function definitions + +**πŸ’‘ Best Practice**: Use YAML for workflow structure and agent configuration, Python for executable logic (tools and routers). + +#### Benefits of YAML Workflows + +- **πŸ”„ Reproducible**: Version-controlled workflow definitions +- **πŸ“ Maintainable**: Easy to modify workflow structure without code changes +- **πŸ§ͺ Testable**: Different configurations for testing vs. production +- **πŸ‘₯ Collaborative**: Non-developers can modify workflow structure +- **πŸš€ Deployable**: Easy CI/CD integration with YAML configurations +- **πŸ” Auditable**: Clear workflow definitions for compliance + +> πŸ“– **For detailed Arium documentation and advanced patterns, see [flo_ai/flo_ai/arium/README.md](flo_ai/flo_ai/arium/README.md)** + +## πŸ“– Documentation + +Visit our [comprehensive documentation](https://flo-ai.rootflo.ai) for: +- Detailed tutorials +- API reference +- 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 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 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 and Processing +- πŸ“ Content Generation and Summarization +- πŸ” Research and Information Retrieval +- 🎯 Task-Specific AI Assistants +- πŸ“§ Email Analysis and Classification + +## 🀝 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) +- [Pydantic](https://github.com/pydantic/pydantic) +- [OpenAI](https://openai.com/) +- [Anthropic](https://www.anthropic.com/) + +--- + +
+ Built with ❀️ by the rootflo team +
Community β€’ + Documentation +
diff --git a/flo_ai/examples/telemetry_example.py b/flo_ai/examples/telemetry_example.py new file mode 100644 index 00000000..fe4dafec --- /dev/null +++ b/flo_ai/examples/telemetry_example.py @@ -0,0 +1,382 @@ +""" +Example demonstrating OpenTelemetry integration with flo_ai + +This example shows how to: +1. Configure telemetry with OTLP and console exporters +2. Track LLM calls with token usage +3. Monitor agent execution with performance metrics +4. Track workflow execution in Arium + +Requirements: +- OpenTelemetry collector running (optional, for OTLP export) +- Or use console export for debugging +""" + +import asyncio +import os +from flo_ai import ( + Agent, + OpenAI, + configure_telemetry, + shutdown_telemetry, + flo_tool, + AriumBuilder, + MessageMemory, +) + + +# Configure telemetry at the start of your application +def setup_telemetry(): + """ + Configure OpenTelemetry for the application + + Options: + 1. Console export (for debugging) - set console_export=True + 2. OTLP export (for production) - provide otlp_endpoint + 3. Both - set both options + + Environment variables: + - FLO_ENV: Environment name (development, staging, production) + - FLO_OTLP_ENDPOINT: OTLP collector endpoint (e.g., http://localhost:4317) + """ + configure_telemetry( + service_name='flo_ai_example', + service_version='1.0.0', + environment=os.getenv('FLO_ENV', 'development'), + # For local testing with Jaeger/OTLP collector: + otlp_endpoint='http://localhost:4317', + # For debugging, export to console: + console_export=True, + additional_attributes={ + 'deployment.region': 'us-west-2', + 'service.instance.id': 'instance-1', + }, + ) + print('βœ“ Telemetry configured') + + +# Example 1: Basic Agent with LLM Telemetry +async def example_basic_agent(llm): + """ + Demonstrates basic agent with automatic LLM telemetry. + + Telemetry captured: + - LLM request duration + - Token usage (prompt, completion, total) + - Agent execution time + - Success/error metrics + """ + print('\n=== Example 1: Basic Agent with Telemetry ===') + + # Create an agent (telemetry is automatically tracked) + agent = Agent( + name='researcher', + system_prompt='You are a helpful research assistant.', + llm=llm, + ) + + # Run the agent - all metrics are automatically captured + response = await agent.run('What is the capital of France?') + print(f'Response: {response}') + + return response + + +# Example 2: Agent with Tools +async def example_agent_with_tools(llm): + """ + Demonstrates agent with tool calls and telemetry. + + Telemetry captured: + - Tool execution duration + - Tool success/failure rates + - Number of tool calls per agent run + """ + print('\n=== Example 2: Agent with Tools ===') + + # Define a simple tool + @flo_tool( + name='calculator', + description='Performs basic arithmetic operations', + ) + async def calculator(operation: str, a: float, b: float) -> float: + """ + Perform calculation + + Args: + operation: One of 'add', 'subtract', 'multiply', 'divide' + a: First number + b: Second number + + Returns: + Result of the operation + """ + if operation == 'add': + return a + b + elif operation == 'subtract': + return a - b + elif operation == 'multiply': + return a * b + elif operation == 'divide': + return a / b if b != 0 else 0 + else: + raise ValueError(f'Unknown operation: {operation}') + + agent = Agent( + name='calculator_agent', + system_prompt='You are a helpful calculator assistant. Use the calculator tool to perform calculations.', + llm=llm, + tools=[ + calculator.tool + ], # Access the .tool attribute from the decorated function + ) + + # Run with tool calls - tool metrics are automatically captured + response = await agent.run('What is 25 multiplied by 4, then add 10?') + print(f'Response: {response}') + + return response + + +# Example 3: Workflow with Arium +async def example_workflow(llm): + """ + Demonstrates workflow telemetry with Arium. + + Telemetry captured: + - Workflow execution duration + - Individual node execution times + - Node success/failure rates + - Workflow traversal paths + """ + print('\n=== Example 3: Workflow Telemetry ===') + + # Create agents + researcher = Agent( + name='researcher', + system_prompt='You are a research assistant. Provide concise, factual information.', + llm=llm, + ) + + summarizer = Agent( + name='summarizer', + system_prompt='You are a summarization expert. Create brief, clear summaries.', + llm=llm, + ) + + # Build workflow + builder = ( + AriumBuilder() + .with_memory(MessageMemory()) + .add_agent(researcher) + .add_agent(summarizer) + .start_with(researcher) + .connect(researcher, summarizer) + .end_with(summarizer) + ) + + # Compile and set name for telemetry + workflow = builder.build() + workflow.name = 'research_workflow' # This name will appear in telemetry + + # Run workflow - all node executions are tracked + result = await workflow.run(inputs=['What are the key benefits of OpenTelemetry?']) + + print(f'Workflow result: {result}') + return result + + +# Example 4: Multiple LLM Providers +async def example_multiple_providers(llm): + """ + Demonstrates telemetry across different LLM providers. + + Telemetry will show: + - Performance comparison between providers + - Token usage by provider + - Error rates by provider + """ + print('\n=== Example 4: Multiple LLM Providers ===') + + agent = Agent( + name='test_agent', + system_prompt='You are a helpful assistant.', + llm=llm, + ) + + question = 'What is 2+2?' + + print(f'Testing {llm.__class__.__name__}...') + response = await agent.run(question) + print(f'Response: {response}') + + # Telemetry will show metrics grouped by provider + + +# Example 5: Streaming Telemetry +async def example_streaming_telemetry(llm): + """ + Demonstrates telemetry for streaming LLM calls. + + Telemetry captured: + - Stream request duration + - Number of chunks received + - Stream success/error rates + - Streaming-specific metrics + """ + print('\n=== Example 5: Streaming Telemetry ===') + + # Test streaming with telemetry + messages = [ + { + 'role': 'user', + 'content': 'Tell me a short story about a robot learning to paint.', + } + ] + + print('Streaming response with telemetry...') + full_response = '' + async for chunk in llm.stream(messages): + content = chunk.get('content', '') + if content: + full_response += content + print(content, end='', flush=True) + + print(f'\n\nFull response length: {len(full_response)} characters') + print('βœ“ Streaming telemetry captured (check your telemetry backend)') + + return full_response + + +# Example 6: Error Tracking +async def example_error_tracking(llm): + """ + Demonstrates how errors are tracked in telemetry. + + Telemetry captured: + - Error types and counts + - Retry attempts + - Failed operations with context + """ + print('\n=== Example 6: Error Tracking ===') + + # Create a tool that might fail + @flo_tool( + name='risky_operation', + description='An operation that might fail', + ) + async def risky_operation(value: int) -> str: + if value < 0: + raise ValueError('Value must be non-negative') + return f'Success with value: {value}' + + agent = Agent( + name='error_test_agent', + system_prompt='You are a testing assistant.', + llm=llm, + tools=[ + risky_operation.tool + ], # Access the .tool attribute from the decorated function + max_retries=2, + ) + + try: + # This might cause errors that will be tracked + response = await agent.run('Use the risky_operation with value -5') + print(f'Response: {response}') + except Exception as e: + print(f'Caught error (tracked in telemetry): {e}') + + +async def main(llm=None): + """Main function to run all examples + + Args: + llm: LLM instance to use for all examples. If None, defaults to OpenAI. + """ + # 1. Configure telemetry first + setup_telemetry() + + # Use provided LLM or default to OpenAI + if llm is None: + llm = OpenAI(model='gpt-4o-mini') + + try: + # 2. Run examples with the provided LLM + await example_basic_agent(llm) + await example_agent_with_tools(llm) + await example_workflow(llm) + await example_multiple_providers(llm) + await example_streaming_telemetry(llm) + await example_error_tracking(llm) + + print('\nβœ“ All examples completed') + print('\n=== Telemetry Data ===') + print('Metrics collected:') + print('- LLM request counts, latencies, token usage') + print('- LLM streaming: chunks received, stream duration, success rates') + print('- Agent execution times, tool calls, retries') + print('- Workflow execution times, node traversals') + print('- Error rates and types') + + finally: + # 3. Shutdown telemetry to flush all data + print('\nShutting down telemetry...') + shutdown_telemetry() + print('βœ“ Telemetry shutdown complete') + + +# Running with Jaeger (optional) +""" +To visualize telemetry data with Jaeger: + +1. Start Jaeger (using Docker): + docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4317:4317 \ + jaegertracing/all-in-one:latest + +2. Update the configure_telemetry call to use OTLP: + configure_telemetry( + service_name="flo_ai_example", + otlp_endpoint="http://localhost:4317", + console_export=False + ) + +3. Run this script + +4. View traces at http://localhost:16686 + +Alternatively, use Grafana, Prometheus, or any OpenTelemetry-compatible backend. +""" + + +if __name__ == '__main__': + # Set your API keys + # os.environ["OPENAI_API_KEY"] = "your-key-here" + # os.environ["GOOGLE_API_KEY"] = "your-key-here" + # os.environ["ANTHROPIC_API_KEY"] = "your-key-here" + + # Example 1: Run with default OpenAI + # print("=== Running with OpenAI ===") + # asyncio.run(main()) + + # Example 2: Run with a specific LLM provider + # Uncomment the provider you want to test: + + from flo_ai import Gemini + + print('\n=== Running with Gemini ===') + gemini_llm = Gemini(model='gemini-2.5-flash') + asyncio.run(main(gemini_llm)) + + # from flo_ai import Anthropic + # print("\n=== Running with Anthropic ===") + # anthropic_llm = Anthropic(model="claude-3-5-sonnet-20241022") + # asyncio.run(main(anthropic_llm)) + + # from flo_ai import OpenAI + # print("\n=== Running with different OpenAI model ===") + # openai_llm = OpenAI(model="gpt-4o") + # asyncio.run(main(openai_llm)) diff --git a/flo_ai/flo_ai/__init__.py b/flo_ai/flo_ai/__init__.py index 671e8abe..d3078b4f 100644 --- a/flo_ai/flo_ai/__init__.py +++ b/flo_ai/flo_ai/__init__.py @@ -40,6 +40,15 @@ # Utils package - Utility functions from .utils import FloUtils +# Telemetry package - OpenTelemetry integration +from .telemetry import ( + configure_telemetry, + shutdown_telemetry, + get_tracer, + get_meter, + FloTelemetry, +) + __all__ = [ # Models 'Agent', @@ -81,6 +90,12 @@ 'AriumEventType', 'AriumEvent', 'default_event_callback', + # Telemetry + 'configure_telemetry', + 'shutdown_telemetry', + 'get_tracer', + 'get_meter', + 'FloTelemetry', ] __version__ = '1.0.0' diff --git a/flo_ai/flo_ai/arium/arium.py b/flo_ai/flo_ai/arium/arium.py index 663feebf..cc969e71 100644 --- a/flo_ai/flo_ai/arium/arium.py +++ b/flo_ai/flo_ai/arium/arium.py @@ -15,6 +15,9 @@ validate_multi_agent_variables, resolve_variables, ) +from flo_ai.telemetry.instrumentation import workflow_metrics +from flo_ai.telemetry import get_tracer +from opentelemetry.trace import Status, StatusCode import asyncio import time @@ -64,37 +67,110 @@ async def run( # Emit workflow started event self._emit_event(AriumEventType.WORKFLOW_STARTED, event_callback, events_filter) - try: - # Extract and validate variables from inputs and all agents - self._extract_and_validate_variables(inputs, variables) + # Get workflow name for telemetry + workflow_name = getattr(self, 'name', 'unnamed_workflow') + + # Start telemetry tracing + tracer = get_tracer() + workflow_start_time = time.time() + + if tracer: + with tracer.start_as_current_span( + f'workflow.{workflow_name}', + attributes={ + 'workflow.name': workflow_name, + 'workflow.node_count': len(self.nodes), + }, + ) as workflow_span: + try: + # Extract and validate variables from inputs and all agents + self._extract_and_validate_variables(inputs, variables) + + # Resolve variables in inputs and agent prompts + resolved_inputs = self._resolve_inputs(inputs, variables) + self._resolve_agent_prompts(variables) + + # Execute the workflow with event support + result = await self._execute_graph( + resolved_inputs, event_callback, events_filter, variables + ) + + # Record successful workflow execution + workflow_duration_ms = (time.time() - workflow_start_time) * 1000 + workflow_metrics.record_workflow(workflow_name, 'success') + workflow_metrics.record_workflow_latency( + workflow_duration_ms, workflow_name + ) + + workflow_span.set_status(Status(StatusCode.OK)) + workflow_span.set_attribute( + 'workflow.result.length', len(str(result)) + ) + + # Emit workflow completed event + self._emit_event( + AriumEventType.WORKFLOW_COMPLETED, event_callback, events_filter + ) + + self.memory = MessageMemory() # cleanup the graph + + return result + + except Exception as e: + # Record failed workflow execution + workflow_duration_ms = (time.time() - workflow_start_time) * 1000 + error_type = type(e).__name__ + + workflow_metrics.record_workflow(workflow_name, 'error') + workflow_metrics.record_error(workflow_name, error_type) + workflow_metrics.record_workflow_latency( + workflow_duration_ms, workflow_name + ) + + workflow_span.set_status(Status(StatusCode.ERROR, str(e))) + workflow_span.set_attribute('error.type', error_type) + + # Emit workflow failed event + self._emit_event( + AriumEventType.WORKFLOW_FAILED, + event_callback, + events_filter, + error=str(e), + ) + raise + else: + # No telemetry, execute without tracing + try: + # Extract and validate variables from inputs and all agents + self._extract_and_validate_variables(inputs, variables) - # Resolve variables in inputs and agent prompts - resolved_inputs = self._resolve_inputs(inputs, variables) - self._resolve_agent_prompts(variables) + # Resolve variables in inputs and agent prompts + resolved_inputs = self._resolve_inputs(inputs, variables) + self._resolve_agent_prompts(variables) - # Execute the workflow with event support - result = await self._execute_graph( - resolved_inputs, event_callback, events_filter, variables - ) + # Execute the workflow with event support + result = await self._execute_graph( + resolved_inputs, event_callback, events_filter, variables + ) - # Emit workflow completed event - self._emit_event( - AriumEventType.WORKFLOW_COMPLETED, event_callback, events_filter - ) + # Emit workflow completed event + self._emit_event( + AriumEventType.WORKFLOW_COMPLETED, event_callback, events_filter + ) - self.memory = MessageMemory() # cleanup the graph (if used as AriumNode multiple times in graph, then the same instance is used for now hence we need to cleanup memory) + self.memory = MessageMemory() # cleanup the graph - return result + return result - except Exception as e: - # Emit workflow failed event - self._emit_event( - AriumEventType.WORKFLOW_FAILED, - event_callback, - events_filter, - error=str(e), - ) - raise + except Exception as e: + # Emit workflow failed event + self._emit_event( + AriumEventType.WORKFLOW_FAILED, + event_callback, + events_filter, + error=str(e), + ) + raise def _emit_event( self, @@ -349,62 +425,158 @@ async def _execute_node( ) start_time = time.time() - - try: - # Execute the node based on its type - if isinstance(node, Agent): - # Variables are already resolved, pass empty dict to avoid re-processing - result = await node.run(self.memory.get(), variables={}) - elif isinstance(node, Tool): - # result = await node.execute() # as Tool is also an ExecutableNode now - result = await node.run(inputs=[], variables=None) - elif isinstance(node, ForEachNode): - result = await node.run( - inputs=self.memory.get(), - variables=variables, + workflow_name = getattr(self, 'name', 'unnamed_workflow') + + # Start node telemetry tracing + tracer = get_tracer() + + if tracer and node_type not in ['start', 'end']: + with tracer.start_as_current_span( + f'workflow.node.{node.name}', + attributes={ + 'workflow.name': workflow_name, + 'node.name': node.name, + 'node.type': node_type, + }, + ) as node_span: + try: + # Execute the node based on its type + if isinstance(node, Agent): + # Variables are already resolved, pass empty dict to avoid re-processing + result = await node.run(self.memory.get(), variables={}) + elif isinstance(node, Tool): + result = await node.run(inputs=[], variables=None) + elif isinstance(node, ForEachNode): + result = await node.run( + inputs=self.memory.get(), + variables=variables, + ) + elif isinstance(node, AriumNode): + # AriumNode execution + result = await node.run( + inputs=self.memory.get(), variables=variables + ) + elif isinstance(node, StartNode): + result = None + elif isinstance(node, EndNode): + result = None + else: + result = None + + # Calculate execution time + execution_time = time.time() - start_time + execution_time_ms = execution_time * 1000 + + # Record node metrics + workflow_metrics.record_node( + workflow_name, node.name, node_type, 'success' + ) + workflow_metrics.record_node_latency( + execution_time_ms, workflow_name, node.name, node_type + ) + + node_span.set_status(Status(StatusCode.OK)) + node_span.set_attribute('node.execution_time_ms', execution_time_ms) + + # Emit node completed event + self._emit_event( + AriumEventType.NODE_COMPLETED, + event_callback, + events_filter, + node_name=node.name, + node_type=node_type, + execution_time=execution_time, + ) + + return result + + except Exception as e: + # Calculate execution time even on failure + execution_time = time.time() - start_time + execution_time_ms = execution_time * 1000 + error_type = type(e).__name__ + + # Record node failure + workflow_metrics.record_node( + workflow_name, node.name, node_type, 'error' + ) + workflow_metrics.record_node_latency( + execution_time_ms, workflow_name, node.name, node_type + ) + + node_span.set_status(Status(StatusCode.ERROR, str(e))) + node_span.set_attribute('error.type', error_type) + node_span.set_attribute('node.execution_time_ms', execution_time_ms) + + # Emit node failed event + self._emit_event( + AriumEventType.NODE_FAILED, + event_callback, + events_filter, + node_name=node.name, + node_type=node_type, + execution_time=execution_time, + error=str(e), + ) + + # Re-raise the exception + raise e + else: + # No telemetry or start/end node, execute without tracing + try: + # Execute the node based on its type + if isinstance(node, Agent): + result = await node.run(self.memory.get(), variables={}) + elif isinstance(node, Tool): + result = await node.run(inputs=[], variables=None) + elif isinstance(node, ForEachNode): + result = await node.run( + inputs=self.memory.get(), + variables=variables, + ) + elif isinstance(node, AriumNode): + result = await node.run( + inputs=self.memory.get(), variables=variables + ) + elif isinstance(node, StartNode): + result = None + elif isinstance(node, EndNode): + result = None + else: + result = None + + # Calculate execution time + execution_time = time.time() - start_time + + # Emit node completed event + self._emit_event( + AriumEventType.NODE_COMPLETED, + event_callback, + events_filter, + node_name=node.name, + node_type=node_type, + execution_time=execution_time, ) - elif isinstance(node, AriumNode): - # AriumNode execution - result = await node.run(inputs=self.memory.get(), variables=variables) - elif isinstance(node, StartNode): - result = None - elif isinstance(node, EndNode): - result = None - else: - result = None - - # Calculate execution time - execution_time = time.time() - start_time - - # Emit node completed event - self._emit_event( - AriumEventType.NODE_COMPLETED, - event_callback, - events_filter, - node_name=node.name, - node_type=node_type, - execution_time=execution_time, - ) - return result - - except Exception as e: - # Calculate execution time even on failure - execution_time = time.time() - start_time - - # Emit node failed event - self._emit_event( - AriumEventType.NODE_FAILED, - event_callback, - events_filter, - node_name=node.name, - node_type=node_type, - execution_time=execution_time, - error=str(e), - ) + return result + + except Exception as e: + # Calculate execution time even on failure + execution_time = time.time() - start_time + + # Emit node failed event + self._emit_event( + AriumEventType.NODE_FAILED, + event_callback, + events_filter, + node_name=node.name, + node_type=node_type, + execution_time=execution_time, + error=str(e), + ) - # Re-raise the exception - raise e + # Re-raise the exception + raise e def _add_to_memory(self, result: str): # TODO result will be None for start and end nodes diff --git a/flo_ai/flo_ai/arium/llm_router.py b/flo_ai/flo_ai/arium/llm_router.py index 17f98d6c..24bc1b7f 100644 --- a/flo_ai/flo_ai/arium/llm_router.py +++ b/flo_ai/flo_ai/arium/llm_router.py @@ -186,11 +186,11 @@ def get_routing_prompt( ) -> str: conversation = memory.get() - # Format conversation history + # Format conversation history with smart truncation if isinstance(conversation, list): - conversation_text = '\n'.join( - [str(msg) for msg in conversation[-5:]] - ) # Last 5 messages + # Start with last message and add more if we have space + messages = conversation[-5:] # Last 5 messages + conversation_text = self._truncate_conversation_for_tokens(messages) else: conversation_text = str(conversation) @@ -244,6 +244,34 @@ def get_routing_prompt( return prompt + def _truncate_conversation_for_tokens( + self, messages: List[Any], max_tokens: int = 128000 + ) -> str: + """ + Intelligently truncate conversation to fit within token limits. + Prioritizes recent messages while ensuring we don't exceed token limits. + """ + if not messages: + return '' + + # Start with the most recent message + truncated_messages = [messages[-1]] + current_text = str(messages[-1]) + + # Add older messages if we have space + for msg in reversed(messages[:-1]): + msg_text = str(msg) + # Rough token estimation (4 chars per token is a common approximation) + estimated_tokens = len(current_text + '\n' + msg_text) // 4 + + if estimated_tokens <= max_tokens: + truncated_messages.insert(0, msg) + current_text = '\n'.join([str(m) for m in truncated_messages]) + else: + break + + return '\n'.join([str(msg) for msg in truncated_messages]) + class TaskClassifierRouter(BaseLLMRouter): """ diff --git a/flo_ai/flo_ai/arium/nodes.py b/flo_ai/flo_ai/arium/nodes.py index fb6a2ffb..81e0f510 100644 --- a/flo_ai/flo_ai/arium/nodes.py +++ b/flo_ai/flo_ai/arium/nodes.py @@ -1,6 +1,7 @@ from flo_ai.arium.protocols import ExecutableNode from typing import List, Any, Dict, Optional, TYPE_CHECKING from flo_ai.utils.logger import logger +from flo_ai.arium.memory import MessageMemory if TYPE_CHECKING: # need to have an optional import else will get circular dependency error as arium also has AriumNode reference from flo_ai.arium.arium import Arium @@ -93,3 +94,49 @@ async def run( logger.info(f"ForEach '{self.name}': Completed processing {len(results)} items") return results + + async def _execute_item_with_isolated_memory( + self, + item: Any, + index: int, + variables: Optional[Dict[str, Any]] = None, + ) -> Any: + """ + Execute the node on a single item with isolated memory. + This prevents memory accumulation across iterations. + """ + logger.info( + f"ForEach '{self.name}': Processing item {index + 1} with isolated memory" + ) + + # Create execution variables with item context + item_variables = (variables or {}).copy() + + # If the execute_node is an AriumNode, we can create a new memory instance + if hasattr(self.execute_node, 'arium') and hasattr( + self.execute_node.arium, 'memory' + ): + # Create a new memory instance for this iteration + original_memory = self.execute_node.arium.memory + self.execute_node.arium.memory = MessageMemory() + + try: + # Execute the node with isolated memory + result = await self.execute_node.run( + inputs=[item], + variables=item_variables, + ) + finally: + # Restore original memory + self.execute_node.arium.memory = original_memory + else: + # For non-Arium nodes, execute normally + result = await self.execute_node.run( + inputs=[item], + variables=item_variables, + ) + + # Return last item if result is a list, otherwise return as-is + if isinstance(result, list) and result: + return result[-1] + return result diff --git a/flo_ai/flo_ai/llm/anthropic_llm.py b/flo_ai/flo_ai/llm/anthropic_llm.py index 308d177d..39247f67 100644 --- a/flo_ai/flo_ai/llm/anthropic_llm.py +++ b/flo_ai/flo_ai/llm/anthropic_llm.py @@ -3,6 +3,14 @@ import json from .base_llm import BaseLLM, ImageMessage from flo_ai.tool.base_tool import Tool +from flo_ai.telemetry.instrumentation import ( + trace_llm_call, + trace_llm_stream, + llm_metrics, + add_span_attributes, +) +from flo_ai.telemetry import get_tracer +from opentelemetry import trace class Anthropic(BaseLLM): @@ -17,6 +25,7 @@ def __init__( super().__init__(model, api_key, temperature, **kwargs) self.client = AsyncAnthropic(api_key=self.api_key, base_url=base_url) + @trace_llm_call(provider='anthropic') async def generate( self, messages: List[Dict[str, str]], @@ -60,6 +69,31 @@ async def generate( response = await self.client.messages.create(**kwargs) + # Record token usage if available + if hasattr(response, 'usage') and response.usage: + usage = response.usage + llm_metrics.record_tokens( + total_tokens=usage.input_tokens + usage.output_tokens, + prompt_tokens=usage.input_tokens, + completion_tokens=usage.output_tokens, + model=self.model, + provider='anthropic', + ) + + # Add token info to current span + tracer = get_tracer() + if tracer: + current_span = trace.get_current_span() + add_span_attributes( + current_span, + { + 'llm.tokens.prompt': usage.input_tokens, + 'llm.tokens.completion': usage.output_tokens, + 'llm.tokens.total': usage.input_tokens + + usage.output_tokens, + }, + ) + # Check if there's a tool use in the response for content_block in response.content: if content_block.type == 'tool_use': @@ -77,6 +111,7 @@ async def generate( except Exception as e: raise Exception(f'Error in Claude API call: {str(e)}') + @trace_llm_stream(provider='anthropic') async def stream( self, messages: List[Dict[str, str]], diff --git a/flo_ai/flo_ai/llm/gemini_llm.py b/flo_ai/flo_ai/llm/gemini_llm.py index 5f891903..55065527 100644 --- a/flo_ai/flo_ai/llm/gemini_llm.py +++ b/flo_ai/flo_ai/llm/gemini_llm.py @@ -1,8 +1,17 @@ +import base64 from typing import Dict, Any, List, Optional, AsyncIterator from google import genai from google.genai import types from .base_llm import BaseLLM, ImageMessage from flo_ai.tool.base_tool import Tool +from flo_ai.telemetry.instrumentation import ( + trace_llm_call, + trace_llm_stream, + llm_metrics, + add_span_attributes, +) +from flo_ai.telemetry import get_tracer +from opentelemetry import trace class Gemini(BaseLLM): @@ -19,6 +28,7 @@ def __init__( genai.Client(api_key=self.api_key) if self.api_key else genai.Client() ) + @trace_llm_call(provider='gemini') async def generate( self, messages: List[Dict[str, str]], @@ -63,6 +73,34 @@ async def generate( config=generation_config, ) + # Record token usage if available + if hasattr(response, 'usage_metadata') and response.usage_metadata: + usage = response.usage_metadata + prompt_tokens = getattr(usage, 'prompt_token_count', 0) + completion_tokens = getattr(usage, 'candidates_token_count', 0) + total_tokens = getattr(usage, 'total_token_count', 0) + + llm_metrics.record_tokens( + total_tokens=total_tokens, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + model=self.model, + provider='gemini', + ) + + # Add token info to current span + tracer = get_tracer() + if tracer: + current_span = trace.get_current_span() + add_span_attributes( + current_span, + { + 'llm.tokens.prompt': prompt_tokens, + 'llm.tokens.completion': completion_tokens, + 'llm.tokens.total': total_tokens, + }, + ) + # Check for function call in the response if ( functions @@ -89,6 +127,7 @@ async def generate( except Exception as e: raise Exception(f'Error in Gemini API call: {str(e)}') + @trace_llm_stream(provider='gemini') async def stream( self, messages: List[Dict[str, str]], @@ -182,6 +221,11 @@ def format_image_in_message(self, image: ImageMessage) -> str: data=image.image_bytes, mime_type=image.mime_type, ) + elif image.image_base64: + return types.Part.from_bytes( + data=base64.b64decode(image.image_base64), + mime_type=image.mime_type, + ) raise NotImplementedError( 'Not other way other than file path has been implemented' ) diff --git a/flo_ai/flo_ai/llm/ollama_llm.py b/flo_ai/flo_ai/llm/ollama_llm.py index 8e7015be..caabb3b4 100644 --- a/flo_ai/flo_ai/llm/ollama_llm.py +++ b/flo_ai/flo_ai/llm/ollama_llm.py @@ -3,6 +3,7 @@ import json from .base_llm import BaseLLM, ImageMessage from flo_ai.tool.base_tool import Tool +from flo_ai.telemetry.instrumentation import trace_llm_call, trace_llm_stream class OllamaLLM(BaseLLM): @@ -17,6 +18,7 @@ def __init__( super().__init__(model, api_key, temperature, **kwargs) self.base_url = base_url.rstrip('/') + @trace_llm_call(provider='ollama') async def generate( self, messages: List[Dict[str, str]], @@ -65,6 +67,7 @@ async def generate( 'function_call': result.get('function_call'), } + @trace_llm_stream(provider='ollama') async def stream( self, messages: List[Dict[str, str]], diff --git a/flo_ai/flo_ai/llm/openai_llm.py b/flo_ai/flo_ai/llm/openai_llm.py index 3531f500..5fd721c3 100644 --- a/flo_ai/flo_ai/llm/openai_llm.py +++ b/flo_ai/flo_ai/llm/openai_llm.py @@ -2,6 +2,14 @@ from openai import AsyncOpenAI from .base_llm import BaseLLM, ImageMessage from flo_ai.tool.base_tool import Tool +from flo_ai.telemetry.instrumentation import ( + trace_llm_call, + trace_llm_stream, + llm_metrics, + add_span_attributes, +) +from flo_ai.telemetry import get_tracer +from opentelemetry import trace class OpenAI(BaseLLM): @@ -20,6 +28,7 @@ def __init__( self.model = model self.kwargs = kwargs + @trace_llm_call(provider='openai') async def generate( self, messages: list[dict], output_schema: dict = None, **kwargs ) -> Any: @@ -62,9 +71,34 @@ async def generate( response = await self.client.chat.completions.create(**openai_kwargs) message = response.choices[0].message + # Record token usage if available + if hasattr(response, 'usage') and response.usage: + usage = response.usage + llm_metrics.record_tokens( + total_tokens=usage.total_tokens, + prompt_tokens=usage.prompt_tokens, + completion_tokens=usage.completion_tokens, + model=self.model, + provider='openai', + ) + + # Add token info to current span + tracer = get_tracer() + if tracer: + current_span = trace.get_current_span() + add_span_attributes( + current_span, + { + 'llm.tokens.prompt': usage.prompt_tokens, + 'llm.tokens.completion': usage.completion_tokens, + 'llm.tokens.total': usage.total_tokens, + }, + ) + # Return the full message object instead of just the content return message + @trace_llm_stream(provider='openai') async def stream( self, messages: List[Dict[str, Any]], diff --git a/flo_ai/flo_ai/llm/openai_vllm.py b/flo_ai/flo_ai/llm/openai_vllm.py index 37ffa068..88213077 100644 --- a/flo_ai/flo_ai/llm/openai_vllm.py +++ b/flo_ai/flo_ai/llm/openai_vllm.py @@ -1,5 +1,6 @@ from typing import Any, AsyncIterator, Dict, List, Optional from .openai_llm import OpenAI +from flo_ai.telemetry.instrumentation import trace_llm_stream class OpenAIVLLM(OpenAI): @@ -67,6 +68,7 @@ async def generate( # Return the full message object instead of just the content return message + @trace_llm_stream(provider='openai_vllm') async def stream( self, messages: List[Dict[str, Any]], diff --git a/flo_ai/flo_ai/models/agent.py b/flo_ai/flo_ai/models/agent.py index e1ee2e53..de203acf 100644 --- a/flo_ai/flo_ai/models/agent.py +++ b/flo_ai/flo_ai/models/agent.py @@ -12,6 +12,11 @@ validate_multi_agent_variables, resolve_variables, ) +from flo_ai.telemetry.instrumentation import ( + trace_agent_execution, + agent_metrics, +) +from flo_ai.telemetry import get_tracer class Agent(BaseAgent): @@ -49,6 +54,7 @@ def __init__( self.output_schema = output_schema self.role = role + @trace_agent_execution() async def run( self, inputs: List[str | ImageMessage | DocumentMessage] | str, @@ -131,14 +137,14 @@ async def _run_conversational( } ] + self.conversation_history - logger.debug('Sending messages to LLM:', messages) + logger.debug(f'Sending messages to LLM: {messages}') response = await self.llm.generate( messages, output_schema=self.output_schema ) - logger.debug('Raw LLM Response:', response) + logger.debug(f'Raw LLM Response: {response}') assistant_message = self.llm.get_message_content(response) - logger.debug('Extracted message:', assistant_message) + logger.debug(f'Extracted message: {assistant_message}') if assistant_message: self.add_to_history('assistant', assistant_message) @@ -219,8 +225,13 @@ async def _run_with_tools( return assistant_message else: # This is intermediate reasoning, add to context and continue + msg_preview = ( + assistant_message[:100] + if len(assistant_message) > 100 + else assistant_message + ) logger.debug( - f'Detected intermediate reasoning (not final answer): {assistant_message[:100]}...' + f'Detected intermediate reasoning (not final answer): {msg_preview}...' ) self.add_to_history('assistant', assistant_message) messages.append( @@ -248,10 +259,33 @@ async def _run_with_tools( function_args = function_call['arguments'] tool = self.tools_dict[function_name] - # function_response = await tool.execute(**function_args) - function_response = await tool.run( - inputs=[], variables=None, **function_args + + # Track tool execution with telemetry + tracer = get_tracer() + + if tracer: + with tracer.start_as_current_span( + f'agent.tool.{function_name}', + attributes={ + 'tool.name': function_name, + 'agent.name': self.name, + }, + ) as tool_span: + function_response = await tool.run( + inputs=[], variables=None, **function_args + ) + tool_span.set_attribute( + 'tool.result.length', len(str(function_response)) + ) + else: + function_response = await tool.run( + inputs=[], variables=None, **function_args + ) + + agent_metrics.record_tool_call( + self.name, function_name, 'success' ) + tool_call_count += 1 # Add function call to history @@ -279,6 +313,11 @@ async def _run_with_tools( ) except (json.JSONDecodeError, KeyError, ToolExecutionError) as e: + # Record tool call failure + agent_metrics.record_tool_call( + self.name, function_name, 'error' + ) + retry_count += 1 context = { 'function_call': function_call, @@ -286,6 +325,11 @@ async def _run_with_tools( } should_retry, analysis = await self.handle_error(e, context) if should_retry and retry_count <= self.max_retries: + # Record retry + agent_metrics.record_retry( + self.name, 'tool_execution_error' + ) + self.add_to_history( 'system', f'Tool execution error: {analysis}' ) @@ -322,6 +366,9 @@ async def _run_with_tools( should_retry, analysis = await self.handle_error(e, context) if should_retry and retry_count <= self.max_retries: + # Record retry + agent_metrics.record_retry(self.name, 'execution_error') + self.add_to_history( 'system', f'Error occurred. Analysis: {analysis}' ) @@ -485,8 +532,13 @@ async def _is_final_answer( analysis = self.llm.get_message_content(analysis_response).strip().upper() is_final = 'FINAL' in analysis + msg_preview = ( + message_stripped[:80] + if len(message_stripped) > 80 + else message_stripped + ) logger.debug( - f'LLM classifier: "{analysis}" -> is_final={is_final} (message preview: "{message_stripped[:80]}...")' + f'LLM classifier: "{analysis}" -> is_final={is_final} (message preview: "{msg_preview}...")' ) return is_final diff --git a/flo_ai/flo_ai/models/document.py b/flo_ai/flo_ai/models/document.py index 5f82d5a5..6907bec7 100644 --- a/flo_ai/flo_ai/models/document.py +++ b/flo_ai/flo_ai/models/document.py @@ -12,8 +12,8 @@ class DocumentType(Enum): """Enumeration of supported document types.""" - PDF = 'pdf' - TXT = 'txt' + PDF = 'application/pdf' + TXT = 'text/plain' @dataclass diff --git a/flo_ai/flo_ai/telemetry/README.md b/flo_ai/flo_ai/telemetry/README.md new file mode 100644 index 00000000..4374eb8d --- /dev/null +++ b/flo_ai/flo_ai/telemetry/README.md @@ -0,0 +1,397 @@ +# OpenTelemetry Integration for flo_ai + +This module provides comprehensive OpenTelemetry integration for the flo_ai framework, enabling you to monitor and track: + +- πŸ” **LLM Calls**: Track token usage, latency, and errors across different providers +- πŸ€– **Agent Execution**: Monitor agent performance, tool calls, and retry attempts +- πŸ”„ **Workflows**: Track workflow execution, node traversals, and bottlenecks +- πŸ“Š **Metrics**: Export performance metrics to your observability platform + +## Quick Start + +### 1. Installation + +The telemetry dependencies are included in flo_ai. Install with: + +```bash +pip install flo_ai +``` + +### 2. Basic Configuration + +```python +from flo_ai import configure_telemetry, shutdown_telemetry + +# Configure at the start of your application +configure_telemetry( + service_name="my_ai_app", + service_version="1.0.0", + console_export=True # For debugging +) + +# ... your application code ... + +# Shutdown at the end to flush data +shutdown_telemetry() +``` + +### 3. Export to OTLP Collector + +```python +configure_telemetry( + service_name="my_ai_app", + otlp_endpoint="http://localhost:4317", # Your OTLP collector + console_export=False +) +``` + +## Features + +### Automatic Instrumentation + +Once configured, telemetry is automatically captured for: + +#### LLM Calls +- Request duration (latency) +- Token usage (prompt, completion, total) +- Model and provider information +- Success/error rates +- **Streaming support**: Chunk count, stream duration, streaming success rates + +#### Agent Execution +- Total execution time +- Number of tool calls +- Retry attempts +- Conversation history size + +#### Workflows (Arium) +- End-to-end workflow duration +- Individual node execution times +- Router decisions +- Node success/failure rates + +### Metrics Collected + +#### LLM Metrics +- `llm.tokens.total` - Total tokens used +- `llm.tokens.prompt` - Prompt tokens +- `llm.tokens.completion` - Completion tokens +- `llm.requests.total` - Number of LLM requests +- `llm.errors.total` - Number of errors +- `llm.request.duration` - Request latency histogram +- `llm.streams.total` - Number of LLM stream requests +- `llm.stream.chunks.total` - Total number of stream chunks received +- `llm.stream.duration` - Stream request latency histogram + +#### Agent Metrics +- `agent.executions.total` - Number of agent executions +- `agent.tool_calls.total` - Number of tool calls +- `agent.retries.total` - Number of retries +- `agent.errors.total` - Number of errors +- `agent.execution.duration` - Execution time histogram + +#### Workflow Metrics +- `workflow.executions.total` - Number of workflow executions +- `workflow.nodes.executed` - Number of nodes executed +- `workflow.errors.total` - Number of errors +- `workflow.execution.duration` - Workflow execution time +- `workflow.node.duration` - Node execution time + +### Spans (Traces) + +Spans are automatically created for: +- `llm.{provider}.generate` - LLM API calls +- `agent.{name}.run` - Agent executions +- `agent.tool.{name}` - Tool executions +- `workflow.{name}` - Workflow executions +- `workflow.node.{name}` - Node executions + +## Configuration Options + +### Environment Variables + +- `FLO_ENV` - Environment name (default: "development") +- `FLO_OTLP_ENDPOINT` - OTLP endpoint URL + +### Configuration Parameters + +```python +configure_telemetry( + service_name: str = "flo_ai", # Service name for telemetry + service_version: str = "1.0.0", # Service version + environment: str = None, # Environment (dev/staging/prod) + otlp_endpoint: str = None, # OTLP collector endpoint + console_export: bool = False, # Export to console + additional_attributes: Dict[str, Any] = None # Custom resource attributes +) +``` + +## Integration Examples + +### With Jaeger + +```bash +# Start Jaeger +docker run -d --name jaeger \ + -e COLLECTOR_OTLP_ENABLED=true \ + -p 16686:16686 \ + -p 4317:4317 \ + jaegertracing/all-in-one:latest +``` + +```python +configure_telemetry( + service_name="my_app", + otlp_endpoint="http://localhost:4317" +) +``` + +View traces at http://localhost:16686 + +### With Prometheus & Grafana + +```python +# Use OTLP exporter pointing to your collector +configure_telemetry( + service_name="my_app", + otlp_endpoint="http://otel-collector:4317" +) +``` + +### With Cloud Providers + +#### AWS X-Ray +```python +# Use OTLP with AWS Distro for OpenTelemetry +configure_telemetry( + service_name="my_app", + otlp_endpoint="http://aws-otel-collector:4317" +) +``` + +#### Google Cloud Trace +```python +# Use Cloud Trace exporter +configure_telemetry( + service_name="my_app", + otlp_endpoint="http://localhost:4317", # Cloud Ops collector + additional_attributes={ + "gcp.project_id": "your-project-id" + } +) +``` + +#### Azure Monitor +```python +# Use Azure Monitor OTLP endpoint +configure_telemetry( + service_name="my_app", + otlp_endpoint="https://.monitor.azure.com/opentelemetry/v1/traces" +) +``` + +## Advanced Usage + +### Custom Spans + +```python +from flo_ai import get_tracer + +tracer = get_tracer() +if tracer: + with tracer.start_as_current_span("custom_operation") as span: + span.set_attribute("custom.attribute", "value") + # Your code here +``` + +### Custom Metrics + +```python +from flo_ai import get_meter + +meter = get_meter() +if meter: + # Create a counter + my_counter = meter.create_counter( + name="my_custom_counter", + description="Custom operation counter", + unit="operations" + ) + my_counter.add(1, {"operation": "custom"}) + + # Create a histogram + my_histogram = meter.create_histogram( + name="my_custom_duration", + description="Custom operation duration", + unit="ms" + ) + my_histogram.record(123.45, {"operation": "custom"}) +``` + +### Filtering Telemetry + +To disable telemetry temporarily: + +```python +# Don't call configure_telemetry() +# The framework will detect no telemetry is configured and skip instrumentation +``` + +## Best Practices + +### 1. Configure Once at Startup + +```python +def initialize_app(): + configure_telemetry( + service_name="my_app", + otlp_endpoint=os.getenv("OTLP_ENDPOINT"), + environment=os.getenv("ENV", "development") + ) +``` + +### 2. Always Shutdown on Exit + +```python +import atexit +from flo_ai import shutdown_telemetry + +atexit.register(shutdown_telemetry) +``` + +### 3. Use Meaningful Service Names + +```python +configure_telemetry( + service_name="customer-support-bot", # Specific and descriptive + service_version="2.1.0" +) +``` + +### 4. Add Context with Attributes + +```python +configure_telemetry( + service_name="my_app", + additional_attributes={ + "deployment.region": "us-west-2", + "team": "ml-platform", + "environment.type": "production" + } +) +``` + +### 5. Monitor Key Metrics + +Focus on: +- Token usage trends (cost monitoring) +- LLM latency (performance) +- Error rates (reliability) +- Agent execution times (user experience) + +## Performance Impact + +The telemetry instrumentation has minimal performance overhead: + +- **LLM calls**: < 1ms additional latency +- **Agent execution**: < 2ms overhead +- **Workflow execution**: < 5ms overhead +- **Memory**: ~10MB for buffering spans/metrics + +Metrics are exported asynchronously and don't block your application. + +## Troubleshooting + +### No Data Appearing + +1. Check OTLP endpoint is accessible: + ```bash + curl http://localhost:4317 + ``` + +2. Enable console export for debugging: + ```python + configure_telemetry(console_export=True) + ``` + +3. Ensure shutdown is called: + ```python + shutdown_telemetry() + ``` + +### High Memory Usage + +If you see high memory usage: + +1. Reduce export interval (default is 5 seconds) +2. Ensure shutdown is called to flush buffers +3. Check your OTLP collector is accepting data + +### Missing Token Metrics + +Token metrics are only available for providers that return usage data: +- βœ… OpenAI (full support) +- βœ… Anthropic (full support) +- βœ… Gemini (full support) +- ⚠️ Ollama (limited support) + +## Examples + +See the [telemetry_example.py](../../examples/telemetry_example.py) file for complete examples including: + +- Basic agent with LLM telemetry +- Agent with tool calls +- Workflow telemetry with Arium +- Multiple LLM providers +- Error tracking + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Your Application β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Agent.run() β†’ @trace_agent_execution β”‚ +β”‚ LLM.generate() β†’ @trace_llm_call β”‚ +β”‚ Workflow.run() β†’ telemetry tracking β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ OpenTelemetry SDK β”‚ +β”‚ β€’ Tracer Provider (spans/traces) β”‚ +β”‚ β€’ Meter Provider (metrics) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Exporters β”‚ +β”‚ β€’ OTLP Exporter β†’ Collector/Backend β”‚ +β”‚ β€’ Console Exporter β†’ stdout β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Observability Platforms β”‚ +β”‚ Jaeger, Prometheus, Grafana, Cloud Providers, etc. β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +## Contributing + +To add telemetry to new components: + +1. Import instrumentation utilities: + ```python + from flo_ai.telemetry.instrumentation import trace_llm_call, agent_metrics + from flo_ai.telemetry import get_tracer + ``` + +2. Add decorators or manual instrumentation +3. Record metrics using the appropriate metrics instance +4. Add span attributes for context + +## License + +This telemetry module is part of flo_ai and follows the same MIT license. + diff --git a/flo_ai/flo_ai/telemetry/__init__.py b/flo_ai/flo_ai/telemetry/__init__.py new file mode 100644 index 00000000..caddaf2f --- /dev/null +++ b/flo_ai/flo_ai/telemetry/__init__.py @@ -0,0 +1,19 @@ +""" +OpenTelemetry integration for flo_ai +""" + +from .telemetry import ( + FloTelemetry, + get_tracer, + get_meter, + configure_telemetry, + shutdown_telemetry, +) + +__all__ = [ + 'FloTelemetry', + 'get_tracer', + 'get_meter', + 'configure_telemetry', + 'shutdown_telemetry', +] diff --git a/flo_ai/flo_ai/telemetry/instrumentation.py b/flo_ai/flo_ai/telemetry/instrumentation.py new file mode 100644 index 00000000..4861af54 --- /dev/null +++ b/flo_ai/flo_ai/telemetry/instrumentation.py @@ -0,0 +1,660 @@ +""" +Instrumentation utilities for flo_ai components +""" + +from typing import Optional, Dict, Any, Callable +from functools import wraps +from opentelemetry.trace import Status, StatusCode, Span +from .telemetry import get_tracer, get_meter +import time +import asyncio + + +class LLMMetrics: + """Metrics for LLM operations""" + + def __init__(self): + self.meter = get_meter() + if self.meter: + # Token counters + self.token_counter = self.meter.create_counter( + name='llm.tokens.total', + description='Total number of tokens used', + unit='tokens', + ) + + self.prompt_tokens_counter = self.meter.create_counter( + name='llm.tokens.prompt', + description='Number of prompt tokens', + unit='tokens', + ) + + self.completion_tokens_counter = self.meter.create_counter( + name='llm.tokens.completion', + description='Number of completion tokens', + unit='tokens', + ) + + # Request counters + self.request_counter = self.meter.create_counter( + name='llm.requests.total', + description='Total number of LLM requests', + unit='requests', + ) + + self.error_counter = self.meter.create_counter( + name='llm.errors.total', + description='Total number of LLM errors', + unit='errors', + ) + + # Latency histogram + self.latency_histogram = self.meter.create_histogram( + name='llm.request.duration', + description='Duration of LLM requests', + unit='ms', + ) + + # Streaming metrics + self.stream_counter = self.meter.create_counter( + name='llm.streams.total', + description='Total number of LLM stream requests', + unit='streams', + ) + + self.stream_chunks_counter = self.meter.create_counter( + name='llm.stream.chunks.total', + description='Total number of stream chunks received', + unit='chunks', + ) + + self.stream_duration_histogram = self.meter.create_histogram( + name='llm.stream.duration', + description='Duration of LLM streaming requests', + unit='ms', + ) + + def record_tokens( + self, + total_tokens: int = 0, + prompt_tokens: int = 0, + completion_tokens: int = 0, + model: str = '', + provider: str = '', + ): + """Record token usage""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider} + + if total_tokens > 0: + self.token_counter.add(total_tokens, attributes) + if prompt_tokens > 0: + self.prompt_tokens_counter.add(prompt_tokens, attributes) + if completion_tokens > 0: + self.completion_tokens_counter.add(completion_tokens, attributes) + + def record_request( + self, model: str = '', provider: str = '', status: str = 'success' + ): + """Record LLM request""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider, 'status': status} + self.request_counter.add(1, attributes) + + def record_error(self, model: str = '', provider: str = '', error_type: str = ''): + """Record LLM error""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider, 'error_type': error_type} + self.error_counter.add(1, attributes) + + def record_latency(self, duration_ms: float, model: str = '', provider: str = ''): + """Record request latency""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider} + self.latency_histogram.record(duration_ms, attributes) + + def record_stream( + self, model: str = '', provider: str = '', status: str = 'success' + ): + """Record LLM stream request""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider, 'status': status} + self.stream_counter.add(1, attributes) + + def record_stream_chunks( + self, chunk_count: int, model: str = '', provider: str = '' + ): + """Record stream chunks received""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider} + self.stream_chunks_counter.add(chunk_count, attributes) + + def record_stream_latency( + self, duration_ms: float, model: str = '', provider: str = '' + ): + """Record stream request latency""" + if not self.meter: + return + + attributes = {'model': model, 'provider': provider} + self.stream_duration_histogram.record(duration_ms, attributes) + + +class AgentMetrics: + """Metrics for Agent operations""" + + def __init__(self): + self.meter = get_meter() + if self.meter: + # Execution counters + self.execution_counter = self.meter.create_counter( + name='agent.executions.total', + description='Total number of agent executions', + unit='executions', + ) + + self.tool_call_counter = self.meter.create_counter( + name='agent.tool_calls.total', + description='Total number of tool calls', + unit='calls', + ) + + self.retry_counter = self.meter.create_counter( + name='agent.retries.total', + description='Total number of retries', + unit='retries', + ) + + self.error_counter = self.meter.create_counter( + name='agent.errors.total', + description='Total number of agent errors', + unit='errors', + ) + + # Latency histogram + self.latency_histogram = self.meter.create_histogram( + name='agent.execution.duration', + description='Duration of agent executions', + unit='ms', + ) + + def record_execution( + self, agent_name: str = '', agent_type: str = '', status: str = 'success' + ): + """Record agent execution""" + if not self.meter: + return + + attributes = { + 'agent_name': agent_name, + 'agent_type': agent_type, + 'status': status, + } + self.execution_counter.add(1, attributes) + + def record_tool_call( + self, agent_name: str = '', tool_name: str = '', status: str = 'success' + ): + """Record tool call""" + if not self.meter: + return + + attributes = { + 'agent_name': agent_name, + 'tool_name': tool_name, + 'status': status, + } + self.tool_call_counter.add(1, attributes) + + def record_retry(self, agent_name: str = '', reason: str = ''): + """Record retry attempt""" + if not self.meter: + return + + attributes = {'agent_name': agent_name, 'reason': reason} + self.retry_counter.add(1, attributes) + + def record_error(self, agent_name: str = '', error_type: str = ''): + """Record agent error""" + if not self.meter: + return + + attributes = {'agent_name': agent_name, 'error_type': error_type} + self.error_counter.add(1, attributes) + + def record_latency( + self, duration_ms: float, agent_name: str = '', agent_type: str = '' + ): + """Record execution latency""" + if not self.meter: + return + + attributes = {'agent_name': agent_name, 'agent_type': agent_type} + self.latency_histogram.record(duration_ms, attributes) + + +class WorkflowMetrics: + """Metrics for Arium workflow operations""" + + def __init__(self): + self.meter = get_meter() + if self.meter: + # Workflow counters + self.workflow_counter = self.meter.create_counter( + name='workflow.executions.total', + description='Total number of workflow executions', + unit='executions', + ) + + self.node_counter = self.meter.create_counter( + name='workflow.nodes.executed', + description='Total number of nodes executed', + unit='nodes', + ) + + self.error_counter = self.meter.create_counter( + name='workflow.errors.total', + description='Total number of workflow errors', + unit='errors', + ) + + # Latency histograms + self.workflow_latency = self.meter.create_histogram( + name='workflow.execution.duration', + description='Duration of workflow executions', + unit='ms', + ) + + self.node_latency = self.meter.create_histogram( + name='workflow.node.duration', + description='Duration of node executions', + unit='ms', + ) + + def record_workflow(self, workflow_name: str = '', status: str = 'success'): + """Record workflow execution""" + if not self.meter: + return + + attributes = {'workflow_name': workflow_name, 'status': status} + self.workflow_counter.add(1, attributes) + + def record_node( + self, + workflow_name: str = '', + node_name: str = '', + node_type: str = '', + status: str = 'success', + ): + """Record node execution""" + if not self.meter: + return + + attributes = { + 'workflow_name': workflow_name, + 'node_name': node_name, + 'node_type': node_type, + 'status': status, + } + self.node_counter.add(1, attributes) + + def record_error(self, workflow_name: str = '', error_type: str = ''): + """Record workflow error""" + if not self.meter: + return + + attributes = {'workflow_name': workflow_name, 'error_type': error_type} + self.error_counter.add(1, attributes) + + def record_workflow_latency(self, duration_ms: float, workflow_name: str = ''): + """Record workflow latency""" + if not self.meter: + return + + attributes = {'workflow_name': workflow_name} + self.workflow_latency.record(duration_ms, attributes) + + def record_node_latency( + self, + duration_ms: float, + workflow_name: str = '', + node_name: str = '', + node_type: str = '', + ): + """Record node latency""" + if not self.meter: + return + + attributes = { + 'workflow_name': workflow_name, + 'node_name': node_name, + 'node_type': node_type, + } + self.node_latency.record(duration_ms, attributes) + + +# Global metric instances +llm_metrics = LLMMetrics() +agent_metrics = AgentMetrics() +workflow_metrics = WorkflowMetrics() + + +def trace_llm_call(provider: str = '', model: str = ''): + """ + Decorator to trace LLM API calls + + Args: + provider: LLM provider name (e.g., 'openai', 'anthropic', 'gemini') + model: Model name + + Example: + @trace_llm_call(provider="openai", model="gpt-4") + async def generate(self, messages): + ... + """ + + def decorator(func: Callable): + @wraps(func) + async def async_wrapper(*args, **kwargs): + tracer = get_tracer() + if not tracer: + return await func(*args, **kwargs) + + # Extract self to get instance attributes + self_arg = args[0] if args else None + actual_model = model or (getattr(self_arg, 'model', '') if self_arg else '') + actual_provider = provider or ( + self_arg.__class__.__name__ if self_arg else '' + ) + + with tracer.start_as_current_span( + f'llm.{actual_provider}.generate', + attributes={ + 'llm.provider': actual_provider, + 'llm.model': actual_model, + 'llm.temperature': getattr(self_arg, 'temperature', 0.0) + if self_arg + else 0.0, + }, + ) as span: + start_time = time.time() + try: + result = await func(*args, **kwargs) + + # Record success + duration_ms = (time.time() - start_time) * 1000 + llm_metrics.record_request(actual_model, actual_provider, 'success') + llm_metrics.record_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.OK)) + span.set_attribute('llm.response.received', True) + + return result + + except Exception as e: + # Record error + duration_ms = (time.time() - start_time) * 1000 + error_type = type(e).__name__ + + llm_metrics.record_request(actual_model, actual_provider, 'error') + llm_metrics.record_error(actual_model, actual_provider, error_type) + llm_metrics.record_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.ERROR, str(e))) + span.set_attribute('error.type', error_type) + span.set_attribute('error.message', str(e)) + + raise + + @wraps(func) + def sync_wrapper(*args, **kwargs): + tracer = get_tracer() + if not tracer: + return func(*args, **kwargs) + + self_arg = args[0] if args else None + actual_model = model or (getattr(self_arg, 'model', '') if self_arg else '') + actual_provider = provider or ( + self_arg.__class__.__name__ if self_arg else '' + ) + + with tracer.start_as_current_span( + f'llm.{actual_provider}.generate', + attributes={ + 'llm.provider': actual_provider, + 'llm.model': actual_model, + }, + ) as span: + start_time = time.time() + try: + result = func(*args, **kwargs) + + duration_ms = (time.time() - start_time) * 1000 + llm_metrics.record_request(actual_model, actual_provider, 'success') + llm_metrics.record_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.OK)) + return result + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + error_type = type(e).__name__ + + llm_metrics.record_request(actual_model, actual_provider, 'error') + llm_metrics.record_error(actual_model, actual_provider, error_type) + llm_metrics.record_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.ERROR, str(e))) + span.set_attribute('error.type', error_type) + + raise + + # Return appropriate wrapper based on function type + if asyncio.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + return decorator + + +def trace_llm_stream(provider: str = '', model: str = ''): + """ + Decorator to trace LLM streaming API calls + + Args: + provider: LLM provider name (e.g., 'openai', 'anthropic', 'gemini') + model: Model name + + Example: + @trace_llm_stream(provider="openai", model="gpt-4") + async def stream(self, messages): + ... + """ + + def decorator(func: Callable): + @wraps(func) + async def async_wrapper(*args, **kwargs): + tracer = get_tracer() + if not tracer: + async for chunk in func(*args, **kwargs): + yield chunk + return + + # Extract self to get instance attributes + self_arg = args[0] if args else None + actual_model = model or (getattr(self_arg, 'model', '') if self_arg else '') + actual_provider = provider or ( + self_arg.__class__.__name__ if self_arg else '' + ) + + with tracer.start_as_current_span( + f'llm.{actual_provider}.stream', + attributes={ + 'llm.provider': actual_provider, + 'llm.model': actual_model, + 'llm.temperature': getattr(self_arg, 'temperature', 0.0) + if self_arg + else 0.0, + 'llm.operation': 'stream', + }, + ) as span: + start_time = time.time() + chunk_count = 0 + try: + # Record stream start + llm_metrics.record_stream(actual_model, actual_provider, 'start') + + # Track the streaming response + async for chunk in func(*args, **kwargs): + chunk_count += 1 + yield chunk + + # Record success + duration_ms = (time.time() - start_time) * 1000 + llm_metrics.record_stream(actual_model, actual_provider, 'success') + llm_metrics.record_stream_chunks( + chunk_count, actual_model, actual_provider + ) + llm_metrics.record_stream_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.OK)) + span.set_attribute('llm.stream.chunks', chunk_count) + span.set_attribute('llm.stream.duration_ms', duration_ms) + span.set_attribute('llm.stream.completed', True) + + except Exception as e: + # Record error + duration_ms = (time.time() - start_time) * 1000 + error_type = type(e).__name__ + + llm_metrics.record_stream(actual_model, actual_provider, 'error') + llm_metrics.record_error(actual_model, actual_provider, error_type) + llm_metrics.record_stream_latency( + duration_ms, actual_model, actual_provider + ) + + span.set_status(Status(StatusCode.ERROR, str(e))) + span.set_attribute('error.type', error_type) + span.set_attribute('error.message', str(e)) + span.set_attribute('llm.stream.chunks', chunk_count) + span.set_attribute('llm.stream.duration_ms', duration_ms) + + raise + + return async_wrapper + + return decorator + + +def trace_agent_execution(agent_name: str = ''): + """ + Decorator to trace agent executions + + Args: + agent_name: Name of the agent + + Example: + @trace_agent_execution(agent_name="research_agent") + async def run(self, inputs): + ... + """ + + def decorator(func: Callable): + @wraps(func) + async def async_wrapper(*args, **kwargs): + tracer = get_tracer() + if not tracer: + return await func(*args, **kwargs) + + self_arg = args[0] if args else None + actual_agent_name = agent_name or ( + getattr(self_arg, 'name', '') if self_arg else '' + ) + agent_type = getattr(self_arg, 'agent_type', '') if self_arg else '' + + with tracer.start_as_current_span( + f'agent.{actual_agent_name}.run', + attributes={ + 'agent.name': actual_agent_name, + 'agent.type': str(agent_type), + }, + ) as span: + start_time = time.time() + try: + result = await func(*args, **kwargs) + + duration_ms = (time.time() - start_time) * 1000 + agent_metrics.record_execution( + actual_agent_name, str(agent_type), 'success' + ) + agent_metrics.record_latency( + duration_ms, actual_agent_name, str(agent_type) + ) + + span.set_status(Status(StatusCode.OK)) + span.set_attribute( + 'agent.result.length', len(str(result)) if result else 0 + ) + + return result + + except Exception as e: + duration_ms = (time.time() - start_time) * 1000 + error_type = type(e).__name__ + + agent_metrics.record_execution( + actual_agent_name, str(agent_type), 'error' + ) + agent_metrics.record_error(actual_agent_name, error_type) + agent_metrics.record_latency( + duration_ms, actual_agent_name, str(agent_type) + ) + + span.set_status(Status(StatusCode.ERROR, str(e))) + span.set_attribute('error.type', error_type) + + raise + + return async_wrapper + + return decorator + + +def add_span_attributes(span: Optional[Span], attributes: Dict[str, Any]) -> None: + """ + Helper to add attributes to a span safely + + Args: + span: OpenTelemetry span + attributes: Dictionary of attributes to add + """ + if span and attributes: + for key, value in attributes.items(): + # OpenTelemetry only supports certain types + if isinstance(value, (str, bool, int, float)): + span.set_attribute(key, value) + else: + span.set_attribute(key, str(value)) diff --git a/flo_ai/flo_ai/telemetry/telemetry.py b/flo_ai/flo_ai/telemetry/telemetry.py new file mode 100644 index 00000000..66c82054 --- /dev/null +++ b/flo_ai/flo_ai/telemetry/telemetry.py @@ -0,0 +1,228 @@ +""" +OpenTelemetry telemetry implementation for flo_ai framework +""" + +from typing import Optional, Dict, Any +from opentelemetry import trace, metrics +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import ( + PeriodicExportingMetricReader, + ConsoleMetricExporter, +) +from opentelemetry.sdk.resources import Resource +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter +import os + + +class FloTelemetry: + """ + Central telemetry configuration for flo_ai framework. + + Provides OpenTelemetry integration with tracing and metrics support. + """ + + _instance: Optional['FloTelemetry'] = None + _initialized: bool = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super(FloTelemetry, cls).__new__(cls) + return cls._instance + + def __init__(self): + if not FloTelemetry._initialized: + self.tracer_provider: Optional[TracerProvider] = None + self.meter_provider: Optional[MeterProvider] = None + self.tracer: Optional[trace.Tracer] = None + self.meter: Optional[metrics.Meter] = None + FloTelemetry._initialized = True + + def configure( + self, + service_name: str = 'flo_ai', + service_version: str = '1.0.0', + environment: str = 'development', + otlp_endpoint: Optional[str] = None, + console_export: bool = False, + additional_attributes: Optional[Dict[str, Any]] = None, + ) -> None: + """ + Configure OpenTelemetry for the flo_ai framework. + + Args: + service_name: Name of the service for telemetry + service_version: Version of the service + environment: Environment (development, production, etc.) + otlp_endpoint: OTLP endpoint for exporting telemetry (e.g., http://localhost:4317) + console_export: Whether to export to console for debugging + additional_attributes: Additional resource attributes + """ + # Create resource with service information + resource_attrs = { + 'service.name': service_name, + 'service.version': service_version, + 'deployment.environment': environment, + } + + if additional_attributes: + resource_attrs.update(additional_attributes) + + resource = Resource.create(resource_attrs) + + # Configure tracing + self.tracer_provider = TracerProvider(resource=resource) + + # Add span processors + if console_export: + console_processor = BatchSpanProcessor(ConsoleSpanExporter()) + self.tracer_provider.add_span_processor(console_processor) + + # Add OTLP exporter if endpoint is provided + if otlp_endpoint: + otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True) + otlp_processor = BatchSpanProcessor(otlp_exporter) + self.tracer_provider.add_span_processor(otlp_processor) + + # Set the tracer provider + trace.set_tracer_provider(self.tracer_provider) + self.tracer = trace.get_tracer(__name__) + + # Configure metrics + metric_readers = [] + + if console_export: + console_reader = PeriodicExportingMetricReader( + ConsoleMetricExporter(), export_interval_millis=5000 + ) + metric_readers.append(console_reader) + + if otlp_endpoint: + otlp_metric_exporter = OTLPMetricExporter( + endpoint=otlp_endpoint, insecure=True + ) + otlp_reader = PeriodicExportingMetricReader( + otlp_metric_exporter, export_interval_millis=5000 + ) + metric_readers.append(otlp_reader) + + if metric_readers: + self.meter_provider = MeterProvider( + resource=resource, + metric_readers=metric_readers, + ) + metrics.set_meter_provider(self.meter_provider) + self.meter = metrics.get_meter(__name__) + + def get_tracer(self) -> Optional[trace.Tracer]: + """Get the configured tracer instance""" + return self.tracer + + def get_meter(self) -> Optional[metrics.Meter]: + """Get the configured meter instance""" + return self.meter + + def shutdown(self) -> None: + """Shutdown telemetry providers and flush data""" + if self.tracer_provider: + self.tracer_provider.shutdown() + if self.meter_provider: + self.meter_provider.shutdown() + + +# Global telemetry instance +_global_telemetry = FloTelemetry() + + +def configure_telemetry( + service_name: str = 'flo_ai', + service_version: str = '1.0.0', + environment: str = None, + otlp_endpoint: str = None, + console_export: bool = False, + additional_attributes: Optional[Dict[str, Any]] = None, +) -> None: + """ + Configure OpenTelemetry for flo_ai. + + Args: + service_name: Name of the service + service_version: Version of the service + environment: Environment (defaults to FLO_ENV or 'development') + otlp_endpoint: OTLP endpoint (defaults to FLO_OTLP_ENDPOINT or None) + console_export: Export to console for debugging + additional_attributes: Additional resource attributes + + Example: + >>> from flo_ai.telemetry import configure_telemetry + >>> configure_telemetry( + ... service_name="my_ai_app", + ... otlp_endpoint="http://localhost:4317", + ... console_export=True + ... ) + """ + # Use environment variables as defaults + if environment is None: + environment = os.getenv('FLO_ENV', 'development') + + if otlp_endpoint is None: + otlp_endpoint = os.getenv('FLO_OTLP_ENDPOINT') + + _global_telemetry.configure( + service_name=service_name, + service_version=service_version, + environment=environment, + otlp_endpoint=otlp_endpoint, + console_export=console_export, + additional_attributes=additional_attributes, + ) + + +def get_tracer() -> Optional[trace.Tracer]: + """ + Get the global tracer instance. + + Returns: + Tracer instance or None if not configured + + Example: + >>> from flo_ai.telemetry import get_tracer + >>> tracer = get_tracer() + >>> if tracer: + ... with tracer.start_as_current_span("my_operation"): + ... # Your code here + ... pass + """ + return _global_telemetry.get_tracer() + + +def get_meter() -> Optional[metrics.Meter]: + """ + Get the global meter instance. + + Returns: + Meter instance or None if not configured + + Example: + >>> from flo_ai.telemetry import get_meter + >>> meter = get_meter() + >>> if meter: + ... counter = meter.create_counter("operations_total") + ... counter.add(1, {"operation": "inference"}) + """ + return _global_telemetry.get_meter() + + +def shutdown_telemetry() -> None: + """ + Shutdown telemetry and flush all data. + + Call this at the end of your application lifecycle. + + Example: + >>> from flo_ai.telemetry import shutdown_telemetry + >>> shutdown_telemetry() + """ + _global_telemetry.shutdown() diff --git a/flo_ai/poetry.lock b/flo_ai/poetry.lock index 1021e07f..7ee51314 100644 --- a/flo_ai/poetry.lock +++ b/flo_ai/poetry.lock @@ -819,6 +819,24 @@ files = [ {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, ] +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + [[package]] name = "distlib" version = "0.3.9" @@ -1640,6 +1658,30 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.5.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + [[package]] name = "iniconfig" version = "2.1.0" @@ -2519,6 +2561,161 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] +[[package]] +name = "opentelemetry-api" +version = "1.28.2" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.28.2-py3-none-any.whl", hash = "sha256:6fcec89e265beb258fe6b1acaaa3c8c705a934bd977b9f534a2b7c0d2d4275a6"}, + {file = "opentelemetry_api-1.28.2.tar.gz", hash = "sha256:ecdc70c7139f17f9b0cf3742d57d7020e3e8315d6cffcdf1a12a905d45b19cc0"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<=8.5.0" + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.28.2" +description = "OpenTelemetry Collector Exporters" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp-1.28.2-py3-none-any.whl", hash = "sha256:b50f6d4a80e6bcd329e36f360ac486ecfa106ea704d6226ceea05d3a48455f70"}, + {file = "opentelemetry_exporter_otlp-1.28.2.tar.gz", hash = "sha256:45f8d7fe4cdd41526464b542ce91b1fd1ae661be92d2c6cba71a3d948b2bdf70"}, +] + +[package.dependencies] +opentelemetry-exporter-otlp-proto-grpc = "1.28.2" +opentelemetry-exporter-otlp-proto-http = "1.28.2" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.28.2" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.28.2-py3-none-any.whl", hash = "sha256:545b1943b574f666c35b3d6cc67cb0b111060727e93a1e2866e346b33bff2a12"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.28.2.tar.gz", hash = "sha256:7aebaa5fc9ff6029374546df1f3a62616fda07fccd9c6a8b7892ec130dd8baca"}, +] + +[package.dependencies] +opentelemetry-proto = "1.28.2" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.28.2" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.28.2-py3-none-any.whl", hash = "sha256:6083d9300863aab35bfce7c172d5fc1007686e6f8dff366eae460cd9a21592e2"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.28.2.tar.gz", hash = "sha256:07c10378380bbb01a7f621a5ce833fc1fab816e971140cd3ea1cd587840bc0e6"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +grpcio = ">=1.63.2,<2.0.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.28.2" +opentelemetry-proto = "1.28.2" +opentelemetry-sdk = ">=1.28.2,<1.29.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.28.2" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.28.2-py3-none-any.whl", hash = "sha256:af921c18212a56ef4be68458ba475791c0517ebfd8a2ff04669c9cd477d90ff2"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.28.2.tar.gz", hash = "sha256:d9b353d67217f091aaf4cfe8693c170973bb3e90a558992570d97020618fda79"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.28.2" +opentelemetry-proto = "1.28.2" +opentelemetry-sdk = ">=1.28.2,<1.29.0" +requests = ">=2.7,<3.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.49b2" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation-0.49b2-py3-none-any.whl", hash = "sha256:f6d782b0ef9fef4a4c745298651c65f5c532c34cd4c40d230ab5b9f3b3b4d151"}, + {file = "opentelemetry_instrumentation-0.49b2.tar.gz", hash = "sha256:8cf00cc8d9d479e4b72adb9bd267ec544308c602b7188598db5a687e77b298e2"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +opentelemetry-semantic-conventions = "0.49b2" +packaging = ">=18.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-proto" +version = "1.28.2" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_proto-1.28.2-py3-none-any.whl", hash = "sha256:0837498f59db55086462915e5898d0b1a18c1392f6db4d7e937143072a72370c"}, + {file = "opentelemetry_proto-1.28.2.tar.gz", hash = "sha256:7c0d125a6b71af88bfeeda16bfdd0ff63dc2cf0039baf6f49fa133b203e3f566"}, +] + +[package.dependencies] +protobuf = ">=5.0,<6.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.28.2" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.28.2-py3-none-any.whl", hash = "sha256:93336c129556f1e3ccd21442b94d3521759541521861b2214c499571b85cb71b"}, + {file = "opentelemetry_sdk-1.28.2.tar.gz", hash = "sha256:5fed24c5497e10df30282456fe2910f83377797511de07d14cec0d3e0a1a3110"}, +] + +[package.dependencies] +opentelemetry-api = "1.28.2" +opentelemetry-semantic-conventions = "0.49b2" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.49b2" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.49b2-py3-none-any.whl", hash = "sha256:51e7e1d0daa958782b6c2a8ed05e5f0e7dd0716fc327ac058777b8659649ee54"}, + {file = "opentelemetry_semantic_conventions-0.49b2.tar.gz", hash = "sha256:44e32ce6a5bb8d7c0c617f84b9dc1c8deda1045a07dc16a688cc7cbeab679997"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +opentelemetry-api = "1.28.2" + [[package]] name = "packaging" version = "24.2" @@ -4409,6 +4606,97 @@ files = [ beautifulsoup4 = "*" requests = ">=2.0.0,<3.0.0" +[[package]] +name = "wrapt" +version = "1.17.3" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + [[package]] name = "yarl" version = "1.20.1" @@ -4528,10 +4816,30 @@ idna = ">=2.0" multidict = ">=4.0" propcache = ">=0.2.1" +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [extras] vizualize = ["matplotlib", "networkx"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "16ddcd337f56cbd3a241afbbab9c69baaaff08fea81c81e5ff2ba8a13d638156" +content-hash = "fb38f83474b0037eff56fe33bca274303f8b0e96e528cba3776bc74ec8181d48" diff --git a/flo_ai/pyproject.toml b/flo_ai/pyproject.toml index fb8ea04a..5bc38a9d 100644 --- a/flo_ai/pyproject.toml +++ b/flo_ai/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "flo_ai" -version = "1.0.7-rc2" +version = "1.0.7-rc5" description = "A easy way to create structured AI agents" authors = ["rootflo <*@rootflo.ai>"] license = "MIT" @@ -24,6 +24,10 @@ google-cloud-aiplatform = "^1.109.0" pypdf = "^4.2.0" pymupdf4llm = "^0.0.17" chardet = ">=3.dev0,<4.dev0" +opentelemetry-api = "^1.28.2" +opentelemetry-sdk = "^1.28.2" +opentelemetry-exporter-otlp = "^1.28.2" +opentelemetry-instrumentation = "^0.49b2" [tool.poetry.extras] vizualize = ["matplotlib", "networkx"] diff --git a/flo_ai/setup.py b/flo_ai/setup.py index 1f0aebe6..fda02f7c 100644 --- a/flo_ai/setup.py +++ b/flo_ai/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name='flo-ai', - version='1.0.7-rc2', + version='1.0.7-rc5', author='Rootflo', description='Create composable AI agents', long_description=long_description, diff --git a/flo_ai/tests/test_anthropic_llm.py b/flo_ai/tests/test_anthropic_llm.py index 657c08b2..0d7bf701 100644 --- a/flo_ai/tests/test_anthropic_llm.py +++ b/flo_ai/tests/test_anthropic_llm.py @@ -86,7 +86,12 @@ async def test_anthropic_generate_basic(self): mock_content.text = 'Hello, world!' mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() @@ -116,7 +121,12 @@ async def test_anthropic_generate_with_system_message(self): mock_content.text = "I'm a helpful assistant" mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() @@ -153,7 +163,12 @@ async def test_anthropic_generate_with_output_schema(self): mock_content.text = '{"message": "test", "count": 42}' mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() @@ -188,7 +203,12 @@ async def test_anthropic_generate_with_functions(self): mock_content.text = "I'll use the function" mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() @@ -216,7 +236,12 @@ async def test_anthropic_generate_with_tool_use(self): mock_text_content.text = 'I used the tool' mock_text_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_text_content, mock_tool_content] llm.client = Mock() @@ -240,7 +265,12 @@ async def test_anthropic_generate_with_max_tokens(self): mock_content.text = 'Response with max tokens' mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() @@ -263,7 +293,12 @@ async def test_anthropic_generate_with_kwargs(self): mock_content.text = 'Response with kwargs' mock_content.type = 'text' + mock_usage = Mock() + mock_usage.input_tokens = 5 + mock_usage.output_tokens = 4 + mock_response = Mock() + mock_response.usage = mock_usage mock_response.content = [mock_content] llm.client = Mock() diff --git a/flo_ai/tests/test_gemini_llm.py b/flo_ai/tests/test_gemini_llm.py index 6c53976f..7143a374 100644 --- a/flo_ai/tests/test_gemini_llm.py +++ b/flo_ai/tests/test_gemini_llm.py @@ -418,15 +418,6 @@ def test_gemini_format_image_in_message_unsupported(self): ): llm.format_image_in_message(image) - # Test with image_base64 (not implemented) - image = ImageMessage(image_base64='base64_string') - - with pytest.raises( - NotImplementedError, - match='Not other way other than file path has been implemented', - ): - llm.format_image_in_message(image) - @pytest.mark.asyncio async def test_gemini_generate_error_handling(self): """Test error handling in generate method."""