diff --git a/.github/workflows/agent-evaluation.yml b/.github/workflows/agent-evaluation.yml
new file mode 100644
index 000000000..34b554cbc
--- /dev/null
+++ b/.github/workflows/agent-evaluation.yml
@@ -0,0 +1,190 @@
+name: Agent Evaluation
+
+on:
+ # Run on PR when agent/evaluation code changes
+ pull_request:
+ paths:
+ - 'agentic_ai/agents/**'
+ - 'agentic_ai/evaluations/**'
+
+ # Allow manual trigger
+ workflow_dispatch:
+ inputs:
+ environment:
+ description: Target environment
+ type: choice
+ options: [dev, integration]
+ default: dev
+ agent_name:
+ description: 'Agent name for evaluation tracking'
+ type: string
+ default: 'ci-agent'
+ limit:
+ description: 'Limit number of test cases (0 = all)'
+ type: number
+ default: 5
+ eval_type:
+ description: 'Evaluation type'
+ type: choice
+ options: [all, single-turn-only, multi-turn-only]
+ default: all
+ push_to_foundry:
+ description: 'Push results to Azure AI Foundry'
+ type: boolean
+ default: false
+
+ # Callable from other workflows
+ workflow_call:
+ inputs:
+ environment:
+ type: string
+ required: false
+ default: 'dev'
+ backend_endpoint:
+ type: string
+ required: true
+ description: 'Backend API endpoint URL'
+ mcp_endpoint:
+ type: string
+ required: true
+ description: 'MCP service endpoint URL'
+ agent_name:
+ type: string
+ required: false
+ default: 'ci-agent'
+ limit:
+ type: number
+ required: false
+ default: 0
+ push_to_foundry:
+ type: boolean
+ required: false
+ default: false
+
+env:
+ PYTHON_VERSION: '3.12'
+
+jobs:
+ # ============================================================================
+ # Evaluation - Run agent evaluation against test scenarios
+ # ============================================================================
+ evaluate:
+ name: Agent Evaluation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ id-token: write # For OIDC authentication
+
+ environment: ${{ inputs.environment || 'dev' }}
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ cache: 'pip'
+
+ - name: Install uv
+ run: |
+ curl -LsSf https://astral.sh/uv/install.sh | sh
+ echo "$HOME/.cargo/bin" >> $GITHUB_PATH
+
+ - name: Install dependencies
+ run: |
+ cd agentic_ai/applications
+ uv sync
+
+ - name: Azure Login (OIDC)
+ if: ${{ inputs.push_to_foundry == true }}
+ uses: azure/login@v2
+ with:
+ client-id: ${{ vars.AZURE_CLIENT_ID }}
+ tenant-id: ${{ vars.AZURE_TENANT_ID }}
+ subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
+
+ - name: Get Azure credentials from Key Vault
+ if: ${{ inputs.push_to_foundry == true }}
+ run: |
+ KEYVAULT_NAME="${{ vars.KEYVAULT_NAME }}"
+
+ if [ -n "$KEYVAULT_NAME" ]; then
+ AOAI_KEY=$(az keyvault secret show --vault-name "$KEYVAULT_NAME" --name "aoai-key" --query value -o tsv 2>/dev/null || echo "")
+ echo "::add-mask::$AOAI_KEY"
+ echo "AZURE_OPENAI_API_KEY=$AOAI_KEY" >> $GITHUB_ENV
+
+ AI_PROJECT_ENDPOINT=$(az keyvault secret show --vault-name "$KEYVAULT_NAME" --name "ai-project-endpoint" --query value -o tsv 2>/dev/null || echo "")
+ echo "AZURE_AI_PROJECT_ENDPOINT=$AI_PROJECT_ENDPOINT" >> $GITHUB_ENV
+ fi
+
+ - name: Run Agent Evaluation
+ run: |
+ cd agentic_ai/applications
+
+ # Build command
+ CMD="uv run python ../evaluations/run_agent_eval.py"
+ CMD="$CMD --agent ${{ inputs.agent_name || 'ci-agent' }}"
+ CMD="$CMD --backend-url ${{ inputs.backend_endpoint || 'http://localhost:7000' }}"
+
+ # Add limit if specified
+ if [ "${{ inputs.limit }}" != "0" ] && [ -n "${{ inputs.limit }}" ]; then
+ CMD="$CMD --limit ${{ inputs.limit }}"
+ fi
+
+ # Add eval type filter
+ if [ "${{ inputs.eval_type }}" == "single-turn-only" ]; then
+ CMD="$CMD --single-turn-only"
+ elif [ "${{ inputs.eval_type }}" == "multi-turn-only" ]; then
+ CMD="$CMD --multi-turn-only"
+ fi
+
+ # Add remote flag if pushing to Foundry
+ if [ "${{ inputs.push_to_foundry }}" == "true" ]; then
+ CMD="$CMD --remote"
+ else
+ CMD="$CMD --local"
+ fi
+
+ echo "Running: $CMD"
+ $CMD
+ env:
+ AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }}
+ AZURE_OPENAI_CHAT_DEPLOYMENT: ${{ vars.AZURE_OPENAI_DEPLOYMENT }}
+ AZURE_OPENAI_API_VERSION: '2025-03-01-preview'
+ MCP_SERVER_URI: ${{ inputs.mcp_endpoint || 'http://localhost:8000/mcp' }}
+
+ - name: Upload evaluation results
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: evaluation-results
+ path: |
+ agentic_ai/evaluations/eval_results/
+ agentic_ai/evaluations/evaluation_input_data.jsonl
+ retention-days: 30
+
+ - name: Generate Summary
+ if: always()
+ run: |
+ echo "## π Agent Evaluation Results" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Agent | ${{ inputs.agent_name || 'ci-agent' }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Environment | ${{ inputs.environment || 'dev' }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Eval Type | ${{ inputs.eval_type || 'all' }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Test Limit | ${{ inputs.limit || 'all' }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| Push to Foundry | ${{ inputs.push_to_foundry || 'false' }} |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Metrics Evaluated" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Single-Turn (tool-focused):**" >> $GITHUB_STEP_SUMMARY
+ echo "- Tool behavior (recall, precision, efficiency)" >> $GITHUB_STEP_SUMMARY
+ echo "- Completeness, response quality, grounded accuracy" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "**Multi-Turn (outcome-focused):**" >> $GITHUB_STEP_SUMMARY
+ echo "- Solution accuracy, task adherence, intent resolution" >> $GITHUB_STEP_SUMMARY
+ echo "- Coherence, fluency, relevance" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "π See artifacts for detailed results" >> $GITHUB_STEP_SUMMARY
diff --git a/README.md b/README.md
index 512be46e1..46a587aa4 100644
--- a/README.md
+++ b/README.md
@@ -32,10 +32,11 @@ Welcome to the official repository for the Microsoft AI Agentic Workshop! This r
## Key Features
- **[Microsoft Agent Framework](https://github.com/microsoft/agent-framework) Integration** - Single-agent, multi-agent Magentic orchestration, and handoff-based domain routing with MCP tools. [Pattern guide β](agentic_ai/agents/agent_framework/README.md)
-- **[Workflow Orchestration](agentic_ai/workflow/)** - Pregel-style execution, checkpointing, human-in-the-loop patterns, and real-time observability. [Fraud Detection Demo β](agentic_ai/workflow/fraud_detection/)
-- **Advanced UI Options** - React frontend with streaming visualization or Streamlit for quick prototyping
+- **[Workflow Orchestration](agentic_ai/workflow/)** - Hybrid Workflow + Durable Task architecture with fan-out/fan-in topology, human-in-the-loop, and real-time observability. [Fraud Detection Demo β](agentic_ai/workflow/fraud_detection_durable/)
+- **[Observability with Application Insights](agentic_ai/observability/)** - Full tracing of agent executions, tool calls, and LLM invocations with pre-built Grafana dashboards. [Setup Guide β](agentic_ai/observability/README.md)
+- **Advanced UI Options** - React frontend with interactive workflow visualization and step-by-step tool call details
- **[MCP Server Integration](mcp/)** - Model Context Protocol for enhanced agent tool capabilities with advanced features: authentication, RBAC, and APIM integration
-- **[Emerging Agentic Scenarios](agentic_ai/scenarios/)** - Long-running workflows, progress updates, and durable agent patterns
+- **[Agent Evaluations](agentic_ai/evaluations/)** - Evaluate agent performance with custom metrics and test datasets
- **Agent State & History Persistence** - In-memory or CosmosDB backend for conversation history and agent state
- **[Enterprise-Ready Reference Architecture](infra/README.md)** - Production-grade deployment with VNet integration, private endpoints, managed identity, Terraform/Bicep IaC, and GitHub Actions CI/CD
@@ -46,7 +47,7 @@ Welcome to the official repository for the Microsoft AI Agentic Workshop! This r
1. Review the [Setup Instructions](./SETUP.md) for environment prerequisites and step-by-step installation.
2. Explore the [Business Scenario and Agent Design](./SCENARIO.md) to understand the workshop challenge.
3. Check out the **[Agent Framework Implementation Patterns](agentic_ai/agents/agent_framework/README.md)** to choose the right multi-agent approach (single-agent, Magentic orchestration, or handoff pattern).
-4. Try the **[Fraud Detection Workflow Demo](agentic_ai/workflow/fraud_detection/)** to see enterprise orchestration patterns in action.
+4. Try the **[Durable Fraud Detection Workflow](agentic_ai/workflow/fraud_detection_durable/)** to see hybrid Workflow + Durable Task orchestration with human-in-the-loop.
5. Dive into [System Architecture](./ARCHITECTURE.md) before building and customizing your agent solutions.
6. Utilize the [Support Guide](./SUPPORT.md) for troubleshooting and assistance.
diff --git a/agentic_ai/agents/agent_framework/__init__.py b/agentic_ai/agents/agent_framework/__init__.py
new file mode 100644
index 000000000..7a035dac1
--- /dev/null
+++ b/agentic_ai/agents/agent_framework/__init__.py
@@ -0,0 +1,6 @@
+# Agent Framework module
+# This package contains agent implementations built using the agent_framework SDK
+
+from .single_agent import Agent
+
+__all__ = ["Agent"]
diff --git a/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md b/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md
deleted file mode 100644
index ff01f20b1..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md
+++ /dev/null
@@ -1,634 +0,0 @@
-# Integration Guide: Workflow Reflection Agent
-
-This guide shows how to integrate the workflow-based reflection agent into your existing application.
-
-## Quick Start
-
-### 1. Import the Agent
-
-```python
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-```
-
-### 2. Basic Integration
-
-Replace your existing reflection agent import:
-
-```python
-# OLD: Traditional reflection agent
-# from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent
-
-# NEW: Workflow-based reflection agent
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-```
-
-### 3. Use Same Interface
-
-The workflow agent implements the same `BaseAgent` interface:
-
-```python
-# Create agent instance
-state_store = {}
-session_id = "user_123"
-agent = Agent(state_store=state_store, session_id=session_id)
-
-# Optional: Set WebSocket manager for streaming
-agent.set_websocket_manager(ws_manager)
-
-# Chat with user
-response = await agent.chat_async("Help me with billing for customer 1")
-```
-
-## Backend Integration (FastAPI/Flask)
-
-### Example: FastAPI Backend
-
-```python
-from fastapi import FastAPI, WebSocket
-from typing import Dict, Any
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-app = FastAPI()
-
-# Global state store (in production, use Redis or database)
-state_store: Dict[str, Any] = {}
-
-@app.post("/chat")
-async def chat_endpoint(
- session_id: str,
- message: str,
- use_workflow: bool = True # Toggle between traditional and workflow
-):
- """
- Chat endpoint with workflow reflection agent.
- """
-
- if use_workflow:
- from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
- else:
- from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent
-
- # Create agent
- agent = Agent(state_store=state_store, session_id=session_id)
-
- # Process message
- response = await agent.chat_async(message)
-
- return {
- "session_id": session_id,
- "response": response,
- "agent_type": "workflow" if use_workflow else "traditional"
- }
-
-
-@app.websocket("/ws/{session_id}")
-async def websocket_endpoint(websocket: WebSocket, session_id: str):
- """
- WebSocket endpoint for streaming support.
- """
- await websocket.accept()
-
- # Create WebSocket manager (simplified)
- class WSManager:
- async def broadcast(self, sid: str, message: dict):
- if sid == session_id:
- await websocket.send_json(message)
-
- ws_manager = WSManager()
-
- try:
- while True:
- # Receive message from client
- data = await websocket.receive_json()
- message = data.get("message", "")
-
- # Create agent with streaming support
- agent = Agent(state_store=state_store, session_id=session_id)
- agent.set_websocket_manager(ws_manager)
-
- # Process message (will stream updates via WebSocket)
- response = await agent.chat_async(message)
-
- # Send final confirmation
- await websocket.send_json({
- "type": "complete",
- "response": response
- })
-
- except Exception as e:
- print(f"WebSocket error: {e}")
- finally:
- await websocket.close()
-```
-
-## Frontend Integration
-
-### JavaScript/TypeScript Client
-
-```typescript
-interface ChatMessage {
- role: 'user' | 'assistant';
- content: string;
-}
-
-interface StreamEvent {
- type: 'orchestrator' | 'agent_start' | 'agent_token' | 'agent_message' | 'final_result';
- agent_id?: string;
- content?: string;
- kind?: 'plan' | 'progress' | 'result';
-}
-
-class WorkflowReflectionClient {
- private ws: WebSocket;
- private sessionId: string;
-
- constructor(sessionId: string) {
- this.sessionId = sessionId;
- this.ws = new WebSocket(`ws://localhost:8000/ws/${sessionId}`);
- this.setupEventHandlers();
- }
-
- private setupEventHandlers() {
- this.ws.onmessage = (event) => {
- const data: StreamEvent = JSON.parse(event.data);
- this.handleStreamEvent(data);
- };
- }
-
- private handleStreamEvent(event: StreamEvent) {
- switch (event.type) {
- case 'orchestrator':
- this.updateOrchestrator(event.kind!, event.content!);
- break;
-
- case 'agent_start':
- this.showAgentBadge(event.agent_id!);
- break;
-
- case 'agent_token':
- this.appendToken(event.agent_id!, event.content!);
- break;
-
- case 'agent_message':
- this.finalizeAgentMessage(event.agent_id!, event.content!);
- break;
-
- case 'final_result':
- this.displayFinalResponse(event.content!);
- break;
- }
- }
-
- private updateOrchestrator(kind: string, content: string) {
- const container = document.getElementById('orchestrator-status');
- if (container) {
- container.innerHTML = `
-
-
${kind.toUpperCase()}
-
${content}
-
- `;
- }
- }
-
- private showAgentBadge(agentId: string) {
- const badge = document.createElement('div');
- badge.className = `agent-badge ${agentId}`;
- badge.textContent = agentId.replace('_', ' ').toUpperCase();
- document.getElementById('agent-container')?.appendChild(badge);
- }
-
- private appendToken(agentId: string, token: string) {
- const messageDiv = document.getElementById(`message-${agentId}`)
- || this.createMessageDiv(agentId);
- messageDiv.textContent += token;
- }
-
- private createMessageDiv(agentId: string): HTMLDivElement {
- const div = document.createElement('div');
- div.id = `message-${agentId}`;
- div.className = 'agent-message streaming';
- document.getElementById('messages-container')?.appendChild(div);
- return div;
- }
-
- private finalizeAgentMessage(agentId: string, content: string) {
- const messageDiv = document.getElementById(`message-${agentId}`);
- if (messageDiv) {
- messageDiv.classList.remove('streaming');
- messageDiv.classList.add('complete');
- }
- }
-
- private displayFinalResponse(content: string) {
- const responseDiv = document.createElement('div');
- responseDiv.className = 'final-response';
- responseDiv.innerHTML = `
-
-
Assistant:
-
${content}
-
- `;
- document.getElementById('chat-container')?.appendChild(responseDiv);
- }
-
- public sendMessage(message: string) {
- this.ws.send(JSON.stringify({ message }));
- }
-}
-
-// Usage
-const client = new WorkflowReflectionClient('user_session_123');
-client.sendMessage('What is the billing status for customer 1?');
-```
-
-### React Component
-
-```tsx
-import React, { useState, useEffect, useCallback } from 'react';
-
-interface StreamEvent {
- type: string;
- agent_id?: string;
- content?: string;
- kind?: string;
-}
-
-const WorkflowReflectionChat: React.FC<{ sessionId: string }> = ({ sessionId }) => {
- const [messages, setMessages] = useState>([]);
- const [orchestratorStatus, setOrchestratorStatus] = useState('');
- const [activeAgents, setActiveAgents] = useState>(new Set());
- const [ws, setWs] = useState(null);
-
- useEffect(() => {
- const websocket = new WebSocket(`ws://localhost:8000/ws/${sessionId}`);
-
- websocket.onmessage = (event) => {
- const data: StreamEvent = JSON.parse(event.data);
- handleStreamEvent(data);
- };
-
- setWs(websocket);
-
- return () => {
- websocket.close();
- };
- }, [sessionId]);
-
- const handleStreamEvent = (event: StreamEvent) => {
- switch (event.type) {
- case 'orchestrator':
- setOrchestratorStatus(event.content || '');
- break;
-
- case 'agent_start':
- setActiveAgents(prev => new Set(prev).add(event.agent_id!));
- break;
-
- case 'final_result':
- setMessages(prev => [...prev, { role: 'assistant', content: event.content! }]);
- setActiveAgents(new Set());
- break;
- }
- };
-
- const sendMessage = useCallback((message: string) => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify({ message }));
- setMessages(prev => [...prev, { role: 'user', content: message }]);
- }
- }, [ws]);
-
- return (
-
-
- {orchestratorStatus && (
-
- {orchestratorStatus}
-
- )}
-
-
-
- {Array.from(activeAgents).map(agentId => (
-
- {agentId.replace('_', ' ')}
-
- ))}
-
-
-
- {messages.map((msg, idx) => (
-
-
{msg.role}:
-
{msg.content}
-
- ))}
-
-
-
-
- );
-};
-```
-
-## Streamlit Integration
-
-```python
-import streamlit as st
-import asyncio
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-# Initialize session state
-if 'state_store' not in st.session_state:
- st.session_state.state_store = {}
-if 'session_id' not in st.session_state:
- st.session_state.session_id = "streamlit_session"
-
-# Create agent
-@st.cache_resource
-def get_agent():
- return Agent(
- state_store=st.session_state.state_store,
- session_id=st.session_state.session_id
- )
-
-# UI
-st.title("Workflow Reflection Agent Chat")
-
-# Display chat history
-chat_history = st.session_state.state_store.get(
- f"{st.session_state.session_id}_chat_history", []
-)
-
-for msg in chat_history:
- with st.chat_message(msg["role"]):
- st.write(msg["content"])
-
-# Chat input
-if prompt := st.chat_input("Ask me anything..."):
- # Display user message
- with st.chat_message("user"):
- st.write(prompt)
-
- # Get agent response
- agent = get_agent()
-
- # Show processing indicator
- with st.spinner("Processing with workflow reflection..."):
- response = asyncio.run(agent.chat_async(prompt))
-
- # Display assistant response
- with st.chat_message("assistant"):
- st.write(response)
-
- # Rerun to update chat history
- st.rerun()
-```
-
-## Configuration Management
-
-### Environment Configuration
-
-Create a `.env` file:
-
-```bash
-# Azure OpenAI Configuration
-AZURE_OPENAI_API_KEY=your_api_key_here
-AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4
-AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
-AZURE_OPENAI_API_VERSION=2024-02-15-preview
-OPENAI_MODEL_NAME=gpt-4
-
-# Optional: MCP Server
-MCP_SERVER_URI=http://localhost:5000/mcp
-```
-
-### Dynamic Agent Selection
-
-```python
-from typing import Literal
-from agentic_ai.agents.base_agent import BaseAgent
-
-AgentType = Literal["workflow", "traditional"]
-
-def create_agent(
- agent_type: AgentType,
- state_store: dict,
- session_id: str,
- **kwargs
-) -> BaseAgent:
- """
- Factory function to create the appropriate agent type.
- """
- if agent_type == "workflow":
- from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
- elif agent_type == "traditional":
- from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent
- else:
- raise ValueError(f"Unknown agent type: {agent_type}")
-
- return Agent(state_store=state_store, session_id=session_id, **kwargs)
-
-# Usage
-agent = create_agent(
- agent_type="workflow", # or "traditional"
- state_store=state_store,
- session_id=session_id,
- access_token=access_token
-)
-```
-
-## Monitoring and Logging
-
-### Enhanced Logging
-
-```python
-import logging
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-# Configure detailed logging
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- handlers=[
- logging.FileHandler('workflow_agent.log'),
- logging.StreamHandler()
- ]
-)
-
-# Create agent
-agent = Agent(state_store=state_store, session_id=session_id)
-
-# Use agent (logs will capture all workflow steps)
-response = await agent.chat_async("Help me")
-```
-
-### Metrics Collection
-
-```python
-import time
-from dataclasses import dataclass
-from typing import List
-
-@dataclass
-class WorkflowMetrics:
- session_id: str
- request_id: str
- start_time: float
- end_time: float
- refinement_count: int
- approved: bool
-
- @property
- def duration(self) -> float:
- return self.end_time - self.start_time
-
-class MetricsCollector:
- def __init__(self):
- self.metrics: List[WorkflowMetrics] = []
-
- def track_request(self, session_id: str, request_id: str):
- # Implementation for tracking metrics
- pass
-
- def report(self):
- total_requests = len(self.metrics)
- avg_duration = sum(m.duration for m in self.metrics) / total_requests
- avg_refinements = sum(m.refinement_count for m in self.metrics) / total_requests
-
- print(f"Total Requests: {total_requests}")
- print(f"Average Duration: {avg_duration:.2f}s")
- print(f"Average Refinements: {avg_refinements:.2f}")
-
-# Usage with agent
-metrics = MetricsCollector()
-# Integrate with agent workflow
-```
-
-## Testing
-
-### Unit Tests
-
-```python
-import pytest
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-@pytest.fixture
-def agent():
- state_store = {}
- return Agent(state_store=state_store, session_id="test_session")
-
-@pytest.mark.asyncio
-async def test_basic_chat(agent):
- response = await agent.chat_async("What is 2+2?")
- assert response is not None
- assert len(response) > 0
-
-@pytest.mark.asyncio
-async def test_conversation_history(agent):
- # First message
- await agent.chat_async("My name is John")
-
- # Second message should have context
- response = await agent.chat_async("What is my name?")
- assert "john" in response.lower()
-
-@pytest.mark.asyncio
-async def test_mcp_tool_usage(agent):
- # Assuming MCP is configured
- response = await agent.chat_async("Get customer details for ID 1")
- # Verify tool was used and response contains customer data
- assert "customer" in response.lower()
-```
-
-### Integration Tests
-
-```python
-import pytest
-from fastapi.testclient import TestClient
-from your_backend import app
-
-@pytest.fixture
-def client():
- return TestClient(app)
-
-def test_chat_endpoint(client):
- response = client.post(
- "/chat",
- json={
- "session_id": "test_123",
- "message": "Hello",
- "use_workflow": True
- }
- )
- assert response.status_code == 200
- data = response.json()
- assert data["agent_type"] == "workflow"
- assert "response" in data
-```
-
-## Best Practices
-
-1. **Session Management**: Use unique session IDs per user
-2. **State Persistence**: Store state in Redis/database for production
-3. **Error Handling**: Implement proper error boundaries
-4. **Rate Limiting**: Protect endpoints from abuse
-5. **Authentication**: Secure MCP endpoints with proper tokens
-6. **Monitoring**: Log all workflow events for debugging
-7. **Testing**: Write comprehensive tests for edge cases
-
-## Troubleshooting
-
-### Issue: Workflow hangs
-
-**Cause**: Missing message handlers or unconnected edges
-
-**Solution**: Verify WorkflowBuilder has all necessary edges:
-```python
-.add_edge(primary_agent, reviewer_agent)
-.add_edge(reviewer_agent, primary_agent)
-```
-
-### Issue: MCP tools not working
-
-**Cause**: MCP_SERVER_URI not set or server not running
-
-**Solution**:
-```bash
-# Start MCP server
-python mcp/mcp_service.py
-
-# Set environment variable
-export MCP_SERVER_URI=http://localhost:5000/mcp
-```
-
-### Issue: Streaming not working
-
-**Cause**: WebSocket manager not set
-
-**Solution**:
-```python
-agent.set_websocket_manager(ws_manager)
-```
-
-## Migration Checklist
-
-- [ ] Update agent imports
-- [ ] Test basic chat functionality
-- [ ] Verify conversation history persistence
-- [ ] Test streaming with WebSocket
-- [ ] Validate MCP tool integration
-- [ ] Update frontend to handle new event types
-- [ ] Configure monitoring and logging
-- [ ] Run integration tests
-- [ ] Deploy to staging environment
-- [ ] Monitor performance metrics
-
-## Support
-
-For issues or questions:
-1. Check the [README](WORKFLOW_REFLECTION_README.md)
-2. Review [Architecture Diagrams](WORKFLOW_DIAGRAMS.md)
-3. Run tests: `python test_reflection_workflow_agent.py`
-4. Enable debug logging for detailed traces
diff --git a/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md b/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md
deleted file mode 100644
index 752e0f928..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md
+++ /dev/null
@@ -1,449 +0,0 @@
-# Workflow-Based Reflection Agent - Project Summary
-
-## What We Created
-
-A complete workflow-based implementation of the reflection agent pattern using Agent Framework's `WorkflowBuilder`, featuring a 3-party communication design with quality assurance gates.
-
-## Files Created
-
-### 1. **reflection_workflow_agent.py** (Main Implementation)
-Location: `agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py`
-
-**Key Components:**
-- `PrimaryAgentExecutor`: Customer support agent with MCP tool support
-- `ReviewerAgentExecutor`: Quality assurance gate with conditional routing
-- `Agent`: Main class implementing `BaseAgent` interface
-
-**Features:**
-- β
3-party communication pattern (User β Primary β Reviewer β User)
-- β
Conversation history management
-- β
MCP tool integration
-- β
Streaming support via WebSocket
-- β
Iterative refinement with feedback loops
-- β
Compatible with existing `BaseAgent` interface
-
-### 2. **test_reflection_workflow_agent.py** (Test Suite)
-Location: `agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py`
-
-**Features:**
-- Environment variable validation
-- Basic chat functionality tests
-- MCP tool integration tests
-- Conversation history verification
-- User-friendly output with progress indicators
-
-**Usage:**
-```bash
-python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
-```
-
-### 3. **WORKFLOW_REFLECTION_README.md** (Documentation)
-Location: `agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md`
-
-**Contents:**
-- Architecture overview
-- 3-party communication pattern explanation
-- Implementation details
-- Usage examples
-- Environment configuration
-- Troubleshooting guide
-- Comparison with traditional approach
-- Best practices
-
-### 4. **WORKFLOW_DIAGRAMS.md** (Visual Documentation)
-Location: `agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md`
-
-**Mermaid Diagrams:**
-- 3-party communication flow
-- Detailed workflow execution sequence
-- Message type relationships
-- Workflow graph structure
-- State management flow
-- Conversation history flow
-- Traditional vs Workflow comparison
-- MCP tool integration
-- Error handling flow
-- Streaming events flow
-
-### 5. **INTEGRATION_GUIDE.md** (Integration Documentation)
-Location: `agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md`
-
-**Contents:**
-- Quick start guide
-- Backend integration (FastAPI example)
-- Frontend integration (JavaScript/TypeScript, React)
-- Streamlit integration
-- Configuration management
-- Monitoring and logging
-- Testing strategies
-- Migration checklist
-
-## Architecture Highlights
-
-### 3-Party Communication Pattern
-
-```
-User β PrimaryAgent β ReviewerAgent β {approve: User, reject: PrimaryAgent}
- β |
- |__________________________________________|
- (feedback loop)
-```
-
-**Key Principles:**
-1. PrimaryAgent receives user messages but cannot send directly to user
-2. All PrimaryAgent outputs go to ReviewerAgent
-3. ReviewerAgent acts as conditional gate (approve/reject)
-4. Conversation history maintained between User and PrimaryAgent only
-5. Both agents receive history for context
-
-### Workflow Graph
-
-```python
-workflow = (
- WorkflowBuilder()
- .add_edge(primary_agent, reviewer_agent) # Forward path
- .add_edge(reviewer_agent, primary_agent) # Feedback path
- .set_start_executor(primary_agent)
- .build()
- .as_agent()
-)
-```
-
-### Message Types
-
-1. **PrimaryAgentRequest**: User β PrimaryAgent
- - `request_id`: Unique identifier
- - `user_prompt`: User's question
- - `conversation_history`: Previous messages
-
-2. **ReviewRequest**: PrimaryAgent β ReviewerAgent
- - `request_id`: Same as original request
- - `user_prompt`: Original question
- - `conversation_history`: For context
- - `primary_agent_response`: Agent's answer
-
-3. **ReviewResponse**: ReviewerAgent β PrimaryAgent
- - `request_id`: Correlation ID
- - `approved`: Boolean decision
- - `feedback`: Constructive feedback or approval note
-
-## Key Features
-
-### β
Workflow-Based Architecture
-- Built using `WorkflowBuilder` for explicit control flow
-- Bidirectional edges between executors
-- Conditional routing based on structured decisions
-
-### β
Quality Assurance
-- Every response reviewed before reaching user
-- Structured evaluation criteria:
- - Accuracy of information
- - Completeness of answer
- - Professional tone
- - Proper tool usage
- - Clarity and helpfulness
-
-### β
Iterative Refinement
-- Failed reviews trigger regeneration with feedback
-- Conversation context preserved across iterations
-- Unlimited refinement cycles until approval
-
-### β
MCP Tool Integration
-- Supports MCP tools for external data access
-- Tools available to both agents
-- Proper authentication via bearer tokens
-
-### β
Streaming Support
-- WebSocket-based streaming for real-time updates
-- Progress indicators for each workflow stage
-- Token-level streaming for agent responses
-
-### β
State Management
-- Conversation history persisted in state store
-- Session-based isolation
-- Compatible with Redis/database for production
-
-## Usage Examples
-
-### Basic Usage
-
-```python
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-# Create agent
-state_store = {}
-agent = Agent(state_store=state_store, session_id="user_123")
-
-# Chat
-response = await agent.chat_async("Help with customer 1")
-```
-
-### With Streaming
-
-```python
-# Set WebSocket manager
-agent.set_websocket_manager(ws_manager)
-
-# Chat with streaming updates
-response = await agent.chat_async("What promotions are available?")
-```
-
-### With MCP Tools
-
-```python
-# Set MCP_SERVER_URI environment variable
-os.environ["MCP_SERVER_URI"] = "http://localhost:5000/mcp"
-
-# Agent will automatically use MCP tools
-agent = Agent(state_store=state_store, session_id="user_123", access_token=token)
-response = await agent.chat_async("Get billing summary for customer 1")
-```
-
-## Comparison: Workflow vs Traditional
-
-| Feature | Traditional | Workflow |
-|---------|------------|----------|
-| **Architecture** | Sequential agent.run() calls | Message-based graph execution |
-| **Control Flow** | Implicit (procedural code) | Explicit (workflow edges) |
-| **State Management** | Manual (instance variables) | Framework-managed |
-| **Scalability** | Limited | Highly scalable |
-| **Testing** | Mock agent methods | Mock message handlers |
-| **Debugging** | Step through code | Trace message flow |
-| **Extensibility** | Modify agent code | Add executors/edges |
-
-## Integration Points
-
-### Backend Integration
-- β
FastAPI example provided
-- β
WebSocket support for streaming
-- β
Compatible with existing BaseAgent interface
-- β
No breaking changes to API
-
-### Frontend Integration
-- β
JavaScript/TypeScript client example
-- β
React component example
-- β
Stream event handlers
-- β
Progressive UI updates
-
-### Streamlit Integration
-- β
Complete Streamlit example
-- β
Session state management
-- β
Chat history display
-- β
Async execution handling
-
-## Testing
-
-### Run Tests
-
-```bash
-# Basic test
-python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
-
-# With specific Python
-python3.11 agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
-```
-
-### Test Coverage
-- β
Environment validation
-- β
Basic chat functionality
-- β
Conversation history
-- β
MCP tool integration
-- β
Error handling
-
-## Environment Variables
-
-**Required:**
-- `AZURE_OPENAI_API_KEY`
-- `AZURE_OPENAI_CHAT_DEPLOYMENT`
-- `AZURE_OPENAI_ENDPOINT`
-- `AZURE_OPENAI_API_VERSION`
-- `OPENAI_MODEL_NAME`
-
-**Optional:**
-- `MCP_SERVER_URI` (enables MCP tool usage)
-
-## Documentation Structure
-
-```
-agentic_ai/agents/agent_framework/multi_agent/
-βββ reflection_workflow_agent.py # Main implementation
-βββ test_reflection_workflow_agent.py # Test suite
-βββ WORKFLOW_REFLECTION_README.md # Main documentation
-βββ WORKFLOW_DIAGRAMS.md # Visual diagrams
-βββ INTEGRATION_GUIDE.md # Integration examples
-βββ PROJECT_SUMMARY.md # This file
-```
-
-## Key Learnings from Reference Examples
-
-### From `workflow_as_agent_reflection_pattern_azure.py`
-- β
WorkflowBuilder usage patterns
-- β
Message-based communication
-- β
AgentRunUpdateEvent for output emission
-- β
Structured output with Pydantic
-
-### From `workflow_as_agent_human_in_the_loop_azure.py`
-- β
RequestInfoExecutor pattern
-- β
Correlation with request IDs
-- β
Bidirectional edge configuration
-
-### From `edge_condition.py`
-- β
Conditional routing with predicates
-- β
Boolean edge conditions
-- β
Structured decision parsing
-
-### From `guessing_game_with_human_input.py`
-- β
Event-driven architecture
-- β
RequestResponse correlation
-- β
Typed request payloads
-
-## Advantages of Workflow Approach
-
-### 1. **Explicit Control Flow**
-Workflow edges make the communication pattern crystal clear:
-```python
-.add_edge(primary_agent, reviewer_agent)
-.add_edge(reviewer_agent, primary_agent)
-```
-
-### 2. **Better Separation of Concerns**
-Each executor has a single responsibility:
-- PrimaryAgent: Generate responses
-- ReviewerAgent: Evaluate quality
-
-### 3. **Framework-Managed State**
-No need to manually track pending requests across retries.
-
-### 4. **Easier Testing**
-Mock message handlers instead of complex agent interactions.
-
-### 5. **Scalability**
-Easy to add more executors (e.g., specialized reviewers, human escalation).
-
-### 6. **Debugging**
-Message flow is traceable through logs.
-
-## Future Enhancement Ideas
-
-### Short Term
-- [ ] Add max refinement limit to prevent infinite loops
-- [ ] Implement retry logic with exponential backoff
-- [ ] Add metrics collection for performance monitoring
-- [ ] Create Jupyter notebook examples
-
-### Medium Term
-- [ ] Support parallel reviewer agents (consensus-based approval)
-- [ ] Add human-in-the-loop escalation for edge cases
-- [ ] Implement A/B testing framework for review criteria
-- [ ] Create dashboard for workflow analytics
-
-### Long Term
-- [ ] Multi-modal support (images, files)
-- [ ] Fine-tuned reviewer models
-- [ ] Dynamic workflow routing based on request type
-- [ ] Integration with external approval systems
-
-## Migration from Traditional Agent
-
-### Step-by-Step Migration
-
-1. **Update Import**
- ```python
- # OLD
- from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent
-
- # NEW
- from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
- ```
-
-2. **No Code Changes Required**
- The workflow agent implements the same `BaseAgent` interface.
-
-3. **Test Thoroughly**
- Run integration tests to verify behavior.
-
-4. **Monitor Performance**
- Compare response times and quality metrics.
-
-5. **Gradual Rollout**
- Use feature flags to gradually migrate users.
-
-### Migration Checklist
-
-- [ ] Update agent imports
-- [ ] Test basic chat functionality
-- [ ] Verify conversation history
-- [ ] Test streaming with WebSocket
-- [ ] Validate MCP tool integration
-- [ ] Update frontend event handlers
-- [ ] Configure monitoring
-- [ ] Run integration tests
-- [ ] Deploy to staging
-- [ ] Monitor metrics
-- [ ] Full production rollout
-
-## Success Criteria
-
-### Functional Requirements
-- β
All responses reviewed before delivery
-- β
Conversation history maintained correctly
-- β
MCP tools work as expected
-- β
Streaming updates work properly
-- β
Compatible with existing interface
-
-### Non-Functional Requirements
-- β
Response time < 5 seconds (typical)
-- β
Clear logging for debugging
-- β
Proper error handling
-- β
Comprehensive documentation
-- β
Test coverage > 80%
-
-## Resources
-
-### Documentation
-- [Main README](WORKFLOW_REFLECTION_README.md)
-- [Architecture Diagrams](WORKFLOW_DIAGRAMS.md)
-- [Integration Guide](INTEGRATION_GUIDE.md)
-
-### Code
-- [Implementation](reflection_workflow_agent.py)
-- [Tests](test_reflection_workflow_agent.py)
-
-### References
-- [Agent Framework Reflection Example](../../../reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern_azure.py)
-- [Human-in-the-Loop Example](../../../reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop_azure.py)
-- [Edge Conditions Example](../../../reference/agent-framework/python/samples/getting_started/workflows/control-flow/edge_condition.py)
-
-## Support and Feedback
-
-For issues, questions, or feedback:
-
-1. **Check Documentation**: Review README and integration guide
-2. **Run Tests**: Execute test suite to validate setup
-3. **Enable Debug Logging**: Set log level to DEBUG
-4. **Review Diagrams**: Check architecture diagrams for understanding
-5. **Create Issue**: Document issue with logs and reproduction steps
-
-## Conclusion
-
-The workflow-based reflection agent provides a robust, scalable, and maintainable implementation of the reflection pattern. It leverages Agent Framework's workflow capabilities to create an explicit, testable, and extensible architecture that's ready for production use.
-
-**Key Benefits:**
-- β
Explicit 3-party communication pattern
-- β
Quality-assured responses
-- β
Iterative refinement
-- β
Production-ready with streaming
-- β
Fully compatible with existing system
-- β
Comprehensive documentation
-
-**Ready to Use:**
-- All code tested and documented
-- Integration examples provided
-- Migration path clear
-- Support materials available
-
----
-
-**Version**: 1.0.0
-**Date**: October 2025
-**Status**: Production Ready β
diff --git a/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md b/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md
deleted file mode 100644
index 2f7e1a7d8..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md
+++ /dev/null
@@ -1,351 +0,0 @@
-# Workflow Reflection Agent - Quick Reference
-
-## One-Minute Overview
-
-**What**: Workflow-based reflection agent with 3-party quality assurance pattern
-**When**: Use for high-quality responses with built-in review process
-**Why**: Better control flow, scalability, and maintainability vs traditional approach
-
-## Quick Start (30 seconds)
-
-```python
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-state_store = {}
-agent = Agent(state_store=state_store, session_id="user_123")
-response = await agent.chat_async("Your question here")
-```
-
-## Architecture at a Glance
-
-```
-User ββ¬ββ> PrimaryAgent ββ¬ββ> ReviewerAgent ββ¬ββ> User (if approved)
- β β β
- ββ History ββββββββββ βββ> PrimaryAgent (if rejected)
- β
- βββ> (loop)
-```
-
-## Key Files
-
-| File | Purpose | Size |
-|------|---------|------|
-| `reflection_workflow_agent.py` | Main implementation | ~600 lines |
-| `test_reflection_workflow_agent.py` | Test suite | ~200 lines |
-| `WORKFLOW_REFLECTION_README.md` | Full documentation | ~400 lines |
-| `WORKFLOW_DIAGRAMS.md` | Visual diagrams | ~500 lines |
-| `INTEGRATION_GUIDE.md` | Integration examples | ~800 lines |
-
-## Message Flow Cheat Sheet
-
-### 1οΈβ£ User β PrimaryAgent
-```python
-PrimaryAgentRequest(
- request_id=uuid4(),
- user_prompt="Help me",
- conversation_history=[...]
-)
-```
-
-### 2οΈβ£ PrimaryAgent β ReviewerAgent
-```python
-ReviewRequest(
- request_id=request_id,
- user_prompt="Help me",
- conversation_history=[...],
- primary_agent_response=[ChatMessage(...)]
-)
-```
-
-### 3οΈβ£ ReviewerAgent Decision
-```python
-ReviewDecision(
- approved=True/False,
- feedback="..."
-)
-```
-
-### 4οΈβ£ Output
-- **If approved**: `AgentRunUpdateEvent` β User
-- **If rejected**: `ReviewResponse` β PrimaryAgent (loop to step 2)
-
-## Common Tasks
-
-### Enable Streaming
-```python
-agent.set_websocket_manager(ws_manager)
-```
-
-### Enable MCP Tools
-```bash
-export MCP_SERVER_URI=http://localhost:5000/mcp
-```
-
-### Access History
-```python
-history = agent.chat_history # List of dicts
-# or
-history = agent._conversation_history # List of ChatMessage
-```
-
-### Run Tests
-```bash
-python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
-```
-
-## Environment Variables
-
-```bash
-# Required
-AZURE_OPENAI_API_KEY=sk-...
-AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4
-AZURE_OPENAI_ENDPOINT=https://....openai.azure.com/
-AZURE_OPENAI_API_VERSION=2024-02-15-preview
-OPENAI_MODEL_NAME=gpt-4
-
-# Optional
-MCP_SERVER_URI=http://localhost:5000/mcp
-```
-
-## Streaming Events
-
-| Event Type | When | Purpose |
-|------------|------|---------|
-| `orchestrator` | Start/Progress/End | Workflow status |
-| `agent_start` | Agent begins | Show agent badge |
-| `agent_token` | Token generated | Stream text |
-| `agent_message` | Agent completes | Full message |
-| `tool_called` | Tool invoked | Show tool usage |
-| `final_result` | Workflow done | Final response |
-
-## Debug Checklist
-
-β **Not working?**
-1. Check environment variables are set
-2. Verify MCP server is running (if using tools)
-3. Enable debug logging: `logging.basicConfig(level=logging.DEBUG)`
-4. Check WebSocket manager is set (for streaming)
-5. Review logs for error messages
-
-β **Infinite loop?**
-1. Check reviewer criteria are achievable
-2. Add max refinement counter
-3. Review feedback content for clarity
-
-β **No MCP tools?**
-1. Verify `MCP_SERVER_URI` is set
-2. Test MCP server: `curl $MCP_SERVER_URI/health`
-3. Check access token is valid
-
-## Comparison Matrix
-
-| Feature | Traditional | Workflow | Winner |
-|---------|------------|----------|--------|
-| Control Flow | Implicit | Explicit | π Workflow |
-| Testability | Medium | High | π Workflow |
-| Scalability | Limited | High | π Workflow |
-| Learning Curve | Low | Medium | π₯ Traditional |
-| State Management | Manual | Auto | π Workflow |
-| Debugging | Hard | Easy | π Workflow |
-
-## Code Snippets
-
-### Backend Integration (FastAPI)
-```python
-@app.post("/chat")
-async def chat(session_id: str, message: str):
- agent = Agent(state_store, session_id)
- response = await agent.chat_async(message)
- return {"response": response}
-```
-
-### Frontend Integration (React)
-```tsx
-const [response, setResponse] = useState('');
-ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
- if (data.type === 'final_result') {
- setResponse(data.content);
- }
-};
-```
-
-### Streamlit Integration
-```python
-agent = Agent(st.session_state.state_store, session_id)
-if prompt := st.chat_input("Ask..."):
- response = asyncio.run(agent.chat_async(prompt))
- st.chat_message("assistant").write(response)
-```
-
-## Performance Tips
-
-β
**DO:**
-- Use streaming for better UX
-- Enable debug logging during development
-- Implement retry logic for MCP tools
-- Cache frequent queries
-- Monitor refinement counts
-
-β **DON'T:**
-- Allow unlimited refinement loops
-- Log sensitive customer data
-- Skip error handling
-- Forget to persist state
-- Ignore WebSocket errors
-
-## Workflow Builder Pattern
-
-```python
-workflow = (
- WorkflowBuilder()
- .add_edge(executor_a, executor_b) # A β B
- .add_edge(executor_b, executor_a) # B β A (feedback)
- .set_start_executor(executor_a) # Start with A
- .build() # Build workflow
- .as_agent() # Expose as agent
-)
-```
-
-## Executor Handlers
-
-```python
-class MyExecutor(Executor):
- @handler
- async def handle_message(
- self,
- request: RequestType,
- ctx: WorkflowContext[ResponseType]
- ) -> None:
- # Process request
- result = await self.process(request)
-
- # Send to next executor
- await ctx.send_message(result)
-
- # Or emit to user
- await ctx.add_event(
- AgentRunUpdateEvent(
- self.id,
- data=AgentRunResponseUpdate(...)
- )
- )
-```
-
-## Structured Output
-
-```python
-from pydantic import BaseModel
-
-class MyResponse(BaseModel):
- field1: str
- field2: bool
-
-# Use in chat client
-response = await chat_client.get_response(
- messages=[...],
- response_format=MyResponse
-)
-
-# Parse
-parsed = MyResponse.model_validate_json(response.text)
-```
-
-## Logging Best Practices
-
-```python
-import logging
-
-logger = logging.getLogger(__name__)
-
-# In executor
-logger.info(f"[{self.id}] Processing request {request_id[:8]}")
-logger.debug(f"[{self.id}] Full request: {request}")
-logger.error(f"[{self.id}] Error: {e}", exc_info=True)
-```
-
-## Testing Patterns
-
-```python
-@pytest.fixture
-def agent():
- return Agent(state_store={}, session_id="test")
-
-@pytest.mark.asyncio
-async def test_chat(agent):
- response = await agent.chat_async("Hello")
- assert response is not None
- assert len(response) > 0
-
-@pytest.mark.asyncio
-async def test_history(agent):
- await agent.chat_async("My name is John")
- response = await agent.chat_async("What is my name?")
- assert "john" in response.lower()
-```
-
-## Common Pitfalls
-
-π΄ **Pitfall 1**: Not setting start executor
-```python
-# Wrong
-WorkflowBuilder().add_edge(a, b).build()
-
-# Right
-WorkflowBuilder().add_edge(a, b).set_start_executor(a).build()
-```
-
-π΄ **Pitfall 2**: Missing return edges
-```python
-# Wrong (one-way only)
-.add_edge(primary, reviewer)
-
-# Right (bidirectional for loops)
-.add_edge(primary, reviewer)
-.add_edge(reviewer, primary)
-```
-
-π΄ **Pitfall 3**: Not handling async properly
-```python
-# Wrong
-response = agent.chat_async(prompt)
-
-# Right
-response = await agent.chat_async(prompt)
-# or
-response = asyncio.run(agent.chat_async(prompt))
-```
-
-## Links
-
-π **Documentation**
-- [Full README](WORKFLOW_REFLECTION_README.md)
-- [Diagrams](WORKFLOW_DIAGRAMS.md)
-- [Integration Guide](INTEGRATION_GUIDE.md)
-- [Project Summary](PROJECT_SUMMARY.md)
-
-π§ **Code**
-- [Implementation](reflection_workflow_agent.py)
-- [Tests](test_reflection_workflow_agent.py)
-
-π **Examples**
-- Agent Framework Samples in `reference/agent-framework/`
-
-## Support
-
-1. Check docs β
-2. Run tests
-3. Enable debug logging
-4. Review error messages
-5. Check environment vars
-
-## Version Info
-
-- **Version**: 1.0.0
-- **Status**: β
Production Ready
-- **Python**: 3.10+
-- **Dependencies**: agent-framework, pydantic, azure-identity
-
----
-
-**TIP**: Bookmark this page for quick reference! π
diff --git a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md b/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md
deleted file mode 100644
index 065a5f1e2..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md
+++ /dev/null
@@ -1,337 +0,0 @@
-# Workflow-Based Reflection Agent - Architecture Diagrams
-
-## 3-Party Communication Flow
-
-```mermaid
-graph TD
- User[User] -->|PrimaryAgentRequest| PA[PrimaryAgent Executor]
- PA -->|ReviewRequest| RA[ReviewerAgent Executor]
- RA -->|ReviewResponse approved=false| PA
- RA -->|AgentRunUpdateEvent approved=true| User
-
- style User fill:#e1f5ff
- style PA fill:#fff4e1
- style RA fill:#e8f5e8
-```
-
-## Detailed Workflow Execution
-
-```mermaid
-sequenceDiagram
- participant User
- participant WorkflowAgent
- participant PrimaryAgent
- participant ReviewerAgent
-
- User->>WorkflowAgent: chat_async("Help with customer 1")
- WorkflowAgent->>PrimaryAgent: PrimaryAgentRequest
(prompt + history)
-
- Note over PrimaryAgent: Generate response
using MCP tools
and conversation history
-
- PrimaryAgent->>ReviewerAgent: ReviewRequest
(prompt + history + response)
-
- Note over ReviewerAgent: Evaluate response quality
Check accuracy, completeness,
professionalism
-
- alt Response Approved
- ReviewerAgent->>ReviewerAgent: AgentRunUpdateEvent
- ReviewerAgent->>WorkflowAgent: Emit to user
- WorkflowAgent->>User: Final response
- else Response Rejected
- ReviewerAgent->>PrimaryAgent: ReviewResponse
(approved=false, feedback)
-
- Note over PrimaryAgent: Incorporate feedback
Regenerate response
-
- PrimaryAgent->>ReviewerAgent: ReviewRequest
(refined response)
-
- Note over ReviewerAgent: Re-evaluate
-
- ReviewerAgent->>ReviewerAgent: AgentRunUpdateEvent
- ReviewerAgent->>WorkflowAgent: Emit to user
- WorkflowAgent->>User: Final response
- end
-```
-
-## Message Types
-
-```mermaid
-classDiagram
- class PrimaryAgentRequest {
- +str request_id
- +str user_prompt
- +list~ChatMessage~ conversation_history
- }
-
- class ReviewRequest {
- +str request_id
- +str user_prompt
- +list~ChatMessage~ conversation_history
- +list~ChatMessage~ primary_agent_response
- }
-
- class ReviewResponse {
- +str request_id
- +bool approved
- +str feedback
- }
-
- class ReviewDecision {
- +bool approved
- +str feedback
- }
-
- PrimaryAgentRequest --> ReviewRequest : transforms to
- ReviewRequest --> ReviewDecision : evaluates into
- ReviewDecision --> ReviewResponse : converts to
- ReviewResponse --> PrimaryAgentRequest : triggers retry if rejected
-```
-
-## Workflow Graph Structure
-
-```mermaid
-graph LR
- Start([Start]) --> PA[PrimaryAgent
Executor]
- PA -->|ReviewRequest| RA[ReviewerAgent
Executor]
- RA -->|ReviewResponse
approved=false| PA
- RA -->|AgentRunUpdateEvent
approved=true| End([User])
-
- style Start fill:#90EE90
- style End fill:#FFB6C1
- style PA fill:#FFE4B5
- style RA fill:#E0BBE4
-```
-
-## State Management
-
-```mermaid
-stateDiagram-v2
- [*] --> UserInput: User sends prompt
-
- UserInput --> PrimaryGenerate: Create PrimaryAgentRequest
with conversation history
-
- PrimaryGenerate --> ReviewEvaluate: Send ReviewRequest
to ReviewerAgent
-
- ReviewEvaluate --> Approved: Quality check passes
- ReviewEvaluate --> Rejected: Quality check fails
-
- Rejected --> PrimaryRefinement: Send ReviewResponse
with feedback
-
- PrimaryRefinement --> ReviewEvaluate: Send refined ReviewRequest
-
- Approved --> EmitToUser: AgentRunUpdateEvent
-
- EmitToUser --> UpdateHistory: Add to conversation history
-
- UpdateHistory --> [*]: Return response to user
-
- note right of ReviewEvaluate
- Conditional Gate:
- - Accuracy
- - Completeness
- - Professionalism
- - Tool usage
- - Clarity
- end note
-
- note right of PrimaryRefinement
- Incorporate feedback:
- - Add reviewer feedback to context
- - Regenerate response
- - Maintain conversation history
- end note
-```
-
-## Conversation History Flow
-
-```mermaid
-graph TB
- subgraph "State Store"
- History[Conversation History
User β PrimaryAgent only]
- end
-
- subgraph "Request 1"
- U1[User: Query 1] --> P1[PrimaryAgent]
- P1 --> R1[ReviewerAgent]
- R1 -->|approved| H1[Add to History]
- H1 --> History
- end
-
- subgraph "Request 2 with History"
- History --> P2[PrimaryAgent
receives history]
- U2[User: Query 2] --> P2
- P2 --> R2[ReviewerAgent
receives history]
- R2 -->|approved| H2[Add to History]
- H2 --> History
- end
-
- style History fill:#FFE4E1
- style H1 fill:#90EE90
- style H2 fill:#90EE90
-```
-
-## Comparison: Traditional vs Workflow
-
-```mermaid
-graph TB
- subgraph "Traditional Reflection Agent"
- T1[Agent.run Step 1:
Primary generates] --> T2[Agent.run Step 2:
Reviewer evaluates]
- T2 --> T3{Approved?}
- T3 -->|No| T4[Agent.run Step 3:
Primary refines]
- T4 --> T2
- T3 -->|Yes| T5[Return to user]
-
- style T1 fill:#FFE4B5
- style T2 fill:#E0BBE4
- style T4 fill:#FFE4B5
- end
-
- subgraph "Workflow Reflection Agent"
- W1[PrimaryAgentExecutor
handles request] --> W2[ReviewerAgentExecutor
evaluates]
- W2 --> W3{Approved?}
- W3 -->|No| W4[PrimaryAgentExecutor
handles feedback]
- W4 --> W2
- W3 -->|Yes| W5[AgentRunUpdateEvent
to user]
-
- style W1 fill:#FFE4B5
- style W2 fill:#E0BBE4
- style W4 fill:#FFE4B5
- style W5 fill:#90EE90
- end
-```
-
-## MCP Tool Integration
-
-```mermaid
-graph LR
- subgraph "Workflow"
- PA[PrimaryAgent] --> RA[ReviewerAgent]
- RA --> PA
- end
-
- subgraph "MCP Tools"
- T1[get_customer_detail]
- T2[get_billing_summary]
- T3[get_promotions]
- T4[search_knowledge_base]
- end
-
- PA -.->|Uses tools| T1
- PA -.->|Uses tools| T2
- PA -.->|Uses tools| T3
- PA -.->|Uses tools| T4
-
- RA -.->|May use tools
to verify| T1
-
- subgraph "MCP Server"
- MCP[HTTP MCP Server
:5000/mcp]
- end
-
- T1 --> MCP
- T2 --> MCP
- T3 --> MCP
- T4 --> MCP
-
- style PA fill:#FFE4B5
- style RA fill:#E0BBE4
- style MCP fill:#E1F5FF
-```
-
-## Error Handling Flow
-
-```mermaid
-graph TD
- Start([User Query]) --> Init[Initialize Workflow]
-
- Init --> CheckEnv{Env Config OK?}
- CheckEnv -->|No| Error1[Raise RuntimeError]
- CheckEnv -->|Yes| CreateReq[Create PrimaryAgentRequest]
-
- CreateReq --> PA[PrimaryAgent Process]
-
- PA --> CheckPA{Primary Success?}
- CheckPA -->|Error| Error2[Log error + Raise]
- CheckPA -->|Success| RA[ReviewerAgent Process]
-
- RA --> CheckRA{Review Success?}
- CheckRA -->|Error| Error3[Log error + Raise]
- CheckRA -->|Success| Decision{Approved?}
-
- Decision -->|Yes| Success[Return to User]
- Decision -->|No| CheckRetry{Max Retries?}
-
- CheckRetry -->|Exceeded| Error4[Log warning + Return best attempt]
- CheckRetry -->|Continue| PA
-
- style Error1 fill:#FFB6C1
- style Error2 fill:#FFB6C1
- style Error3 fill:#FFB6C1
- style Error4 fill:#FFE4B5
- style Success fill:#90EE90
-```
-
-## Streaming Events Flow
-
-```mermaid
-sequenceDiagram
- participant User
- participant Backend
- participant WorkflowAgent
- participant WebSocket
-
- User->>Backend: Send query
- Backend->>WorkflowAgent: chat_async(query)
-
- WorkflowAgent->>WebSocket: orchestrator: "plan"
"Workflow starting..."
- WebSocket->>User: Display plan
-
- WorkflowAgent->>WebSocket: agent_start: "primary_agent"
- WebSocket->>User: Show agent badge
-
- loop Primary Generation
- WorkflowAgent->>WebSocket: agent_token: chunk
- WebSocket->>User: Stream text
- end
-
- WorkflowAgent->>WebSocket: agent_message: complete
- WebSocket->>User: Display message
-
- WorkflowAgent->>WebSocket: orchestrator: "progress"
"Reviewer evaluating..."
- WebSocket->>User: Update progress
-
- WorkflowAgent->>WebSocket: agent_start: "reviewer_agent"
- WebSocket->>User: Show reviewer badge
-
- loop Reviewer Evaluation
- WorkflowAgent->>WebSocket: agent_token: chunk
- WebSocket->>User: Stream text
- end
-
- alt Approved
- WorkflowAgent->>WebSocket: orchestrator: "result"
"Approved!"
- WorkflowAgent->>WebSocket: final_result: response
- WebSocket->>User: Display final response
- else Rejected
- WorkflowAgent->>WebSocket: orchestrator: "progress"
"Refining..."
- Note over WorkflowAgent: Loop back to Primary
- end
-```
-
----
-
-## How to View These Diagrams
-
-These diagrams use Mermaid syntax, which is supported by:
-
-1. **GitHub**: Automatically rendered in Markdown files
-2. **VS Code**: Install "Markdown Preview Mermaid Support" extension
-3. **Online**: Copy to https://mermaid.live
-4. **Documentation sites**: GitBook, Docusaurus, etc.
-
-## Legend
-
-- π’ **Green**: Success/approval states
-- π‘ **Yellow**: Processing/agent executors
-- π£ **Purple**: Review/evaluation
-- π΅ **Blue**: User/external
-- π΄ **Red**: Error states
-- β‘οΈ **Solid arrows**: Direct message flow
-- β€ **Dashed arrows**: Tool calls/side effects
diff --git a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md b/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md
deleted file mode 100644
index fe91765f2..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md
+++ /dev/null
@@ -1,345 +0,0 @@
-# Workflow-Based Reflection Agent
-
-A workflow implementation of the reflection pattern using Agent Framework's `WorkflowBuilder`, featuring a 3-party communication design with quality assurance gates.
-
-## Overview
-
-This agent implements a sophisticated reflection pattern where responses are iteratively refined until they meet quality standards. Unlike the traditional two-agent reflection pattern, this uses a workflow-based approach with explicit conditional routing.
-
-## Architecture
-
-### 3-Party Communication Pattern
-
-```
-User β PrimaryAgent β ReviewerAgent β {approve: User, reject: PrimaryAgent}
- β |
- |__________________________________________|
- (feedback loop)
-```
-
-**Key Design Principles:**
-
-1. **PrimaryAgent**: Customer support agent that:
- - Receives user messages with conversation history
- - Cannot send messages directly to user
- - All outputs go to ReviewerAgent for evaluation
- - Uses MCP tools for data retrieval
-
-2. **ReviewerAgent**: Quality assurance gate that:
- - Evaluates PrimaryAgent responses
- - Acts as conditional router:
- - `approve=true` β Emit to user
- - `approve=false` β Send feedback to PrimaryAgent
- - Has access to full conversation context
-
-3. **Conversation History**:
- - Maintained between User and PrimaryAgent only
- - Both agents receive history for context
- - Updated only when approved responses are delivered
-
-## Features
-
-β
**Workflow-Based Architecture**
-- Built using `WorkflowBuilder` for explicit control flow
-- Bidirectional edges between PrimaryAgent and ReviewerAgent
-- Conditional routing based on structured review decisions
-
-β
**Quality Assurance**
-- Every response is reviewed before reaching the user
-- Structured evaluation criteria:
- - Accuracy of information
- - Completeness of answer
- - Professional tone
- - Proper tool usage
- - Clarity and helpfulness
-
-β
**Iterative Refinement**
-- Failed reviews trigger regeneration with feedback
-- Conversation context preserved across iterations
-- Unlimited refinement cycles until approval
-
-β
**MCP Tool Integration**
-- Supports MCP tools for external data access
-- Tools available to both agents
-- Proper authentication via bearer tokens
-
-β
**Streaming Support**
-- WebSocket-based streaming for real-time updates
-- Progress indicators for each workflow stage
-- Token-level streaming for agent responses
-
-## Implementation Details
-
-### Executor Classes
-
-#### `PrimaryAgentExecutor`
-```python
-class PrimaryAgentExecutor(Executor):
- """
- Generates customer support responses.
- Sends all outputs to ReviewerAgent.
- """
-
- @handler
- async def handle_user_request(
- self, request: PrimaryAgentRequest, ctx: WorkflowContext[ReviewRequest]
- ) -> None:
- # Generate response with conversation history
- # Send to ReviewerAgent for evaluation
-
- @handler
- async def handle_review_feedback(
- self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest]
- ) -> None:
- # If not approved: incorporate feedback and regenerate
- # Send refined response back to ReviewerAgent
-```
-
-#### `ReviewerAgentExecutor`
-```python
-class ReviewerAgentExecutor(Executor):
- """
- Evaluates responses and acts as conditional gate.
- """
-
- @handler
- async def review_response(
- self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]
- ) -> None:
- # Evaluate response quality
- # If approved: emit to user via AgentRunUpdateEvent
- # If not: send feedback to PrimaryAgent
-```
-
-### Message Flow
-
-1. **User Input**
- ```python
- PrimaryAgentRequest(
- request_id=uuid4(),
- user_prompt="What is customer 1's billing status?",
- conversation_history=[...previous messages...]
- )
- ```
-
-2. **Primary Agent β Reviewer**
- ```python
- ReviewRequest(
- request_id=request_id,
- user_prompt="What is customer 1's billing status?",
- conversation_history=[...],
- primary_agent_response=[...ChatMessage...]
- )
- ```
-
-3. **Reviewer Decision**
- ```python
- ReviewDecision(
- approved=True/False,
- feedback="Constructive feedback or approval note"
- )
- ```
-
-4. **Conditional Routing**
- - **Approved**: `AgentRunUpdateEvent` β User
- - **Rejected**: `ReviewResponse` β PrimaryAgent β Loop back to step 2
-
-### Workflow Graph
-
-```python
-workflow = (
- WorkflowBuilder()
- .add_edge(primary_agent, reviewer_agent) # Forward path
- .add_edge(reviewer_agent, primary_agent) # Feedback path
- .set_start_executor(primary_agent)
- .build()
- .as_agent() # Expose as standard agent interface
-)
-```
-
-## Usage
-
-### Basic Usage
-
-```python
-from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
-
-# Create agent instance
-state_store = {}
-session_id = "user_session_123"
-agent = Agent(state_store=state_store, session_id=session_id)
-
-# Process user query
-response = await agent.chat_async("Can you help me with customer ID 1?")
-print(response)
-```
-
-### With Streaming
-
-```python
-# Set WebSocket manager for streaming updates
-agent.set_websocket_manager(ws_manager)
-
-# Chat will now stream progress updates
-response = await agent.chat_async("What promotions are available?")
-```
-
-### With MCP Tools
-
-```python
-# Set MCP_SERVER_URI environment variable
-os.environ["MCP_SERVER_URI"] = "http://localhost:5000/mcp"
-
-# Agent will automatically use MCP tools
-agent = Agent(state_store=state_store, session_id=session_id, access_token=token)
-response = await agent.chat_async("Get billing summary for customer 1")
-```
-
-## Environment Variables
-
-Required:
-- `AZURE_OPENAI_API_KEY`: Azure OpenAI API key
-- `AZURE_OPENAI_CHAT_DEPLOYMENT`: Deployment name
-- `AZURE_OPENAI_ENDPOINT`: Azure OpenAI endpoint URL
-- `AZURE_OPENAI_API_VERSION`: API version (e.g., "2024-02-15-preview")
-- `OPENAI_MODEL_NAME`: Model name (e.g., "gpt-4")
-
-Optional:
-- `MCP_SERVER_URI`: URI for MCP server (enables tool usage)
-
-## Testing
-
-Run the test script:
-
-```bash
-# From project root
-python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
-```
-
-The test script will:
-1. Verify environment configuration
-2. Run basic queries
-3. Test MCP tool integration (if configured)
-4. Display conversation history
-
-## Comparison: Workflow vs Traditional
-
-### Traditional Reflection Agent (`reflection_agent.py`)
-- Direct agent-to-agent communication via `run()` calls
-- Sequential execution (Step 1 β Step 2 β Step 3)
-- Implicit control flow
-- Manual state management
-
-### Workflow Reflection Agent (`reflection_workflow_agent.py`)
-- Message-based communication via `WorkflowContext`
-- Graph-based execution (workflow edges)
-- Explicit conditional routing
-- Framework-managed state
-- Better scalability for complex workflows
-
-## Advanced Features
-
-### Custom Review Criteria
-
-Modify the ReviewerAgent's system prompt to enforce custom quality standards:
-
-```python
-# In ReviewerAgentExecutor.__init__
-custom_criteria = """
-Review for:
-1. Response time < 2 seconds
-2. Includes specific customer name
-3. References at least 2 data points
-4. Professional greeting and closing
-"""
-```
-
-### Multiple Refinement Rounds Limit
-
-Add a counter to prevent infinite loops:
-
-```python
-class PrimaryAgentExecutor(Executor):
- def __init__(self, max_refinements: int = 3):
- self._max_refinements = max_refinements
- self._refinement_counts = {}
-
- async def handle_review_feedback(self, review, ctx):
- count = self._refinement_counts.get(review.request_id, 0)
- if count >= self._max_refinements:
- # Force approval or escalate
- return
-```
-
-### Logging and Monitoring
-
-All workflow events are logged with structured information:
-
-```python
-logger.info(f"[PrimaryAgent] Processing request {request_id[:8]}")
-logger.info(f"[ReviewerAgent] Review decision - Approved: {approved}")
-```
-
-Enable debug logging for detailed traces:
-
-```python
-logging.basicConfig(level=logging.DEBUG)
-```
-
-## Best Practices
-
-1. **Conversation History Management**
- - Keep history concise (last N messages)
- - Summarize old conversations for long sessions
-
-2. **Error Handling**
- - Handle MCP tool failures gracefully
- - Implement retry logic with exponential backoff
-
-3. **Performance**
- - Use streaming for better user experience
- - Consider caching for frequent queries
-
-4. **Security**
- - Always validate MCP tool responses
- - Sanitize user inputs
- - Use bearer tokens for authentication
-
-## Troubleshooting
-
-### Common Issues
-
-**Issue**: Agent not using MCP tools
-- **Solution**: Verify `MCP_SERVER_URI` is set and server is running
-
-**Issue**: Infinite refinement loop
-- **Solution**: Check ReviewerAgent criteria are achievable, add max refinement limit
-
-**Issue**: Missing conversation context
-- **Solution**: Ensure history is properly loaded from state_store
-
-**Issue**: Workflow hangs
-- **Solution**: Check for unhandled message types, verify all edges are configured
-
-## Future Enhancements
-
-- [ ] Support for multi-modal inputs (images, files)
-- [ ] Parallel reviewer agents (consensus-based approval)
-- [ ] A/B testing of different review criteria
-- [ ] Metrics and analytics dashboard
-- [ ] Human-in-the-loop escalation for uncertain cases
-- [ ] Fine-tuned reviewer models
-
-## Related Examples
-
-- `reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern_azure.py` - Two-agent reflection
-- `reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop_azure.py` - Human escalation
-- `reference/agent-framework/python/samples/getting_started/workflows/control-flow/edge_condition.py` - Conditional routing
-
-## License
-
-This code is part of the OpenAI Workshop project. See LICENSE file for details.
-
-## Contributing
-
-Contributions are welcome! Please follow the project's contribution guidelines.
diff --git a/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py b/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py
index 059a3ac6f..dca76b14a 100644
--- a/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py
+++ b/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py
@@ -25,7 +25,7 @@
from agent_framework import ChatAgent, ChatMessage, Role, MCPStreamableHTTPTool
from agent_framework.azure import AzureOpenAIChatClient
-from agents.base_agent import BaseAgent
+from agents.base_agent import BaseAgent, ToolCallTrackingMixin
from agents.agent_framework.utils import create_filtered_tool_list
logger = logging.getLogger(__name__)
@@ -158,7 +158,7 @@ class IntentClassification(BaseModel):
"""
-class Agent(BaseAgent):
+class Agent(ToolCallTrackingMixin, BaseAgent):
"""
Optimized handoff pattern using vanilla workflow and direct agent communication.
@@ -184,6 +184,9 @@ def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: s
self._turn_key = f"{session_id}_handoff_turn"
self._current_turn = state_store.get(self._turn_key, 0)
+ # Initialize tool tracking from mixin
+ self.init_tool_tracking()
+
# Context transfer configuration: -1 = all history, 0 = none, N = last N turns
self._context_transfer_turns = int(os.getenv("HANDOFF_CONTEXT_TRANSFER_TURNS", "-1"))
@@ -510,6 +513,9 @@ async def chat_async(self, prompt: str) -> str:
"""
await self._setup_agents()
+ # Clear tool calls from previous request (from mixin)
+ self.clear_tool_calls()
+
# Increment turn counter
self._current_turn += 1
self.state_store[self._turn_key] = self._current_turn
@@ -589,18 +595,31 @@ async def chat_async(self, prompt: str) -> str:
# Process contents in the chunk
if hasattr(chunk, 'contents') and chunk.contents:
for content in chunk.contents:
- # Check for tool/function calls
+ # Check for tool/function calls - track with arguments
if content.type == "function_call":
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "tool_called",
- "agent_id": target_domain,
- "tool_name": content.name,
- "turn": self._current_turn,
- },
- )
+ if content.name:
+ # New function call - finalize previous and start new
+ self.track_function_call_start(content.name)
+
+ if self._ws_manager:
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "tool_called",
+ "agent_id": target_domain,
+ "tool_name": content.name,
+ "turn": self._current_turn,
+ },
+ )
+
+ # Accumulate arguments
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ # Function completed - finalize
+ self.finalize_tool_tracking()
# Extract text from chunk
if hasattr(chunk, 'text') and chunk.text:
@@ -620,6 +639,9 @@ async def chat_async(self, prompt: str) -> str:
except Exception as exc:
logger.error(f"[HANDOFF] Error during agent streaming: {exc}", exc_info=True)
raise
+
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
assistant_response = ''.join(full_response)
@@ -682,16 +704,26 @@ async def chat_async(self, prompt: str) -> str:
if hasattr(chunk, 'contents') and chunk.contents:
for content in chunk.contents:
if content.type == "function_call":
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "tool_called",
- "agent_id": new_target_domain,
- "tool_name": content.name,
- "turn": self._current_turn,
- },
- )
+ if content.name:
+ self.track_function_call_start(content.name)
+
+ if self._ws_manager:
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "tool_called",
+ "agent_id": new_target_domain,
+ "tool_name": content.name,
+ "turn": self._current_turn,
+ },
+ )
+
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ self.finalize_tool_tracking()
if hasattr(chunk, 'text') and chunk.text:
full_response_handoff.append(chunk.text)
@@ -709,6 +741,9 @@ async def chat_async(self, prompt: str) -> str:
logger.error(f"[HANDOFF] Error during handoff agent streaming: {exc}", exc_info=True)
raise
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
+
# Use handoff response
assistant_response = ''.join(full_response_handoff)
target_domain = new_target_domain
diff --git a/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py b/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py
index 7460a1eb1..9ac4db5c8 100644
--- a/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py
+++ b/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py
@@ -15,12 +15,15 @@
CheckpointStorage,
AgentRunUpdateEvent,
AgentRunEvent,
- MAGENTIC_EVENT_TYPE_ORCHESTRATOR,
- MAGENTIC_EVENT_TYPE_AGENT_DELTA,
+ MagenticOrchestratorEvent,
+ MagenticOrchestratorEventType,
+ RequestInfoEvent,
+ MagenticPlanReviewRequest,
+ MagenticPlanReviewResponse,
)
from agent_framework.azure import AzureOpenAIChatClient # type: ignore[import]
-from agents.base_agent import BaseAgent
+from agents.base_agent import BaseAgent, ToolCallTrackingMixin
from agents.agent_framework.utils import create_filtered_tool_list
logger = logging.getLogger(__name__)
@@ -104,7 +107,7 @@ def clear_all(self) -> None:
self._backing.pop("pending_prompt", None)
-class Agent(BaseAgent):
+class Agent(ToolCallTrackingMixin, BaseAgent):
"""Agent Framework implementation of the collaborative Magentic team."""
DEFAULT_MANAGER_INSTRUCTIONS = (
@@ -226,6 +229,9 @@ def __init__(
self._stream_agent_id: Optional[str] = None
self._stream_line_open: bool = False
self._last_agent_message: Optional[str] = None # Track last agent message for deduplication
+
+ # Initialize tool tracking from mixin
+ self.init_tool_tracking()
def set_websocket_manager(self, manager: Any) -> None:
"""Allow backend to inject WebSocket manager for streaming events."""
@@ -435,7 +441,7 @@ async def _build_workflow(
) -> Any:
participants = await self._create_participants(participant_client, tools)
- builder = MagenticBuilder().participants(**participants)
+ builder = MagenticBuilder().participants(list(participants.values()))
# Note: Streaming is now handled in _run_workflow by processing events from run_stream()
if self._ws_manager:
@@ -451,7 +457,7 @@ async def _build_workflow(
builder = (
builder
- .with_standard_manager(
+ .with_manager(
agent=manager_agent,
max_round_count=self._max_round_count,
max_stall_count=self._max_stall_count,
@@ -461,19 +467,13 @@ async def _build_workflow(
.with_checkpointing(checkpoint_storage)
)
- # Optional: enable plan review if available
+ # Optional: enable plan review
if self._enable_plan_review:
- enable_plan_review = getattr(builder, "enable_plan_review", None)
- if callable(enable_plan_review):
- try:
- builder = enable_plan_review()
- except Exception as exc:
- logger.warning(
- "[AgentFramework-Magentic] Failed to enable plan review: %s", exc
- )
- else:
- logger.debug(
- "[AgentFramework-Magentic] Plan review requested but not available in this framework version."
+ try:
+ builder = builder.with_plan_review(True)
+ except Exception as exc:
+ logger.warning(
+ "[AgentFramework-Magentic] Failed to enable plan review: %s", exc
)
return builder.build()
@@ -634,17 +634,62 @@ async def _run_workflow(
final_answer: str | None = None
try:
+ # Start the initial stream
if checkpoint_id:
event_stream = workflow.run_stream_from_checkpoint(checkpoint_id, checkpoint_storage)
else:
event_stream = workflow.run_stream(task)
-
- async for event in event_stream:
- # Stream events to WebSocket if available
- await self._process_workflow_event(event)
-
- if isinstance(event, WorkflowOutputEvent):
- final_answer = self._extract_text_from_event(event)
+
+ pending_responses: dict[str, Any] | None = None
+ output_received = False
+
+ while not output_received:
+ # If we have pending plan-review responses, resume via send_responses_streaming
+ if pending_responses is not None:
+ event_stream = workflow.send_responses_streaming(pending_responses)
+ pending_responses = None
+
+ pending_request: RequestInfoEvent | None = None
+
+ async for event in event_stream:
+ # Stream events to WebSocket if available
+ await self._process_workflow_event(event)
+
+ if isinstance(event, WorkflowOutputEvent):
+ final_answer = self._extract_text_from_event(event)
+ output_received = True
+
+ elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
+ # Capture plan review request β stream will pause after this
+ pending_request = event
+
+ # Handle plan review: auto-approve and loop back
+ if pending_request is not None and not output_received:
+ request_data = cast(MagenticPlanReviewRequest, pending_request.data)
+ logger.info(
+ "[AgentFramework-Magentic] Plan review requested (stalled=%s) β auto-approving",
+ request_data.is_stalled,
+ )
+
+ # Broadcast plan-review approval to WebSocket
+ if self._ws_manager:
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "orchestrator",
+ "kind": "plan_review_approved",
+ "content": "Plan auto-approved. Continuing execution...",
+ },
+ )
+
+ response = request_data.approve()
+ pending_responses = {pending_request.request_id: response}
+ pending_request = None
+ elif not output_received and pending_request is None:
+ # Stream ended without output and no plan review request β unexpected
+ logger.warning("[AgentFramework-Magentic] workflow stream ended without output or plan review")
+ break
+
except Exception as exc:
logger.error("[AgentFramework-Magentic] workflow failure: %s", exc, exc_info=True)
return None
@@ -660,51 +705,60 @@ async def _process_workflow_event(self, event: Any) -> None:
return
try:
- # Handle AgentRunUpdateEvent (streaming tokens and orchestrator messages)
- if isinstance(event, AgentRunUpdateEvent) and event.data:
- props = getattr(event.data, "additional_properties", None) or {}
- event_type = props.get("magentic_event_type")
-
- if event_type == MAGENTIC_EVENT_TYPE_ORCHESTRATOR:
- # Manager/orchestrator thinking or planning
- message_text = getattr(event.data, "text", "") or ""
- kind = props.get("orchestrator_message_kind", "")
+ # Handle MagenticOrchestratorEvent (plan, replan, progress ledger)
+ if isinstance(event, MagenticOrchestratorEvent):
+ message_text = getattr(event.data, "text", "") or str(event.data)
+ kind = event.event_type.value # e.g. "plan_created", "replanned", "progress_ledger_updated"
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "orchestrator",
+ "kind": kind,
+ "content": message_text,
+ },
+ )
+
+ # Handle RequestInfoEvent for plan review
+ elif isinstance(event, RequestInfoEvent) and event.request_type is MagenticPlanReviewRequest:
+ request_data = cast(MagenticPlanReviewRequest, event.data)
+ plan_text = getattr(request_data.plan, "text", "") or str(request_data.plan)
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "orchestrator",
+ "kind": "plan_review_requested",
+ "content": plan_text,
+ "is_stalled": request_data.is_stalled,
+ },
+ )
+
+ # Handle AgentRunUpdateEvent (streaming tokens from participant agents)
+ elif isinstance(event, AgentRunUpdateEvent) and event.data:
+ agent_id = event.executor_id
+
+ if self._stream_agent_id != agent_id or not self._stream_line_open:
+ self._stream_agent_id = agent_id
+ self._stream_line_open = True
await self._ws_manager.broadcast(
self.session_id,
{
- "type": "orchestrator",
- "kind": kind,
- "content": message_text,
+ "type": "agent_start",
+ "agent_id": agent_id,
+ "show_message_in_internal_process": True,
+ },
+ )
+
+ # Stream text tokens
+ text = getattr(event.data, "text", "") or ""
+ if text:
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "agent_token",
+ "agent_id": agent_id,
+ "content": text,
},
)
-
- elif event_type == MAGENTIC_EVENT_TYPE_AGENT_DELTA:
- # Streaming token from participant agent
- agent_id = event.executor_id
-
- if self._stream_agent_id != agent_id or not self._stream_line_open:
- self._stream_agent_id = agent_id
- self._stream_line_open = True
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "agent_start",
- "agent_id": agent_id,
- "show_message_in_internal_process": True,
- },
- )
-
- # Stream text tokens
- text = getattr(event.data, "text", "") or ""
- if text:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "agent_token",
- "agent_id": agent_id,
- "content": text,
- },
- )
# Handle AgentRunEvent (complete agent response)
elif isinstance(event, AgentRunEvent) and event.data:
diff --git a/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py b/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py
index 5a61e43e7..e06b020c8 100644
--- a/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py
+++ b/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py
@@ -13,7 +13,7 @@
from agent_framework import AgentThread, ChatAgent, MCPStreamableHTTPTool
from agent_framework.azure import AzureOpenAIChatClient
-from agents.base_agent import BaseAgent
+from agents.base_agent import BaseAgent, ToolCallTrackingMixin
logger = logging.getLogger(__name__)
@@ -37,7 +37,7 @@
}
-class Agent(BaseAgent):
+class Agent(ToolCallTrackingMixin, BaseAgent):
"""Reflection Agent with Primary Agent + Reviewer workflow."""
def __init__(
@@ -55,6 +55,8 @@ def __init__(
self._access_token = access_token
self._ws_manager = None
self._max_refinements = max_refinements
+ # Initialize tool tracking from mixin
+ self.init_tool_tracking()
logger.info(f"[Reflection] Initialized session: {session_id}")
def set_websocket_manager(self, manager: Any) -> None:
@@ -154,12 +156,48 @@ async def _run_agent(
prompt: str,
agent_id: str,
) -> str:
- """Run an agent with optional streaming."""
+ """Run an agent with optional streaming.
+
+ Even without WebSocket, we use run_stream to capture tool calls for evaluation.
+ """
if self._ws_manager:
return await self._run_agent_streaming(agent, prompt, agent_id)
else:
- result = await agent.run(prompt, thread=self._thread)
- return result.text
+ # Use run_stream even without WebSocket to capture tool calls
+ return await self._run_agent_non_streaming(agent, prompt, agent_id)
+
+ async def _run_agent_non_streaming(
+ self,
+ agent: ChatAgent,
+ prompt: str,
+ agent_id: str,
+ ) -> str:
+ """Run agent without WebSocket but still capture tool calls."""
+ chunks: List[str] = []
+
+ async for chunk in agent.run_stream(prompt, thread=self._thread):
+ # Track tool calls for evaluation
+ if hasattr(chunk, 'contents') and chunk.contents:
+ for content in chunk.contents:
+ if content.type == "function_call":
+ if content.name:
+ self.track_function_call_start(content.name)
+
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ self.finalize_tool_tracking()
+
+ # Collect text
+ if hasattr(chunk, 'text') and chunk.text:
+ chunks.append(chunk.text)
+
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
+
+ return ''.join(chunks)
async def _run_agent_streaming(
self,
@@ -179,15 +217,25 @@ async def _run_agent_streaming(
chunks: List[str] = []
async for chunk in agent.run_stream(prompt, thread=self._thread):
- # Handle tool calls
+ # Handle tool calls with argument tracking
if hasattr(chunk, 'contents') and chunk.contents:
for content in chunk.contents:
if content.type == "function_call":
- await self._broadcast_raw({
- "type": "tool_called",
- "agent_id": agent_id,
- "tool_name": content.name,
- })
+ if content.name:
+ self.track_function_call_start(content.name)
+
+ await self._broadcast_raw({
+ "type": "tool_called",
+ "agent_id": agent_id,
+ "tool_name": content.name,
+ })
+
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ self.finalize_tool_tracking()
# Stream text
if hasattr(chunk, 'text') and chunk.text:
@@ -198,6 +246,9 @@ async def _run_agent_streaming(
"content": chunk.text,
})
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
+
response = ''.join(chunks)
# Send complete message
@@ -222,6 +273,9 @@ async def chat_async(self, prompt: str) -> str:
if not self._primary_agent or not self._reviewer or not self._thread:
raise RuntimeError("Agents not initialized")
+ # Clear tool calls from previous request (from mixin)
+ self.clear_tool_calls()
+
# Notify start
await self._broadcast("plan", "π Reflection Workflow\n\nStarting Primary Agent β Reviewer pipeline...")
diff --git a/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py b/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py
deleted file mode 100644
index c23d3882e..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py
+++ /dev/null
@@ -1,645 +0,0 @@
-"""
-Agent Framework Workflow-based Reflection Agent
-
-This implementation uses the WorkflowBuilder pattern with a 3-party communication flow:
-User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if rejected)
-
-Key Design:
-- PrimaryAgent receives user messages but cannot send directly to user
-- All PrimaryAgent outputs go to ReviewerAgent for evaluation
-- ReviewerAgent acts as a conditional gate: approve or request_for_edit
-- Conversation history is maintained between user and PrimaryAgent only
-- History is passed to both agents for context
-"""
-
-import json
-import logging
-from dataclasses import dataclass
-from typing import Any, Dict, List
-from uuid import uuid4
-
-from agent_framework import (
- AgentRunResponseUpdate,
- AgentRunUpdateEvent,
- ChatMessage,
- Contents,
- Executor,
- MCPStreamableHTTPTool,
- Role,
- WorkflowBuilder,
- WorkflowContext,
- handler,
-)
-from agent_framework.azure import AzureOpenAIChatClient
-from pydantic import BaseModel
-
-from agents.base_agent import BaseAgent
-
-logger = logging.getLogger(__name__)
-
-
-class ReviewDecision(BaseModel):
- """Structured output from ReviewerAgent for reliable routing."""
- approved: bool
- feedback: str
-
-
-@dataclass
-class PrimaryAgentRequest:
- """Request sent to PrimaryAgent with conversation history."""
- request_id: str
- user_prompt: str
- conversation_history: list[ChatMessage]
-
-
-@dataclass
-class ReviewRequest:
- """Request sent from PrimaryAgent to ReviewerAgent."""
- request_id: str
- user_prompt: str
- conversation_history: list[ChatMessage]
- primary_agent_response: list[ChatMessage]
-
-
-@dataclass
-class ReviewResponse:
- """Response from ReviewerAgent back to PrimaryAgent."""
- request_id: str
- approved: bool
- feedback: str
-
-
-class PrimaryAgentExecutor(Executor):
- """
- Primary Agent - Customer Support Agent with MCP tools.
- Receives user messages and generates responses sent to ReviewerAgent for approval.
- """
-
- def __init__(
- self,
- id: str,
- chat_client: AzureOpenAIChatClient,
- tools: MCPStreamableHTTPTool | None = None,
- model: str | None = None,
- max_refinements: int = 3,
- ) -> None:
- super().__init__(id=id)
- self._chat_client = chat_client
- self._tools = tools
- self._model = model
- self._max_refinements = max_refinements
- # Track pending requests for retry with feedback
- self._pending_requests: dict[str, tuple[PrimaryAgentRequest, list[ChatMessage]]] = {}
- # Track refinement counts to prevent infinite loops
- self._refinement_counts: dict[str, int] = {}
-
- @handler
- async def handle_user_request(
- self, request: PrimaryAgentRequest, ctx: WorkflowContext[ReviewRequest]
- ) -> None:
- """Handle initial user request with conversation history."""
- print(f"[PrimaryAgent] Processing user request (ID: {request.request_id[:8]})")
- logger.info(f"[PrimaryAgent] Processing user request (ID: {request.request_id[:8]})")
-
- # Build message list with system prompt, history, and new user message
- messages = [
- ChatMessage(
- role=Role.SYSTEM,
- text=(
- "You are a helpful customer support assistant for Contoso company. "
- "You can help with billing, promotions, security, account information, and other customer inquiries. "
- "Use the available MCP tools to look up customer information, billing details, promotions, and security settings. "
- "When a customer provides an ID or asks about their account, use the tools to retrieve accurate, up-to-date information. "
- "Always be helpful, professional, and provide detailed information when available."
- ),
- )
- ]
-
- # Add conversation history for context
- messages.extend(request.conversation_history)
-
- # Add current user prompt
- messages.append(ChatMessage(role=Role.USER, text=request.user_prompt))
-
- print(f"[PrimaryAgent] Generating response with {len(messages)} messages in context")
- logger.info(f"[PrimaryAgent] Generating response with {len(messages)} messages in context")
-
- # Generate response
- response = await self._chat_client.get_response(
- messages=messages,
- tools=self._tools,
- model=self._model,
- )
-
- print(f"[PrimaryAgent] Response generated: {response.messages[-1].text[:100]}...")
- logger.info(f"[PrimaryAgent] Response generated")
-
- # Store full message context for potential retry
- all_messages = messages + response.messages
- self._pending_requests[request.request_id] = (request, all_messages)
-
- # Initialize refinement counter
- if request.request_id not in self._refinement_counts:
- self._refinement_counts[request.request_id] = 0
-
- # Send to ReviewerAgent for evaluation
- review_request = ReviewRequest(
- request_id=request.request_id,
- user_prompt=request.user_prompt,
- conversation_history=request.conversation_history,
- primary_agent_response=response.messages,
- )
-
- print(f"[PrimaryAgent] Sending response to ReviewerAgent for evaluation")
- logger.info(f"[PrimaryAgent] Sending response to ReviewerAgent for evaluation")
- await ctx.send_message(review_request)
-
- @handler
- async def handle_review_feedback(
- self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest]
- ) -> None:
- """Handle feedback from ReviewerAgent and regenerate if needed."""
- print(f"[PrimaryAgent] Received review (ID: {review.request_id[:8]}) - Approved: {review.approved}")
- logger.info(f"[PrimaryAgent] Received review (ID: {review.request_id[:8]}) - Approved: {review.approved}")
-
- if review.request_id not in self._pending_requests:
- logger.error(f"[PrimaryAgent] Unknown request ID: {review.request_id}")
- raise ValueError(f"Unknown request ID in review: {review.request_id}")
-
- original_request, messages = self._pending_requests.pop(review.request_id)
-
- if review.approved:
- print(f"[PrimaryAgent] Response approved! Sending to user via WorkflowAgent")
- logger.info(f"[PrimaryAgent] Response approved")
-
- # Clean up refinement counter
- self._refinement_counts.pop(review.request_id, None)
-
- # Extract contents from response to emit to user
- # The WorkflowAgent will handle emitting this to the external consumer
- # We don't send directly - ReviewerAgent will handle final emission
- return
-
- # Check if we've exceeded max refinements
- current_count = self._refinement_counts.get(review.request_id, 0)
- if current_count >= self._max_refinements:
- print(f"[PrimaryAgent] Max refinements ({self._max_refinements}) reached. Force approving response.")
- logger.warning(f"[PrimaryAgent] Max refinements reached for request {review.request_id[:8]}")
-
- # Clean up
- self._refinement_counts.pop(review.request_id, None)
-
- # Force emit the last response even though not approved
- # The ReviewerAgent already sent the ReviewResponse, so we're done
- return
-
- # Increment refinement counter
- self._refinement_counts[review.request_id] = current_count + 1
-
- # Not approved - incorporate feedback and regenerate
- print(f"[PrimaryAgent] Response not approved (attempt {current_count + 1}/{self._max_refinements}). Feedback: {review.feedback[:100]}...")
- logger.info(f"[PrimaryAgent] Regenerating with feedback (attempt {current_count + 1}/{self._max_refinements})")
-
- # Add feedback to message context
- messages.append(
- ChatMessage(
- role=Role.SYSTEM,
- text=f"REVIEWER FEEDBACK: {review.feedback}\n\nPlease improve your response based on this feedback.",
- )
- )
-
- # Add the original user prompt again for clarity
- messages.append(ChatMessage(role=Role.USER, text=original_request.user_prompt))
-
- # Regenerate response
- response = await self._chat_client.get_response(
- messages=messages,
- tools=self._tools,
- model=self._model,
- )
-
- print(f"[PrimaryAgent] New response generated: {response.messages[-1].text[:100]}...")
- logger.info(f"[PrimaryAgent] New response generated")
-
- # Update stored messages
- messages.extend(response.messages)
- self._pending_requests[review.request_id] = (original_request, messages)
-
- # Send updated response for re-review
- review_request = ReviewRequest(
- request_id=review.request_id,
- user_prompt=original_request.user_prompt,
- conversation_history=original_request.conversation_history,
- primary_agent_response=response.messages,
- )
-
- print(f"[PrimaryAgent] Sending refined response to ReviewerAgent")
- logger.info(f"[PrimaryAgent] Sending refined response to ReviewerAgent")
- await ctx.send_message(review_request)
-
-
-class ReviewerAgentExecutor(Executor):
- """
- Reviewer Agent - Quality assurance gate.
- Evaluates PrimaryAgent responses for accuracy, completeness, and professionalism.
- Acts as conditional gate: approved responses go to user, rejected go back to PrimaryAgent.
- """
-
- def __init__(
- self,
- id: str,
- chat_client: AzureOpenAIChatClient,
- tools: MCPStreamableHTTPTool | None = None,
- model: str | None = None,
- ) -> None:
- super().__init__(id=id)
- self._chat_client = chat_client
- self._tools = tools
- self._model = model
-
- @handler
- async def review_response(
- self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse]
- ) -> None:
- """
- Review the PrimaryAgent's response and decide: approve or request edit.
- Approved responses are emitted to user via AgentRunUpdateEvent.
- Rejected responses are sent back to PrimaryAgent with feedback.
- """
- print(f"[ReviewerAgent] Evaluating response (ID: {request.request_id[:8]})")
- logger.info(f"[ReviewerAgent] Evaluating response (ID: {request.request_id[:8]})")
-
- # Build review context with conversation history
- messages = [
- ChatMessage(
- role=Role.SYSTEM,
- text=(
- "You are a quality assurance reviewer for customer support responses. "
- "Review the customer support agent's response for:\n"
- "1. Accuracy of information\n"
- "2. Completeness of answer\n"
- "3. Professional tone\n"
- "4. Proper use of available tools\n"
- "5. Clarity and helpfulness\n\n"
- "Be reasonable in your evaluation. If the response is professional, addresses the customer's question, "
- "and provides useful information, APPROVE it. Only reject if there are significant issues.\n\n"
- "Respond with a structured JSON containing:\n"
- "- approved: true if response meets quality standards (be reasonable), false only for major issues\n"
- "- feedback: constructive feedback (if not approved) or brief approval note"
- ),
- )
- ]
-
- # Add conversation history for context
- messages.extend(request.conversation_history)
-
- # Add the user's question
- messages.append(ChatMessage(role=Role.USER, text=request.user_prompt))
-
- # Add the agent's response
- messages.extend(request.primary_agent_response)
-
- # Add explicit review instruction
- messages.append(
- ChatMessage(
- role=Role.USER,
- text="Please review the agent's response above and provide your assessment.",
- )
- )
-
- print(f"[ReviewerAgent] Sending review request to LLM")
- logger.info(f"[ReviewerAgent] Sending review request to LLM")
-
- # Get structured review decision
- response = await self._chat_client.get_response(
- messages=messages,
- response_format=ReviewDecision,
- tools=self._tools,
- model=self._model,
- )
-
- # Parse decision
- decision = ReviewDecision.model_validate_json(response.messages[-1].text)
-
- print(f"[ReviewerAgent] Review decision - Approved: {decision.approved}")
- if not decision.approved:
- print(f"[ReviewerAgent] Feedback: {decision.feedback[:100]}...")
- logger.info(f"[ReviewerAgent] Review decision - Approved: {decision.approved}")
-
- if decision.approved:
- # Emit approved response to external consumer (user)
- print(f"[ReviewerAgent] Emitting approved response to user")
- logger.info(f"[ReviewerAgent] Emitting approved response to user")
-
- contents: list[Contents] = []
- for message in request.primary_agent_response:
- contents.extend(message.contents)
-
- await ctx.add_event(
- AgentRunUpdateEvent(self.id, data=AgentRunResponseUpdate(contents=contents, role=Role.ASSISTANT))
- )
- else:
- # Send feedback back to PrimaryAgent for refinement
- print(f"[ReviewerAgent] Sending feedback to PrimaryAgent for refinement")
- logger.info(f"[ReviewerAgent] Sending feedback to PrimaryAgent for refinement")
-
- # Always send review response back to enable loop continuation
- await ctx.send_message(
- ReviewResponse(
- request_id=request.request_id,
- approved=decision.approved,
- feedback=decision.feedback,
- )
- )
-
-
-class Agent(BaseAgent):
- """
- Workflow-based Reflection Agent implementation.
-
- Implements a 3-party communication pattern:
- User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if not)
-
- Conversation history is maintained between user and PrimaryAgent only.
- Both agents receive history for context.
- """
-
- def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: str | None = None) -> None:
- super().__init__(state_store, session_id)
- self._workflow = None
- self._initialized = False
- self._access_token = access_token
- self._ws_manager = None
- self._mcp_tool = None # Store connected MCP tool
-
- # Track conversation history as ChatMessage objects
- self._conversation_history: list[ChatMessage] = []
- self._load_conversation_history()
-
- print(f"WORKFLOW REFLECTION AGENT INITIALIZED - Session: {session_id}")
- logger.info(f"WORKFLOW REFLECTION AGENT INITIALIZED - Session: {session_id}")
-
- def _load_conversation_history(self) -> None:
- """Load conversation history from state store and convert to ChatMessage format."""
- chat_history = self.chat_history # From BaseAgent
- for msg in chat_history:
- role = Role.USER if msg.get("role") == "user" else Role.ASSISTANT
- text = msg.get("content", "")
- self._conversation_history.append(ChatMessage(role=role, text=text))
-
- logger.info(f"Loaded {len(self._conversation_history)} messages from history")
-
- def set_websocket_manager(self, manager: Any) -> None:
- """Allow backend to inject WebSocket manager for streaming events."""
- self._ws_manager = manager
- logger.info(f"[STREAMING] WebSocket manager set for workflow reflection agent, session_id={self.session_id}")
-
- async def _setup_workflow(self) -> None:
- """Initialize the workflow with PrimaryAgent and ReviewerAgent executors."""
- if self._initialized:
- return
-
- if not all([self.azure_openai_key, self.azure_deployment, self.azure_openai_endpoint, self.api_version]):
- raise RuntimeError(
- "Azure OpenAI configuration is incomplete. Ensure AZURE_OPENAI_API_KEY, "
- "AZURE_OPENAI_CHAT_DEPLOYMENT, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_API_VERSION are set."
- )
-
- print(f"[WORKFLOW] Setting up workflow agents...")
- logger.info(f"[WORKFLOW] Setting up workflow agents")
-
- # Setup MCP tools if configured (create only once)
- if not self._mcp_tool:
- headers = self._build_headers()
- mcp_tools = await self._maybe_create_tools(headers)
- self._mcp_tool = mcp_tools[0] if mcp_tools else None
-
- if self._mcp_tool:
- print(f"[WORKFLOW] MCP tool created (will connect on first use)")
- logger.info(f"[WORKFLOW] MCP tool created")
-
- # Create Azure OpenAI chat client
- chat_client = AzureOpenAIChatClient(
- api_key=self.azure_openai_key,
- deployment_name=self.azure_deployment,
- endpoint=self.azure_openai_endpoint,
- api_version=self.api_version,
- )
-
- # Create executors
- primary_agent = PrimaryAgentExecutor(
- id="primary_agent",
- chat_client=chat_client,
- tools=self._mcp_tool,
- model=self.openai_model_name,
- )
-
- reviewer_agent = ReviewerAgentExecutor(
- id="reviewer_agent",
- chat_client=chat_client,
- tools=self._mcp_tool,
- model=self.openai_model_name,
- )
-
- print(f"[WORKFLOW] Building workflow graph: PrimaryAgent <-> ReviewerAgent")
- logger.info(f"[WORKFLOW] Building workflow graph")
-
- # Build workflow with bidirectional edges
- self._workflow = (
- WorkflowBuilder()
- .add_edge(primary_agent, reviewer_agent) # Primary -> Reviewer
- .add_edge(reviewer_agent, primary_agent) # Reviewer -> Primary (for feedback)
- .set_start_executor(primary_agent)
- .build()
- )
-
- self._initialized = True
- print(f"[WORKFLOW] Workflow initialization complete")
- logger.info(f"[WORKFLOW] Workflow initialization complete")
-
- def _build_headers(self) -> Dict[str, str]:
- """Build HTTP headers for MCP tool requests."""
- headers = {"Content-Type": "application/json"}
- if self._access_token:
- headers["Authorization"] = f"Bearer {self._access_token}"
- return headers
-
- async def _maybe_create_tools(self, headers: Dict[str, str]) -> List[MCPStreamableHTTPTool] | None:
- """Create MCP tools if server URI is configured."""
- if not self.mcp_server_uri:
- logger.warning("MCP_SERVER_URI not configured; agents run without MCP tools.")
- return None
-
- print(f"[WORKFLOW] Creating MCP tools with server: {self.mcp_server_uri}")
- return [
- MCPStreamableHTTPTool(
- name="mcp-streamable",
- url=self.mcp_server_uri,
- headers=headers,
- timeout=30,
- request_timeout=30,
- )
- ]
-
- async def chat_async(self, prompt: str) -> str:
- """
- Process user prompt through the reflection workflow.
-
- Flow:
- 1. Create PrimaryAgentRequest with conversation history
- 2. PrimaryAgent generates response
- 3. ReviewerAgent evaluates response
- 4. If approved -> return to user
- 5. If not approved -> PrimaryAgent refines with feedback (loop continues)
- """
- print(f"WORKFLOW REFLECTION AGENT chat_async called with prompt: {prompt[:50]}...")
- logger.info(f"WORKFLOW REFLECTION AGENT chat_async called with prompt: {prompt[:50]}...")
-
- await self._setup_workflow()
- if not self._workflow:
- raise RuntimeError("Workflow not initialized correctly.")
-
- # Create request with conversation history
- request_id = str(uuid4())
- request = PrimaryAgentRequest(
- request_id=request_id,
- user_prompt=prompt,
- conversation_history=self._conversation_history.copy(),
- )
-
- print(f"[WORKFLOW] Starting workflow execution (Request ID: {request_id[:8]})")
- logger.info(f"[WORKFLOW] Starting workflow execution")
-
- # Run workflow (streaming or non-streaming based on ws_manager)
- if self._ws_manager:
- print(f"[WORKFLOW] Using STREAMING mode")
- logger.info(f"[WORKFLOW] Using STREAMING mode")
- response_text = await self._run_workflow_streaming(request)
- else:
- print(f"[WORKFLOW] Using NON-STREAMING mode")
- logger.info(f"[WORKFLOW] Using NON-STREAMING mode")
- response_text = await self._run_workflow(request)
-
- # Update conversation history
- self._conversation_history.append(ChatMessage(role=Role.USER, text=prompt))
- self._conversation_history.append(ChatMessage(role=Role.ASSISTANT, text=response_text))
-
- # Update chat history in base class format
- messages = [
- {"role": "user", "content": prompt},
- {"role": "assistant", "content": response_text},
- ]
- self.append_to_chat_history(messages)
-
- print(f"[WORKFLOW] Workflow execution complete")
- logger.info(f"[WORKFLOW] Workflow execution complete")
-
- return response_text
-
- async def _run_workflow(self, request: PrimaryAgentRequest) -> str:
- """Run workflow in non-streaming mode."""
- # Run the workflow directly with the custom request
- response = await self._workflow.run(request)
-
- # Extract text from the workflow result
- response_text = response.output if hasattr(response, 'output') else str(response)
-
- print(f"[WORKFLOW] Response received: {response_text[:100]}...")
- logger.info(f"[WORKFLOW] Response received")
-
- return response_text
-
- async def _run_workflow_streaming(self, request: PrimaryAgentRequest) -> str:
- """Run workflow in streaming mode with WebSocket updates."""
-
- # Notify UI that workflow is starting
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "orchestrator",
- "kind": "plan",
- "content": "Workflow Reflection Pattern Starting\n\nInitiating PrimaryAgent β ReviewerAgent workflow for quality-assured responses...",
- },
- )
-
- response_text = ""
-
- try:
- async for event in self._workflow.run_stream(request):
- # Handle different event types
- event_str = str(event)
- print(f"[WORKFLOW STREAM] Event: {event_str[:100]}...")
-
- # Check if this is an AgentRunUpdateEvent with approved response
- if isinstance(event, AgentRunUpdateEvent):
- print(f"[WORKFLOW STREAM] AgentRunUpdateEvent detected from {event.executor_id}")
-
- # Extract response from the event data
- if hasattr(event, 'data') and isinstance(event.data, AgentRunResponseUpdate):
- # Extract text from contents
- for content in event.data.contents:
- if hasattr(content, 'text') and content.text:
- response_text += content.text
-
- # Stream to WebSocket
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "agent_token",
- "agent_id": "workflow_reflection",
- "content": content.text,
- },
- )
-
- print(f"[WORKFLOW STREAM] Extracted response text: {response_text[:100]}...")
-
- # Also check for text attribute directly on event
- elif hasattr(event, 'text') and event.text:
- response_text += event.text
-
- # Stream to WebSocket
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "agent_token",
- "agent_id": "workflow_reflection",
- "content": event.text,
- },
- )
-
- # Check for messages attribute
- elif hasattr(event, 'messages'):
- for msg in event.messages:
- if hasattr(msg, 'text') and msg.text:
- response_text = msg.text
-
- # Send final result
- if self._ws_manager and response_text:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "final_result",
- "content": response_text,
- },
- )
-
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "orchestrator",
- "kind": "result",
- "content": "Workflow Complete\n\nQuality-assured response delivered through PrimaryAgent β ReviewerAgent workflow!",
- },
- )
-
- except Exception as exc:
- logger.error(f"[WORKFLOW] Error during streaming: {exc}", exc_info=True)
- raise
-
- print(f"[WORKFLOW STREAM] Complete. Response length: {len(response_text)}")
- logger.info(f"[WORKFLOW STREAM] Complete")
-
- return response_text
diff --git a/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py b/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
deleted file mode 100644
index 072bd8985..000000000
--- a/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py
+++ /dev/null
@@ -1,226 +0,0 @@
-"""
-Test script for the Workflow-based Reflection Agent
-
-This script demonstrates the 3-party communication pattern:
-User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if not)
-
-Usage:
- python test_reflection_workflow_agent.py
-"""
-
-import asyncio
-import logging
-import os
-from typing import Dict, Any
-
-# Setup logging
-logging.basicConfig(
- level=logging.INFO,
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
-)
-
-logger = logging.getLogger(__name__)
-
-
-async def test_workflow_reflection_agent():
- """Test the workflow-based reflection agent."""
-
- print("=" * 70)
- print("WORKFLOW REFLECTION AGENT TEST")
- print("=" * 70)
- print()
-
- # Check environment variables
- required_env_vars = [
- "AZURE_OPENAI_API_KEY",
- "AZURE_OPENAI_CHAT_DEPLOYMENT",
- "AZURE_OPENAI_ENDPOINT",
- "AZURE_OPENAI_API_VERSION",
- "OPENAI_MODEL_NAME",
- ]
-
- print("Checking environment variables...")
- missing_vars = [var for var in required_env_vars if not os.getenv(var)]
- if missing_vars:
- print(f"β Missing environment variables: {', '.join(missing_vars)}")
- print("\nPlease set the following environment variables:")
- for var in missing_vars:
- print(f" - {var}")
- return
-
- print("β All required environment variables are set")
- print()
-
- # Optional MCP server
- mcp_uri = os.getenv("MCP_SERVER_URI")
- if mcp_uri:
- print(f"β MCP Server configured: {mcp_uri}")
- else:
- print("βΉ MCP Server not configured (agents will work without MCP tools)")
- print()
-
- # Import the agent (after env check to avoid import errors)
- try:
- from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
- except ImportError as e:
- print(f"β Failed to import Agent: {e}")
- print("\nMake sure you're running from the project root directory:")
- print(" python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py")
- return
-
- # Create state store and agent
- state_store: Dict[str, Any] = {}
- session_id = "test_session_001"
-
- print(f"Creating Workflow Reflection Agent (Session: {session_id})...")
- agent = Agent(state_store=state_store, session_id=session_id)
- print("β Agent created successfully")
- print()
-
- # Test queries
- test_queries = [
- "What is the capital of France?",
- "Can you help me with customer ID 1?",
- ]
-
- for i, query in enumerate(test_queries, 1):
- print("=" * 70)
- print(f"TEST QUERY {i}: {query}")
- print("=" * 70)
- print()
-
- try:
- print(f"Sending query to agent...")
- print(f"Expected flow: User -> PrimaryAgent -> ReviewerAgent -> (approve/reject)")
- print()
-
- response = await agent.chat_async(query)
-
- print()
- print("-" * 70)
- print("FINAL RESPONSE:")
- print("-" * 70)
- print(response)
- print()
-
- print("β Query completed successfully")
- print()
-
- except Exception as e:
- print(f"β Error during query: {e}")
- logger.error(f"Error during query: {e}", exc_info=True)
- print()
-
- print("=" * 70)
- print("TEST COMPLETE")
- print("=" * 70)
- print()
- print("Summary:")
- print(f"- Total queries tested: {len(test_queries)}")
- print(f"- Session ID: {session_id}")
- print(f"- Conversation history entries: {len(state_store.get(f'{session_id}_chat_history', []))}")
- print()
- print("Key features demonstrated:")
- print(" β 3-party communication pattern (User -> PrimaryAgent -> ReviewerAgent)")
- print(" β Conditional gate (approve/reject)")
- print(" β Conversation history maintenance")
- print(" β Iterative refinement loop")
- print()
-
-
-async def test_with_mcp_tools():
- """Test with actual MCP tools if configured."""
-
- print("=" * 70)
- print("WORKFLOW REFLECTION AGENT TEST WITH MCP TOOLS")
- print("=" * 70)
- print()
-
- if not os.getenv("MCP_SERVER_URI"):
- print("β MCP_SERVER_URI not configured. Skipping MCP test.")
- print("To test with MCP tools, set the MCP_SERVER_URI environment variable.")
- return
-
- # Import the agent
- try:
- from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent
- except ImportError as e:
- print(f"β Failed to import Agent: {e}")
- return
-
- # Create state store and agent
- state_store: Dict[str, Any] = {}
- session_id = "test_session_mcp_001"
-
- print(f"Creating Workflow Reflection Agent with MCP tools (Session: {session_id})...")
- agent = Agent(state_store=state_store, session_id=session_id)
- print("β Agent created successfully")
- print()
-
- # Test MCP-specific queries
- mcp_queries = [
- "Can you list all customers?",
- "What are the billing details for customer ID 1?",
- "What promotions are available for customer 1?",
- ]
-
- for i, query in enumerate(mcp_queries, 1):
- print("=" * 70)
- print(f"MCP TEST QUERY {i}: {query}")
- print("=" * 70)
- print()
-
- try:
- print(f"Sending query to agent (expects MCP tool usage)...")
- print(f"Expected: PrimaryAgent will use MCP tools, ReviewerAgent will verify accuracy")
- print()
-
- response = await agent.chat_async(query)
-
- print()
- print("-" * 70)
- print("FINAL RESPONSE:")
- print("-" * 70)
- print(response)
- print()
-
- print("β MCP query completed successfully")
- print()
-
- except Exception as e:
- print(f"β Error during MCP query: {e}")
- logger.error(f"Error during MCP query: {e}", exc_info=True)
- print()
-
- print("=" * 70)
- print("MCP TEST COMPLETE")
- print("=" * 70)
-
-
-def main():
- """Main entry point."""
- print()
- print("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
- print("β WORKFLOW-BASED REFLECTION AGENT TEST SUITE β")
- print("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
- print()
-
- # Run basic test
- asyncio.run(test_workflow_reflection_agent())
-
- print()
- print("-" * 70)
- print()
-
- # Run MCP test if configured
- asyncio.run(test_with_mcp_tools())
-
- print()
- print("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
- print("β ALL TESTS COMPLETE β")
- print("βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ")
- print()
-
-
-if __name__ == "__main__":
- main()
diff --git a/agentic_ai/agents/agent_framework/single_agent.py b/agentic_ai/agents/agent_framework/single_agent.py
index 5b2527031..ae0a4c178 100644
--- a/agentic_ai/agents/agent_framework/single_agent.py
+++ b/agentic_ai/agents/agent_framework/single_agent.py
@@ -1,16 +1,15 @@
-import json
import logging
from typing import Any, Dict, List
from agent_framework import AgentThread, ChatAgent, MCPStreamableHTTPTool
from agent_framework.azure import AzureOpenAIChatClient
-from agents.base_agent import BaseAgent
+from agents.base_agent import BaseAgent, ToolCallTrackingMixin
logger = logging.getLogger(__name__)
-class Agent(BaseAgent):
+class Agent(ToolCallTrackingMixin, BaseAgent):
"""Agent Framework implementation of a single assistant loop."""
def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: str | None = None) -> None:
@@ -23,6 +22,8 @@ def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: s
# Track conversation turn for tool call grouping - load from state store
self._turn_key = f"{session_id}_current_turn"
self._current_turn = state_store.get(self._turn_key, 0)
+ # Initialize tool tracking from mixin
+ self.init_tool_tracking()
def set_websocket_manager(self, manager: Any) -> None:
"""Allow backend to inject WebSocket manager for streaming events."""
@@ -148,12 +149,17 @@ async def _log_mcp_tool_details(self) -> None:
logger.debug("No tools returned from MCP server during inspection.")
return
+ # get_tool_calls() is inherited from ToolCallTrackingMixin
+
async def chat_async(self, prompt: str) -> str:
await self._setup_single_agent()
if not self._agent or not self._thread:
raise RuntimeError("Agent Framework single agent failed to initialize correctly.")
+ # Clear tool calls from previous request (from mixin)
+ self.clear_tool_calls()
+
# Increment turn counter for this new conversation turn and persist to state store
self._current_turn += 1
self.state_store[self._turn_key] = self._current_turn
@@ -162,9 +168,37 @@ async def chat_async(self, prompt: str) -> str:
if self._ws_manager:
return await self._chat_async_streaming(prompt)
- # Non-streaming path
- response = await self._agent.run(prompt, thread=self._thread)
- assistant_response = response.text
+ # Non-streaming path - use run_stream to capture tool calls
+ full_response = []
+ async for chunk in self._agent.run_stream(prompt, thread=self._thread):
+ # Extract tool calls from contents
+ if hasattr(chunk, 'contents') and chunk.contents:
+ for content in chunk.contents:
+ if content.type == "function_call":
+ # Function call chunks come in pieces:
+ # 1. First chunk has name, empty arguments
+ # 2. Subsequent chunks have no name, partial arguments
+ if content.name:
+ # New function call starting - finalize previous if any
+ self.track_function_call_start(content.name)
+
+ # Accumulate arguments
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ # Function result means the call is complete
+ self.finalize_tool_tracking()
+
+ # Extract text
+ if hasattr(chunk, 'text') and chunk.text:
+ full_response.append(chunk.text)
+
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
+
+ assistant_response = ''.join(full_response)
messages = [
{"role": "user", "content": prompt},
@@ -192,7 +226,7 @@ async def _chat_async_streaming(self, prompt: str) -> str:
"show_message_in_internal_process": False, # Convention: don't show message in left panel
},
)
-
+
# Stream the response
full_response = []
@@ -201,18 +235,32 @@ async def _chat_async_streaming(self, prompt: str) -> str:
# Process contents in the chunk
if hasattr(chunk, 'contents') and chunk.contents:
for content in chunk.contents:
- # Check for tool/function calls - only broadcast the tool name
+ # Handle function calls - accumulate arguments across chunks
if content.type == "function_call":
- if self._ws_manager:
- await self._ws_manager.broadcast(
- self.session_id,
- {
- "type": "tool_called",
- "agent_id": "single_agent",
- "tool_name": content.name,
- "turn": self._current_turn,
- },
- )
+ if content.name:
+ # New function call - finalize previous and start new
+ self.track_function_call_start(content.name)
+
+ # Broadcast that a tool is being called
+ if self._ws_manager:
+ await self._ws_manager.broadcast(
+ self.session_id,
+ {
+ "type": "tool_called",
+ "agent_id": "single_agent",
+ "tool_name": content.name,
+ "turn": self._current_turn,
+ },
+ )
+
+ # Accumulate arguments
+ args_chunk = getattr(content, 'arguments', '')
+ if args_chunk:
+ self.track_function_call_arguments(args_chunk)
+
+ elif content.type == "function_result":
+ # Function completed - finalize
+ self.finalize_tool_tracking()
# Extract text from chunk
if hasattr(chunk, 'text') and chunk.text:
@@ -231,6 +279,9 @@ async def _chat_async_streaming(self, prompt: str) -> str:
except Exception as exc:
logger.error("[STREAMING] Error during single agent streaming: %s", exc, exc_info=True)
raise
+
+ # Finalize any remaining function call
+ self.finalize_tool_tracking()
assistant_response = ''.join(full_response)
diff --git a/agentic_ai/agents/base_agent.py b/agentic_ai/agents/base_agent.py
index fb8bbd6f9..7380b4ff1 100644
--- a/agentic_ai/agents/base_agent.py
+++ b/agentic_ai/agents/base_agent.py
@@ -1,4 +1,5 @@
import os
+import json
import logging
from typing import Any, Dict, List, Optional, Union
from dotenv import load_dotenv
@@ -7,7 +8,96 @@
from azure.core.credentials import TokenCredential
load_dotenv() # Load environment variables from .env file if needed
-
+
+
+class ToolCallTrackingMixin:
+ """
+ Mixin class that provides tool call tracking functionality.
+
+ Use this mixin in agents that need to track tool calls for evaluation.
+ The mixin handles:
+ - Accumulating streaming function call arguments
+ - Finalizing function calls with parsed arguments
+ - Providing access to tool calls made during a request
+
+ Usage:
+ class MyAgent(ToolCallTrackingMixin, BaseAgent):
+ def __init__(self, state_store, session_id):
+ super().__init__(state_store, session_id)
+ self.init_tool_tracking() # Call this in __init__
+ """
+
+ def init_tool_tracking(self) -> None:
+ """Initialize tool tracking state. Call this in agent's __init__."""
+ self._tool_calls: List[Dict[str, Any]] = []
+ self._current_function_call: Dict[str, Any] | None = None
+ self._current_function_args: List[str] = []
+
+ def clear_tool_calls(self) -> None:
+ """Clear tool calls from previous request. Call at start of chat_async."""
+ self._tool_calls = []
+ self._current_function_call = None
+ self._current_function_args = []
+
+ def get_tool_calls(self) -> List[Dict[str, Any]]:
+ """Return the list of tool calls made during the last request.
+
+ Returns list of dicts with:
+ - name: tool name
+ - args: arguments passed to the tool
+ """
+ return self._tool_calls.copy()
+
+ def track_function_call_start(self, name: str) -> None:
+ """Start tracking a new function call. Call when function_call content is received."""
+ # Finalize any previous function call first
+ self._finalize_current_function_call()
+ self._current_function_call = {"name": name}
+ self._current_function_args = []
+
+ def track_function_call_arguments(self, arguments: str) -> None:
+ """Accumulate streaming function call arguments."""
+ if arguments:
+ self._current_function_args.append(arguments)
+
+ def _finalize_current_function_call(self) -> None:
+ """Finalize the current function call by parsing accumulated arguments."""
+ if self._current_function_call is None:
+ return
+
+ # Join accumulated argument chunks
+ args_str = ''.join(self._current_function_args)
+
+ # Parse the arguments
+ args = {}
+ if args_str:
+ try:
+ args = json.loads(args_str)
+ except json.JSONDecodeError:
+ # If JSON parsing fails, store raw string
+ args = {"_raw": args_str} if args_str.strip() else {}
+
+ self._tool_calls.append({
+ "name": self._current_function_call["name"],
+ "args": args
+ })
+
+ # Reset accumulators
+ self._current_function_call = None
+ self._current_function_args = []
+
+ def finalize_tool_tracking(self) -> None:
+ """Finalize any pending function calls. Call at end of streaming."""
+ self._finalize_current_function_call()
+
+ def add_tool_call(self, name: str, args: Dict[str, Any] | None = None) -> None:
+ """Directly add a tool call (for non-streaming scenarios)."""
+ self._tool_calls.append({
+ "name": name,
+ "args": args or {}
+ })
+
+
class BaseAgent:
"""
Base class for all agents.
diff --git a/agentic_ai/applications/.env.sample b/agentic_ai/applications/.env.sample
index 6608c22cc..a014a428f 100644
--- a/agentic_ai/applications/.env.sample
+++ b/agentic_ai/applications/.env.sample
@@ -98,3 +98,20 @@ MAGENTIC_MAX_ROUNDS=10
# 0 = no context transfer (domain isolation)
# N = transfer last N turns (1 turn = user message + assistant response)
HANDOFF_CONTEXT_TRANSFER_TURNS=-1
+
+############################################
+# Application Insights Observability #
+############################################
+# Connection string from Azure Portal > Application Insights > Overview > Connection String
+# APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEndpoint=https://xxx.in.applicationinsights.azure.com/;LiveEndpoint=https://xxx.livediagnostics.monitor.azure.com/"
+
+# Enable sensitive data in traces (prompts, responses) - DEV ONLY!
+# ENABLE_SENSITIVE_DATA=true
+
+# Service name for telemetry (appears in Application Insights and Grafana)
+# OTEL_SERVICE_NAME="contoso-agent"
+
+# Grafana Dashboards (after setting up Azure Managed Grafana):
+# - Agent Overview: https://aka.ms/amg/dash/af-agent
+# - Workflow Overview: https://aka.ms/amg/dash/af-workflow
+
diff --git a/agentic_ai/applications/Dockerfile b/agentic_ai/applications/Dockerfile
index ff39a2f98..fed9c28e8 100644
--- a/agentic_ai/applications/Dockerfile
+++ b/agentic_ai/applications/Dockerfile
@@ -37,9 +37,13 @@ RUN --mount=type=cache,target=/root/.cache/uv \
COPY applications/backend.py applications/utils.py ./
# Copy ONLY agent_framework directory (not all agents)
+COPY agents/__init__.py /app/agents/__init__.py
COPY agents/agent_framework /app/agents/agent_framework
COPY agents/base_agent.py /app/agents/base_agent.py
+# Copy observability module
+COPY observability /app/observability
+
# Copy built frontend from previous stage (Vite outputs to 'dist')
COPY --from=frontend-builder /app/frontend/dist ./static
diff --git a/agentic_ai/applications/backend.py b/agentic_ai/applications/backend.py
index 56cf293ef..718177b0b 100644
--- a/agentic_ai/applications/backend.py
+++ b/agentic_ai/applications/backend.py
@@ -15,6 +15,9 @@
from pathlib import Path
from typing import Dict, List, Any, Optional, Set, DefaultDict
from collections import defaultdict
+
+# Add parent directory to path for observability module
+sys.path.insert(0, str(Path(__file__).parent.parent))
import httpx
import jwt
@@ -29,10 +32,29 @@
from dotenv import load_dotenv
# ------------------------------------------------------------------
-# Environment
+# Environment (load first so observability can read connection string)
# ------------------------------------------------------------------
load_dotenv() # read .env if present
+# ------------------------------------------------------------------
+# Observability (must be before any agent imports)
+# ------------------------------------------------------------------
+from observability import setup_observability
+
+# Initialize Application Insights tracing if configured
+# All agents (single, reflection, handoff, etc.) are automatically traced
+_observability_enabled = setup_observability(
+ service_name="contoso-agent-backend",
+ enable_live_metrics=True,
+ enable_sensitive_data=os.getenv("ENABLE_SENSITIVE_DATA", "false").lower() in ("1", "true", "yes"),
+)
+if _observability_enabled:
+ logging.getLogger(__name__).info("β
Application Insights observability enabled")
+
+# ------------------------------------------------------------------
+# Auth Configuration
+# ------------------------------------------------------------------
+
# Feature flag: disable auth for local dev / demos
DISABLE_AUTH = os.getenv("DISABLE_AUTH", "false").lower() in ("1", "true", "yes")
@@ -286,7 +308,8 @@ class ChatRequest(BaseModel):
class ChatResponse(BaseModel):
- response: str
+ response: str
+ tools_used: List[Dict[str, Any]] = [] # List of {name: str, args: dict}
class ConversationHistoryResponse(BaseModel):
@@ -325,8 +348,16 @@ async def chat(req: ChatRequest, token: str = Depends(verify_token)):
agent = Agent(STATE_STORE, req.session_id, access_token=token)
except TypeError:
agent = Agent(STATE_STORE, req.session_id)
- answer = await agent.chat_async(req.prompt)
- return ChatResponse(response=answer)
+ answer = await agent.chat_async(req.prompt)
+
+ # Get tool calls if the agent tracks them
+ tools_used = []
+ if hasattr(agent, 'get_tool_calls'):
+ tools_used = agent.get_tool_calls()
+ elif hasattr(agent, '_tool_calls'):
+ tools_used = agent._tool_calls
+
+ return ChatResponse(response=answer, tools_used=tools_used)
@app.post("/reset_session")
async def reset_session(req: SessionResetRequest, token: str = Depends(verify_token)):
@@ -418,6 +449,45 @@ async def set_active_agent(req: SetAgentRequest, token: str = Depends(verify_tok
"message": "Failed to load agent."
}
+# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+# Diagnostic: check observability status
+# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+@app.get("/api/diagnostics/observability")
+async def diagnostics_observability():
+ """Check if observability is configured and working."""
+ import importlib
+ diag: Dict[str, Any] = {
+ "observability_enabled": _observability_enabled,
+ "connection_string_set": bool(os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")),
+ }
+ # Check key imports
+ for mod_name in ["azure.monitor.opentelemetry", "agent_framework.observability", "opentelemetry"]:
+ try:
+ importlib.import_module(mod_name)
+ diag[f"import_{mod_name}"] = "ok"
+ except Exception as e:
+ diag[f"import_{mod_name}"] = str(e)
+ # Check OTel tracer provider
+ try:
+ from opentelemetry import trace
+ tp = trace.get_tracer_provider()
+ diag["tracer_provider"] = type(tp).__name__
+ if hasattr(tp, '_active_span_processor'):
+ proc = tp._active_span_processor
+ diag["span_processors"] = type(proc).__name__
+ if hasattr(proc, '_span_processors'):
+ diag["span_processor_list"] = [type(sp).__name__ for sp in proc._span_processors]
+ except Exception as e:
+ diag["tracer_provider_error"] = str(e)
+ # Check OTel meter provider
+ try:
+ from opentelemetry import metrics
+ mp = metrics.get_meter_provider()
+ diag["meter_provider"] = type(mp).__name__
+ except Exception as e:
+ diag["meter_provider_error"] = str(e)
+ return diag
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Root route to serve React app
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
diff --git a/agentic_ai/applications/pyproject.toml b/agentic_ai/applications/pyproject.toml
index dad4fd577..eacd63989 100644
--- a/agentic_ai/applications/pyproject.toml
+++ b/agentic_ai/applications/pyproject.toml
@@ -5,10 +5,11 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
- "agent-framework==1.0.0b260107",
- "autogen-agentchat==0.7.1",
- "autogen-ext[mcp]==0.7.1",
+ "agent-framework==1.0.0b260130",
+ "azure-ai-evaluation>=1.14.0",
+ "azure-ai-projects>=2.0.0b2",
"azure-cosmos==4.9.0",
+ "azure-monitor-opentelemetry>=1.6.0",
"fastapi==0.115.12",
"flasgger==0.9.7.1",
"flask==3.0.3",
@@ -26,3 +27,12 @@ dependencies = [
[tool.uv]
prerelease = "allow"
+
+[dependency-groups]
+dev = [
+ "azure-identity>=1.26.0b1",
+ "azure-keyvault-secrets>=4.10.0",
+ "pytest>=9.0.2",
+ "pytest-asyncio>=1.3.0",
+ "pytest-timeout>=2.4.0",
+]
diff --git a/agentic_ai/applications/requirements.txt b/agentic_ai/applications/requirements.txt
index dddda8770..01d01b186 100644
--- a/agentic_ai/applications/requirements.txt
+++ b/agentic_ai/applications/requirements.txt
@@ -2,99 +2,150 @@
# uv pip compile pyproject.toml -o requirements.txt
a2a-sdk==0.3.12
# via agent-framework-a2a
-agent-framework==1.0.0b251028
+ag-ui-protocol==0.1.10
+ # via agent-framework-ag-ui
+agent-framework==1.0.0b260130
# via applications (pyproject.toml)
agent-framework-a2a==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
+agent-framework-ag-ui==1.0.0b260130
+ # via agent-framework-core
+agent-framework-anthropic==1.0.0b260130
+ # via agent-framework-core
agent-framework-azure-ai==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
+agent-framework-azure-ai-search==1.0.0b260130
+ # via agent-framework-core
+agent-framework-azurefunctions==1.0.0b260130
+ # via agent-framework-core
+agent-framework-chatkit==1.0.0b260130
+ # via agent-framework-core
agent-framework-copilotstudio==1.0.0b251114
- # via agent-framework
-agent-framework-core==1.0.0b251114
+ # via agent-framework-core
+agent-framework-core==1.0.0b260130
# via
# agent-framework
# agent-framework-a2a
+ # agent-framework-ag-ui
+ # agent-framework-anthropic
# agent-framework-azure-ai
+ # agent-framework-azure-ai-search
+ # agent-framework-azurefunctions
+ # agent-framework-chatkit
# agent-framework-copilotstudio
+ # agent-framework-declarative
# agent-framework-devui
+ # agent-framework-durabletask
+ # agent-framework-github-copilot
# agent-framework-lab
# agent-framework-mem0
+ # agent-framework-ollama
# agent-framework-purview
# agent-framework-redis
+agent-framework-declarative==1.0.0b260130
+ # via agent-framework-core
agent-framework-devui==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
+agent-framework-durabletask==1.0.0b260130
+ # via
+ # agent-framework-azurefunctions
+ # agent-framework-core
+agent-framework-github-copilot==1.0.0b260130
+ # via agent-framework-core
agent-framework-lab==1.0.0b251024
- # via agent-framework
+ # via agent-framework-core
agent-framework-mem0==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
+agent-framework-ollama==1.0.0b260130
+ # via agent-framework-core
agent-framework-purview==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
agent-framework-redis==1.0.0b251114
- # via agent-framework
+ # via agent-framework-core
aiohappyeyeballs==2.6.1
# via aiohttp
aiohttp==3.13.2
# via
# agent-framework-azure-ai
- # semantic-kernel
-aioice==0.10.1
- # via aiortc
-aiortc==1.14.0
- # via semantic-kernel
+ # azure-ai-evaluation
+ # azure-functions-durable
aiosignal==1.4.0
# via aiohttp
altair==5.6.0.dev20251110
# via streamlit
annotated-types==0.7.0
# via pydantic
+anthropic==0.78.0
+ # via agent-framework-anthropic
anyio==4.11.0
# via
+ # anthropic
# httpx
# mcp
# openai
# sse-starlette
# starlette
# watchfiles
+asgiref==3.11.1
+ # via opentelemetry-instrumentation-asgi
+asyncio==4.0.0
+ # via durabletask
attrs==25.4.0
# via
# aiohttp
# jsonschema
# referencing
-autogen-agentchat==0.7.1
- # via applications (pyproject.toml)
-autogen-core==0.7.1
- # via
- # autogen-agentchat
- # autogen-ext
-autogen-ext==0.7.1
- # via applications (pyproject.toml)
-av==16.0.1
- # via aiortc
azure-ai-agents==1.2.0b5
- # via
- # agent-framework-azure-ai
- # semantic-kernel
+ # via agent-framework-azure-ai
+azure-ai-evaluation==1.14.0
+ # via applications (pyproject.toml)
azure-ai-projects==2.0.0b2
# via
+ # applications (pyproject.toml)
# agent-framework-azure-ai
- # semantic-kernel
+azure-common==1.1.28
+ # via azure-search-documents
azure-core==1.36.0
# via
# agent-framework-purview
# azure-ai-agents
+ # azure-ai-evaluation
# azure-ai-projects
+ # azure-core-tracing-opentelemetry
# azure-cosmos
# azure-identity
+ # azure-monitor-opentelemetry
+ # azure-monitor-opentelemetry-exporter
+ # azure-search-documents
# azure-storage-blob
# microsoft-agents-hosting-core
+ # msrest
+azure-core-tracing-opentelemetry==1.0.0b12
+ # via azure-monitor-opentelemetry
azure-cosmos==4.9.0
# via applications (pyproject.toml)
+azure-functions==1.25.0b3.dev3
+ # via
+ # agent-framework-azurefunctions
+ # azure-functions-durable
+azure-functions-durable==1.4.0
+ # via agent-framework-azurefunctions
azure-identity==1.26.0b1
# via
# agent-framework-core
- # semantic-kernel
+ # azure-ai-evaluation
+ # azure-monitor-opentelemetry-exporter
+ # durabletask-azuremanaged
+azure-monitor-opentelemetry==1.8.6
+ # via applications (pyproject.toml)
+azure-monitor-opentelemetry-exporter==1.0.0b48
+ # via azure-monitor-opentelemetry
+azure-search-documents==11.7.0b2
+ # via agent-framework-azure-ai-search
azure-storage-blob==12.27.1
- # via azure-ai-projects
+ # via
+ # azure-ai-evaluation
+ # azure-ai-projects
backoff==2.2.1
# via posthog
blinker==1.9.0
@@ -109,48 +160,52 @@ certifi==2025.11.12
# via
# httpcore
# httpx
+ # msrest
# requests
cffi==2.0.0
# via
+ # clr-loader
# cryptography
- # pylibsrtp
-chardet==5.2.0
- # via prance
+ # powerfx
charset-normalizer==3.4.4
# via requests
click==8.3.1
# via
# flask
+ # nltk
# streamlit
# uvicorn
-cloudevents==1.12.0
- # via semantic-kernel
+clr-loader==0.2.10
+ # via pythonnet
colorama==0.4.6
# via
# click
+ # griffe
# tqdm
# uvicorn
cryptography==45.0.7
# via
- # aiortc
# azure-identity
# azure-storage-blob
# msal
# pyjwt
- # pyopenssl
-defusedxml==0.8.0rc2
- # via semantic-kernel
-deprecation==2.1.0
- # via cloudevents
distro==1.9.0
# via
+ # anthropic
# openai
# posthog
-dnspython==2.8.0
- # via aioice
+docstring-parser==0.17.0
+ # via anthropic
+durabletask==1.3.0
+ # via
+ # agent-framework-durabletask
+ # durabletask-azuremanaged
+durabletask-azuremanaged==1.3.0
+ # via agent-framework-durabletask
fastapi==0.115.12
# via
# applications (pyproject.toml)
+ # agent-framework-ag-ui
# agent-framework-devui
flasgger==0.9.7.1
# via applications (pyproject.toml)
@@ -162,25 +217,27 @@ frozenlist==1.8.0
# via
# aiohttp
# aiosignal
+furl==2.1.4
+ # via azure-functions-durable
gitdb==4.0.12
# via gitpython
+github-copilot-sdk==0.1.22
+ # via agent-framework-github-copilot
gitpython==3.1.45
# via streamlit
google-api-core==2.28.1
# via a2a-sdk
google-auth==2.43.0
# via google-api-core
-google-crc32c==1.7.1
- # via aiortc
googleapis-common-protos==1.72.0
- # via
- # google-api-core
- # opentelemetry-exporter-otlp-proto-grpc
+ # via google-api-core
greenlet==3.2.4
# via sqlalchemy
+griffe==1.15.0
+ # via openai-agents
grpcio==1.76.0
# via
- # opentelemetry-exporter-otlp-proto-grpc
+ # durabletask
# qdrant-client
h11==0.16.0
# via
@@ -199,7 +256,10 @@ httpx==0.28.1
# applications (pyproject.toml)
# a2a-sdk
# agent-framework-purview
+ # anthropic
+ # azure-ai-evaluation
# mcp
+ # ollama
# openai
# qdrant-client
httpx-sse==0.4.3
@@ -214,57 +274,48 @@ idna==3.11
# httpx
# requests
# yarl
-ifaddr==0.2.0
- # via aioice
importlib-metadata==8.7.0
# via opentelemetry-api
isodate==0.7.2
# via
# azure-ai-agents
# azure-ai-projects
+ # azure-search-documents
# azure-storage-blob
# microsoft-agents-hosting-core
- # openapi-core
+ # msrest
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via
# altair
+ # azure-ai-evaluation
# flask
+ # openai-chatkit
# pydeck
- # semantic-kernel
jiter==0.12.0
- # via openai
+ # via
+ # anthropic
+ # openai
+joblib==1.5.3
+ # via nltk
jsonpath-ng==1.7.0
# via redisvl
-jsonref==1.1.0
- # via autogen-core
jsonschema==4.25.1
# via
# altair
# flasgger
# mcp
- # openapi-core
- # openapi-schema-validator
- # openapi-spec-validator
-jsonschema-path==0.3.4
- # via
- # openapi-core
- # openapi-spec-validator
jsonschema-specifications==2025.9.1
- # via
- # jsonschema
- # openapi-schema-validator
-lazy-object-proxy==1.12.0
- # via openapi-spec-validator
+ # via jsonschema
markupsafe==3.0.3
# via
# jinja2
# werkzeug
-mcp==1.21.1
+mcp==1.26.0
# via
# agent-framework-core
- # autogen-ext
+ # openai-agents
mem0ai==1.0.1
# via agent-framework-mem0
microsoft-agents-activity==0.6.0.dev17
@@ -277,8 +328,6 @@ mistune==3.1.4
# via flasgger
ml-dtypes==0.5.3
# via redisvl
-more-itertools==10.8.0
- # via openapi-core
msal==1.31.0
# via
# applications (pyproject.toml)
@@ -286,14 +335,18 @@ msal==1.31.0
# msal-extensions
msal-extensions==1.3.1
# via azure-identity
+msrest==0.7.1
+ # via
+ # azure-ai-evaluation
+ # azure-monitor-opentelemetry-exporter
multidict==6.7.0
# via
# aiohttp
# yarl
narwhals==2.11.0
# via altair
-nest-asyncio==1.6.0
- # via semantic-kernel
+nltk==3.9.2
+ # via azure-ai-evaluation
numpy==2.3.5
# via
# agent-framework-redis
@@ -302,74 +355,135 @@ numpy==2.3.5
# pydeck
# qdrant-client
# redisvl
- # scipy
- # semantic-kernel
# streamlit
+oauthlib==3.3.1
+ # via requests-oauthlib
+ollama==0.6.1
+ # via agent-framework-ollama
openai==2.8.0
# via
# applications (pyproject.toml)
# agent-framework-core
+ # azure-ai-evaluation
# mem0ai
- # semantic-kernel
-openapi-core==0.19.4
- # via semantic-kernel
-openapi-schema-validator==0.6.3
- # via
- # openapi-core
- # openapi-spec-validator
-openapi-spec-validator==0.7.2
- # via openapi-core
-opentelemetry-api==1.38.0
+ # openai-agents
+ # openai-chatkit
+openai-agents==0.4.2
+ # via openai-chatkit
+openai-chatkit==1.6.0
+ # via agent-framework-chatkit
+opentelemetry-api==1.39.0
# via
# agent-framework-core
- # autogen-core
- # opentelemetry-exporter-otlp-proto-grpc
+ # azure-core-tracing-opentelemetry
+ # azure-functions-durable
+ # azure-monitor-opentelemetry-exporter
+ # opentelemetry-instrumentation
+ # opentelemetry-instrumentation-asgi
+ # opentelemetry-instrumentation-dbapi
+ # opentelemetry-instrumentation-django
+ # opentelemetry-instrumentation-fastapi
+ # opentelemetry-instrumentation-flask
+ # opentelemetry-instrumentation-psycopg2
+ # opentelemetry-instrumentation-requests
+ # opentelemetry-instrumentation-urllib
+ # opentelemetry-instrumentation-urllib3
+ # opentelemetry-instrumentation-wsgi
# opentelemetry-sdk
# opentelemetry-semantic-conventions
- # semantic-kernel
-opentelemetry-exporter-otlp-proto-common==1.38.0
- # via opentelemetry-exporter-otlp-proto-grpc
-opentelemetry-exporter-otlp-proto-grpc==1.38.0
- # via agent-framework-core
-opentelemetry-proto==1.38.0
- # via
- # opentelemetry-exporter-otlp-proto-common
- # opentelemetry-exporter-otlp-proto-grpc
-opentelemetry-sdk==1.38.0
+opentelemetry-instrumentation==0.60b0
+ # via
+ # opentelemetry-instrumentation-asgi
+ # opentelemetry-instrumentation-dbapi
+ # opentelemetry-instrumentation-django
+ # opentelemetry-instrumentation-fastapi
+ # opentelemetry-instrumentation-flask
+ # opentelemetry-instrumentation-psycopg2
+ # opentelemetry-instrumentation-requests
+ # opentelemetry-instrumentation-urllib
+ # opentelemetry-instrumentation-urllib3
+ # opentelemetry-instrumentation-wsgi
+opentelemetry-instrumentation-asgi==0.60b0
+ # via opentelemetry-instrumentation-fastapi
+opentelemetry-instrumentation-dbapi==0.60b0
+ # via opentelemetry-instrumentation-psycopg2
+opentelemetry-instrumentation-django==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-fastapi==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-flask==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-psycopg2==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-requests==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-urllib==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-urllib3==0.60b0
+ # via azure-monitor-opentelemetry
+opentelemetry-instrumentation-wsgi==0.60b0
+ # via
+ # opentelemetry-instrumentation-django
+ # opentelemetry-instrumentation-flask
+opentelemetry-resource-detector-azure==0.1.5
+ # via azure-monitor-opentelemetry
+opentelemetry-sdk==1.39.0
# via
# agent-framework-core
- # opentelemetry-exporter-otlp-proto-grpc
- # semantic-kernel
-opentelemetry-semantic-conventions==0.59b0
- # via opentelemetry-sdk
+ # azure-functions-durable
+ # azure-monitor-opentelemetry
+ # azure-monitor-opentelemetry-exporter
+ # opentelemetry-resource-detector-azure
+opentelemetry-semantic-conventions==0.60b0
+ # via
+ # opentelemetry-instrumentation
+ # opentelemetry-instrumentation-asgi
+ # opentelemetry-instrumentation-dbapi
+ # opentelemetry-instrumentation-django
+ # opentelemetry-instrumentation-fastapi
+ # opentelemetry-instrumentation-flask
+ # opentelemetry-instrumentation-requests
+ # opentelemetry-instrumentation-urllib
+ # opentelemetry-instrumentation-urllib3
+ # opentelemetry-instrumentation-wsgi
+ # opentelemetry-sdk
opentelemetry-semantic-conventions-ai==0.4.13
# via agent-framework-core
+opentelemetry-util-http==0.60b0
+ # via
+ # opentelemetry-instrumentation-asgi
+ # opentelemetry-instrumentation-django
+ # opentelemetry-instrumentation-fastapi
+ # opentelemetry-instrumentation-flask
+ # opentelemetry-instrumentation-requests
+ # opentelemetry-instrumentation-urllib
+ # opentelemetry-instrumentation-urllib3
+ # opentelemetry-instrumentation-wsgi
+orderedmultidict==1.0.2
+ # via furl
packaging==24.2
# via
# agent-framework-core
# altair
- # deprecation
+ # durabletask
# flasgger
- # prance
+ # opentelemetry-instrumentation
+ # opentelemetry-instrumentation-flask
# streamlit
pandas==2.3.3
- # via streamlit
-parse==1.20.2
- # via openapi-core
-pathable==0.4.4
- # via jsonschema-path
-pillow==11.3.0
# via
- # autogen-core
+ # azure-ai-evaluation
# streamlit
+pillow==11.3.0
+ # via streamlit
ply==3.11
# via jsonpath-ng
portalocker==3.2.0
# via qdrant-client
posthog==7.0.1
# via mem0ai
-prance==25.4.8.0
- # via semantic-kernel
+powerfx==0.0.34
+ # via agent-framework-declarative
propcache==0.4.1
# via
# aiohttp
@@ -379,15 +493,15 @@ proto-plus==1.26.1
protobuf==5.29.5
# via
# a2a-sdk
- # autogen-core
+ # durabletask
# google-api-core
# googleapis-common-protos
# mem0ai
- # opentelemetry-proto
# proto-plus
# qdrant-client
- # semantic-kernel
# streamlit
+psutil==7.2.2
+ # via azure-monitor-opentelemetry-exporter
pyarrow==22.0.0
# via streamlit
pyasn1==0.6.1
@@ -396,49 +510,46 @@ pyasn1==0.6.1
# rsa
pyasn1-modules==0.4.2
# via google-auth
-pybars4==0.9.13
- # via semantic-kernel
pycparser==2.23
# via cffi
pydantic==2.11.4
# via
# applications (pyproject.toml)
# a2a-sdk
+ # ag-ui-protocol
# agent-framework-core
- # autogen-core
+ # anthropic
# fastapi
+ # github-copilot-sdk
# mcp
# mem0ai
# microsoft-agents-activity
+ # ollama
# openai
+ # openai-agents
+ # openai-chatkit
# pydantic-settings
# qdrant-client
# redisvl
- # semantic-kernel
pydantic-core==2.33.2
# via pydantic
pydantic-settings==2.12.0
# via
# agent-framework-core
# mcp
- # semantic-kernel
pydeck==0.9.1
# via streamlit
-pyee==13.0.0
- # via aiortc
pyjwt==2.10.1
# via
+ # azure-ai-evaluation
# mcp
# microsoft-agents-hosting-core
# msal
-pylibsrtp==1.0.0
- # via aiortc
-pymeta3==0.5.1
- # via pybars4
-pyopenssl==25.3.0
- # via aiortc
python-dateutil==2.9.0.post0
# via
+ # agent-framework-durabletask
+ # azure-functions-durable
+ # github-copilot-sdk
# pandas
# posthog
python-dotenv==1.2.1
@@ -452,6 +563,8 @@ python-multipart==0.0.20
# via mcp
python-ulid==3.1.0
# via redisvl
+pythonnet==3.0.5
+ # via powerfx
pytz==2025.2
# via
# mem0ai
@@ -462,8 +575,8 @@ pywin32==311
# portalocker
pyyaml==6.0.3
# via
+ # agent-framework-declarative
# flasgger
- # jsonschema-path
# redisvl
# uvicorn
qdrant-client==1.15.1
@@ -477,20 +590,23 @@ redisvl==0.11.0
referencing==0.36.2
# via
# jsonschema
- # jsonschema-path
# jsonschema-specifications
+regex==2026.1.15
+ # via nltk
requests==2.32.4
# via
# applications (pyproject.toml)
# azure-core
+ # azure-functions-durable
# google-api-core
- # jsonschema-path
# msal
+ # msrest
+ # openai-agents
# posthog
- # prance
+ # requests-oauthlib
# streamlit
-rfc3339-validator==0.1.4
- # via openapi-schema-validator
+requests-oauthlib==2.0.0
+ # via msrest
rpds-py==0.29.0
# via
# jsonschema
@@ -498,23 +614,21 @@ rpds-py==0.29.0
rsa==4.9.1
# via google-auth
ruamel-yaml==0.18.16
- # via prance
+ # via azure-ai-evaluation
ruamel-yaml-clib==0.2.15
# via ruamel-yaml
-scipy==1.16.3
- # via semantic-kernel
-semantic-kernel==1.35.0
- # via applications (pyproject.toml)
six==1.17.0
# via
# flasgger
+ # furl
+ # orderedmultidict
# posthog
# python-dateutil
- # rfc3339-validator
smmap==5.0.2
# via gitdb
sniffio==1.3.1
# via
+ # anthropic
# anyio
# openai
sqlalchemy==2.0.44
@@ -537,35 +651,38 @@ toml==0.10.2
tornado==6.5.2
# via streamlit
tqdm==4.67.1
- # via openai
+ # via
+ # nltk
+ # openai
+types-requests==2.32.4.20260107
+ # via openai-agents
typing-extensions==4.15.0
# via
# agent-framework-core
# aiosignal
# altair
+ # anthropic
# anyio
- # autogen-core
# azure-ai-agents
# azure-ai-projects
# azure-core
# azure-cosmos
# azure-identity
+ # azure-search-documents
# azure-storage-blob
# fastapi
+ # github-copilot-sdk
# grpcio
# mcp
# openai
+ # openai-agents
# opentelemetry-api
- # opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-sdk
# opentelemetry-semantic-conventions
# posthog
# pydantic
# pydantic-core
- # pyee
- # pyopenssl
# referencing
- # semantic-kernel
# sqlalchemy
# streamlit
# typing-inspection
@@ -580,11 +697,14 @@ urllib3==2.5.0
# via
# qdrant-client
# requests
+ # types-requests
uvicorn==0.38.0
# via
# applications (pyproject.toml)
+ # agent-framework-ag-ui
# agent-framework-devui
# mcp
+ # openai-chatkit
watchdog==6.0.0
# via streamlit
watchfiles==1.1.1
@@ -593,12 +713,16 @@ websockets==15.0.1
# via
# applications (pyproject.toml)
# mcp
- # semantic-kernel
# uvicorn
werkzeug==3.1.3
# via
+ # azure-functions
# flask
- # openapi-core
+wrapt==1.17.3
+ # via
+ # opentelemetry-instrumentation
+ # opentelemetry-instrumentation-dbapi
+ # opentelemetry-instrumentation-urllib3
yarl==1.22.0
# via aiohttp
zipp==3.23.0
diff --git a/agentic_ai/applications/uv.lock b/agentic_ai/applications/uv.lock
index 5fff23e52..47af74d72 100644
--- a/agentic_ai/applications/uv.lock
+++ b/agentic_ai/applications/uv.lock
@@ -2,7 +2,8 @@ version = 1
revision = 1
requires-python = ">=3.12"
resolution-markers = [
- "python_full_version >= '3.13'",
+ "python_full_version >= '3.14'",
+ "python_full_version == '3.13.*'",
"python_full_version < '3.13'",
]
@@ -39,14 +40,14 @@ wheels = [
[[package]]
name = "agent-framework"
-version = "1.0.0b260107"
+version = "1.0.0b260130"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "agent-framework-core", extra = ["all"] },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189 }
+sdist = { url = "https://files.pythonhosted.org/packages/93/10/ba51bf04ea2900897a221664e4e673dcc7a7a58a6658eeb85115e920d9b4/agent_framework-1.0.0b260130.tar.gz", hash = "sha256:50e13b74366b8092cb81769f07b3b42d6ddc8888a51244933c3214df591b7108", size = 3506765 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554 },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/2a8efa9085c7fec503a64038f986faf0cdf7f5de853c4ae30724e2e2bda6/agent_framework-1.0.0b260130-py3-none-any.whl", hash = "sha256:b9ba1487f91ab22031e01b5c09e5649181fd717f807d94f22ec43a409c43cde1", size = 5552 },
]
[[package]]
@@ -160,7 +161,7 @@ wheels = [
[[package]]
name = "agent-framework-core"
-version = "1.0.0b260107"
+version = "1.0.0b260130"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-identity" },
@@ -174,9 +175,9 @@ dependencies = [
{ name = "pydantic-settings" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757 }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/39/e508e778219bd6d20e023a6f48235861a639e3cf888776f9e873bbad3c6b/agent_framework_core-1.0.0b260130.tar.gz", hash = "sha256:030a5b2ced796eec6839c2dabad90b4bd1ea33d1026f3ed1813050a56ccfa4ec", size = 301823 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298 },
+ { url = "https://files.pythonhosted.org/packages/36/68/afe66c72951a279e0fe048fd5af1e775528cde40dbdab8ec03b42c545df4/agent_framework_core-1.0.0b260130-py3-none-any.whl", hash = "sha256:75b4dd0ca2ae52574d406cf5c9ed7adf63e187379f72fce891743254d83dfd56", size = 348724 },
]
[package.optional-dependencies]
@@ -191,6 +192,8 @@ all = [
{ name = "agent-framework-copilotstudio" },
{ name = "agent-framework-declarative" },
{ name = "agent-framework-devui" },
+ { name = "agent-framework-durabletask" },
+ { name = "agent-framework-github-copilot" },
{ name = "agent-framework-lab" },
{ name = "agent-framework-mem0" },
{ name = "agent-framework-ollama" },
@@ -227,6 +230,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/21/359592eda88e88d8215a5941d120935588bbb7454336514c5353b4ae6240/agent_framework_devui-1.0.0b251114-py3-none-any.whl", hash = "sha256:75657a4b14de5271c587d5ef130d7c031b5936785c3283e16f66b871b1ffa278", size = 338108 },
]
+[[package]]
+name = "agent-framework-durabletask"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "durabletask" },
+ { name = "durabletask-azuremanaged" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/95/9d5ee7fd1fdcd52c10aa1b2902964701d1d62b9d35cc7d05115b90db6329/agent_framework_durabletask-1.0.0b260130.tar.gz", hash = "sha256:63a2c8e0968a51d8e132892e9d385d2b82ccb95263d2c0316dc46b0eaa4dd7a4", size = 30285 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/22/122ed515935926137cc3c6ca795ef01b30feb82160cfc0f29a34f9d603de/agent_framework_durabletask-1.0.0b260130-py3-none-any.whl", hash = "sha256:a46e292800d10a62ce0923efe753594ddbf0bd6d1bb6e1258380f0dbf7d0302f", size = 36357 },
+]
+
+[[package]]
+name = "agent-framework-github-copilot"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "github-copilot-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/00/f69d731db02e256b8d18d6d8cd20d3d0684245df876f22b836743403a9c1/agent_framework_github_copilot-1.0.0b260130.tar.gz", hash = "sha256:3f5f231785bc8e663da2d1db65a5e4ee49a0f6266e31cccbf3ef05a79ab6c90d", size = 7929 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/b8/0a09396682e915dc25dc39c69fc06cc199b9901ccb0fdbb5e9e2886d2cb0/agent_framework_github_copilot-1.0.0b260130-py3-none-any.whl", hash = "sha256:b8844bacbf666ff1ea7f27d34a42c11be4ade1c4d57e7545341bb74462d82703", size = 8752 },
+]
+
[[package]]
name = "agent-framework-lab"
version = "1.0.0b251024"
@@ -465,7 +496,10 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "agent-framework" },
+ { name = "azure-ai-evaluation" },
+ { name = "azure-ai-projects" },
{ name = "azure-cosmos" },
+ { name = "azure-monitor-opentelemetry" },
{ name = "fastapi" },
{ name = "flasgger" },
{ name = "flask" },
@@ -481,10 +515,22 @@ dependencies = [
{ name = "websockets" },
]
+[package.dev-dependencies]
+dev = [
+ { name = "azure-identity" },
+ { name = "azure-keyvault-secrets" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-timeout" },
+]
+
[package.metadata]
requires-dist = [
- { name = "agent-framework", specifier = "==1.0.0b260107" },
+ { name = "agent-framework", specifier = "==1.0.0b260130" },
+ { name = "azure-ai-evaluation", specifier = ">=1.14.0" },
+ { name = "azure-ai-projects", specifier = ">=2.0.0b2" },
{ name = "azure-cosmos", specifier = "==4.9.0" },
+ { name = "azure-monitor-opentelemetry", specifier = ">=1.6.0" },
{ name = "fastapi", specifier = "==0.115.12" },
{ name = "flasgger", specifier = "==0.9.7.1" },
{ name = "flask", specifier = "==3.0.3" },
@@ -500,6 +546,33 @@ requires-dist = [
{ name = "websockets", specifier = ">=15.0.1" },
]
+[package.metadata.requires-dev]
+dev = [
+ { name = "azure-identity", specifier = ">=1.26.0b1" },
+ { name = "azure-keyvault-secrets", specifier = ">=4.10.0" },
+ { name = "pytest", specifier = ">=9.0.2" },
+ { name = "pytest-asyncio", specifier = ">=1.3.0" },
+ { name = "pytest-timeout", specifier = ">=2.4.0" },
+]
+
+[[package]]
+name = "asgiref"
+version = "3.11.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345 },
+]
+
+[[package]]
+name = "asyncio"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555 },
+]
+
[[package]]
name = "attrs"
version = "25.4.0"
@@ -523,6 +596,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948 },
]
+[[package]]
+name = "azure-ai-evaluation"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "azure-core" },
+ { name = "azure-identity" },
+ { name = "azure-storage-blob" },
+ { name = "httpx" },
+ { name = "jinja2" },
+ { name = "msrest" },
+ { name = "nltk" },
+ { name = "openai" },
+ { name = "pandas" },
+ { name = "pyjwt" },
+ { name = "ruamel-yaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/ab/62300008df848b210ef2a21b646480eee7c1bf3906afdc1351795343321c/azure_ai_evaluation-1.14.0.tar.gz", hash = "sha256:2a5681805b7cde65ad663f34d0f647d28498dd9395f7e2ce0789320c26664dae", size = 2196726 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/68/1e0bd2123a5e681dbe474a3dda098c85704556e53ae24c7f4b3915d4e048/azure_ai_evaluation-1.14.0-py3-none-any.whl", hash = "sha256:1785f9be28517839ab9d30a03893951f7c9b530500d939d0ae51dde3aa1478b0", size = 1141136 },
+]
+
[[package]]
name = "azure-ai-projects"
version = "2.0.0b2"
@@ -560,6 +656,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/3c/b90d5afc2e47c4a45f4bba00f9c3193b0417fad5ad3bb07869f9d12832aa/azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b", size = 213302 },
]
+[[package]]
+name = "azure-core-tracing-opentelemetry"
+version = "1.0.0b12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-core" },
+ { name = "opentelemetry-api" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/7f/5de13a331a5f2919417819cc37dcf7c897018f02f83aa82b733e6629a6a6/azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144", size = 26010 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/5e/97a471f66935e7f89f521d0e11ae49c7f0871ca38f5c319dccae2155c8d8/azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac", size = 11962 },
+]
+
[[package]]
name = "azure-cosmos"
version = "4.9.0"
@@ -619,6 +728,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227 },
]
+[[package]]
+name = "azure-keyvault-secrets"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-core" },
+ { name = "isodate" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/e5/3074e581b6e8923c4a1f2e42192ea6f390bb52de3600c68baaaed529ef05/azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36", size = 129695 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/94/7c902e966b28e7cb5080a8e0dd6bffc22ba44bc907f09c4c633d2b7c4f6a/azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9", size = 125237 },
+]
+
+[[package]]
+name = "azure-monitor-opentelemetry"
+version = "1.8.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-core" },
+ { name = "azure-core-tracing-opentelemetry" },
+ { name = "azure-monitor-opentelemetry-exporter" },
+ { name = "opentelemetry-instrumentation-django" },
+ { name = "opentelemetry-instrumentation-fastapi" },
+ { name = "opentelemetry-instrumentation-flask" },
+ { name = "opentelemetry-instrumentation-psycopg2" },
+ { name = "opentelemetry-instrumentation-requests" },
+ { name = "opentelemetry-instrumentation-urllib" },
+ { name = "opentelemetry-instrumentation-urllib3" },
+ { name = "opentelemetry-resource-detector-azure" },
+ { name = "opentelemetry-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8a/2c/572dc7442f69bf3e97d93bb9073bb270437332f3ee8eaeeffbaf734f69f3/azure_monitor_opentelemetry-1.8.6.tar.gz", hash = "sha256:8301c377f2c0550dc9b87b273b746d5841ef8e5117b6c542c9a6f7f3c7f34c20", size = 60541 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/53/a3eeafd014947d2cb6142d0e405fb1e524248d0df49ec8fe2384d359afe7/azure_monitor_opentelemetry-1.8.6-py3-none-any.whl", hash = "sha256:2323eeba15bd2f016806e15f5bd6d24ddd62cc06beae0a06ab3fcfab38d3d120", size = 29350 },
+]
+
+[[package]]
+name = "azure-monitor-opentelemetry-exporter"
+version = "1.0.0b47"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-core" },
+ { name = "azure-identity" },
+ { name = "msrest" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-sdk" },
+ { name = "psutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/d0e4d8e0f61cb82fd3e94e52291036a7415321f9f7d5386ddb1277d31faa/azure_monitor_opentelemetry_exporter-1.0.0b47.tar.gz", hash = "sha256:c1207bd1c356aa77255e256f1af8eb2ac40a3bf51f90735f456056def7ac38c0", size = 279165 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/b1/67361bdb9047591f84b2bbd1e03c3161cf85f718a7532b78b4e48f6eaa38/azure_monitor_opentelemetry_exporter-1.0.0b47-py2.py3-none-any.whl", hash = "sha256:be1eca7ddfc07436793981313a68662e14713902f7e7fa7cf81736f1cf6d8bf8", size = 201193 },
+]
+
[[package]]
name = "azure-search-documents"
version = "11.7.0b2"
@@ -816,7 +979,7 @@ name = "clr-loader"
version = "0.2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi" },
+ { name = "cffi", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 }
wheels = [
@@ -885,6 +1048,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 },
]
+[[package]]
+name = "durabletask"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asyncio" },
+ { name = "grpcio" },
+ { name = "packaging" },
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/27/3d021e6b36fc1aab6099fafc56dfc8059b4e8968615a26c1a0418601e50a/durabletask-1.3.0.tar.gz", hash = "sha256:11e38dda6df4737fadca0c71fc0a0f769955877c8a8bdb25ccbf90cf45afbf63", size = 57830 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/87/31ea460dbfaf50d9877f143e2ce9829cac2fb106747d9900cc353356ea77/durabletask-1.3.0-py3-none-any.whl", hash = "sha256:411f23e13391b8845edca010873dd7a87ee7cfc1fe05753ab28a7cd7c3c1bd77", size = 64112 },
+]
+
+[[package]]
+name = "durabletask-azuremanaged"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-identity" },
+ { name = "durabletask" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/29/6bb0b5fe51aa92e117adcdc93efe97cf5476d86c1496e5c5ab35d99a8d07/durabletask_azuremanaged-1.3.0.tar.gz", hash = "sha256:55172588e075afa80d46dcc2e5ddbd84be0a20cc78c74f687040c3720677d34c", size = 4343 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/11/4d34fec302c4813e626080f1532d189767eb31d6d80e8f3698c230512f14/durabletask_azuremanaged-1.3.0-py3-none-any.whl", hash = "sha256:9da914f569da1597c858d494a95eda37e4372726c0ee65f30080dcafab262d60", size = 6366 },
+]
+
[[package]]
name = "fastapi"
version = "0.115.12"
@@ -1043,6 +1234,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
]
+[[package]]
+name = "github-copilot-sdk"
+version = "0.1.20"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/7d/afde0ec85815a558612130dc5ff79536299f411e672410c3edc0c1edeb2a/github_copilot_sdk-0.1.20.tar.gz", hash = "sha256:9e89cd46577fd18dd808d7113b7e20e021c4f944121a0a4891945460fb26c53c", size = 92207 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/91/f8cfa809184988a273af58824b312d31a532ee3ee70875100b5061540178/github_copilot_sdk-0.1.20-py3-none-any.whl", hash = "sha256:e7fa1bb843e2494930126551b80f3a035f36c47a05f9173ad0cdfb4151ad9346", size = 40306 },
+]
+
[[package]]
name = "gitpython"
version = "3.1.45"
@@ -1321,6 +1526,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 },
]
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
+]
+
[[package]]
name = "isodate"
version = "0.7.2"
@@ -1419,6 +1633,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 },
]
+[[package]]
+name = "joblib"
+version = "1.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 },
+]
+
[[package]]
name = "jsonpath-ng"
version = "1.7.0"
@@ -1676,6 +1899,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 },
]
+[[package]]
+name = "msrest"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-core" },
+ { name = "certifi" },
+ { name = "isodate" },
+ { name = "requests" },
+ { name = "requests-oauthlib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 },
+]
+
[[package]]
name = "multidict"
version = "6.7.0"
@@ -1784,6 +2023,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/a1/4d21933898e23b011ae0528151b57a9230a62960d0919bf2ee48c7f5c20a/narwhals-2.11.0-py3-none-any.whl", hash = "sha256:a9795e1e44aa94e5ba6406ef1c5ee4c172414ced4f1aea4a79e5894f0c7378d4", size = 423069 },
]
+[[package]]
+name = "nltk"
+version = "3.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "joblib" },
+ { name = "regex" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404 },
+]
+
[[package]]
name = "numpy"
version = "2.3.5"
@@ -1847,6 +2101,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 },
]
+[[package]]
+name = "oauthlib"
+version = "3.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
+]
+
[[package]]
name = "ollama"
version = "0.6.1"
@@ -1915,42 +2178,224 @@ wheels = [
[[package]]
name = "opentelemetry-api"
-version = "1.39.1"
+version = "1.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9", size = 65763 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459", size = 66357 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "packaging" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74", size = 31707 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a", size = 33096 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-asgi"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asgiref" },
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91", size = 26083 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3", size = 16907 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-dbapi"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/7f/b4c1fbce01b29daad5ef1396427c9cd3c7a55ee68e75f8c11089c7e2533d/opentelemetry_instrumentation_dbapi-0.60b0.tar.gz", hash = "sha256:2b7eb38e46890cebe5bc1a1c03d2ab07fc159b0b7b91342941ee33dd73876d84", size = 16311 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/23/0a/65e100c6d803de59a9113a993dcd371a4027453ba15ce4dabdb0343ca154/opentelemetry_instrumentation_dbapi-0.60b0-py3-none-any.whl", hash = "sha256:429d8ca34a44a4296b9b09a1bd373fff350998d200525c6e79883c3328559b03", size = 13966 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-django"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-wsgi" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/d2/8ddd9a5c61cd5048d422be8d22fac40f603aa82f0babf9f7c40db871080c/opentelemetry_instrumentation_django-0.60b0.tar.gz", hash = "sha256:461e6fca27936ba97eec26da38bb5f19310783370478c7ca3a3e40faaceac9cc", size = 26596 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/d6/28684547bf6c699582e998a172ba8bb08405cf6706729b0d6a16042e998f/opentelemetry_instrumentation_django-0.60b0-py3-none-any.whl", hash = "sha256:95495649c8c34ce9217c6873cdd10fc4fcaa67c25f8329adc54f5b286999e40b", size = 21169 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-fastapi"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-asgi" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3", size = 24960 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee", size = 13478 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-flask"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-wsgi" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/cc/e0758c23d66fd49956169cb24b5b06130373da2ce8d49945abce82003518/opentelemetry_instrumentation_flask-0.60b0.tar.gz", hash = "sha256:560f08598ef40cdcf7ca05bfb2e3ea74fab076e676f4c18bb36bb379bf5c4a1b", size = 20336 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/b5/387ce11f59e5ce65b890adc3f9c457877143b8a6d107a3a0b305397933a1/opentelemetry_instrumentation_flask-0.60b0-py3-none-any.whl", hash = "sha256:106e5774f79ac9b86dd0d949c1b8f46c807a8af16184301e10d24fc94e680d04", size = 15189 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-psycopg2"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-instrumentation-dbapi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f8/68/5ae8a3b9a28c2fdf8d3d050e451ddb2612ca963679b08a2959f01f6dda4b/opentelemetry_instrumentation_psycopg2-0.60b0.tar.gz", hash = "sha256:59e527fd97739440380634ffcf9431aa7f2965d939d8d5829790886e2b54ede9", size = 11266 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d4/24/66b5a41a2b0d1d07cc9b0fbd80f8b5c66b46a4d4731743505891da8b3cbe/opentelemetry_instrumentation_psycopg2-0.60b0-py3-none-any.whl", hash = "sha256:ea136a32babd559aa717c04dddf6aa78aa94b816fb4e10dfe06751727ef306d4", size = 11284 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-requests"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/0f/94c6181e95c867f559715887c418170a9eadd92ea6090122d464e375ff56/opentelemetry_instrumentation_requests-0.60b0.tar.gz", hash = "sha256:5079ed8df96d01dab915a0766cd28a49be7c33439ce43d6d39843ed6dee3204f", size = 16173 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/e1/2f13b41c5679243ba8eae651170c4ce2f532349877819566ae4a89a2b47f/opentelemetry_instrumentation_requests-0.60b0-py3-none-any.whl", hash = "sha256:e9957f3a650ae55502fa227b29ff985b37d63e41c85e6e1555d48039f092ea83", size = 13122 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-urllib"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/95/db/be895de04bd56d7a2b2ef6d267a4c52f6cd325b6647d1c15ae888b1b0f6a/opentelemetry_instrumentation_urllib-0.60b0.tar.gz", hash = "sha256:89b8796f9ab64d0ea0833cfea98745963baa0d7e4a775b3d2a77791aa97cf3f9", size = 13931 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2b/e0/178914d5cec77baef797c6d47412da478ff871b05eb8732d64037b87c868/opentelemetry_instrumentation_urllib-0.60b0-py3-none-any.whl", hash = "sha256:80e3545d02505dc0ea61b3a0a141ec2828e11bee6b7dedfd3ee7ed9a7adbf862", size = 12673 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-urllib3"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/a8/16a32239e84741fae1a2932badeade5e72b73bfc331b53f7049a648ca00b/opentelemetry_instrumentation_urllib3-0.60b0.tar.gz", hash = "sha256:6ae1640a993901bae8eda5496d8b1440fb326a29e4ba1db342738b8868174aad", size = 15789 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 },
+ { url = "https://files.pythonhosted.org/packages/16/b2/ca27479eaf1f3f4825481769eb0cb200cad839040b8d5f42662d0398a256/opentelemetry_instrumentation_urllib3-0.60b0-py3-none-any.whl", hash = "sha256:9a07504560feae650a9205b3e2a579a835819bb1d55498d26a5db477fe04bba0", size = 13187 },
+]
+
+[[package]]
+name = "opentelemetry-instrumentation-wsgi"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-api" },
+ { name = "opentelemetry-instrumentation" },
+ { name = "opentelemetry-semantic-conventions" },
+ { name = "opentelemetry-util-http" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/10/ad/ae04e35f3b96d9c20d5d3df94a4c296eabf7a54d35d6c831179471128270/opentelemetry_instrumentation_wsgi-0.60b0.tar.gz", hash = "sha256:5815195b1b9890f55c4baafec94ff98591579a7d9b16256064adea8ee5784651", size = 19104 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/0e/1ed4d3cdce7b2e00a24f79933b3472e642d4db98aaccc09769be5cbe5296/opentelemetry_instrumentation_wsgi-0.60b0-py3-none-any.whl", hash = "sha256:0ff80614c1e73f7e94a5860c7e6222a51195eebab3dc5f50d89013db3d5d2f13", size = 14553 },
+]
+
+[[package]]
+name = "opentelemetry-resource-detector-azure"
+version = "0.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "opentelemetry-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/e4/0d359d48d03d447225b30c3dd889d5d454e3b413763ff721f9b0e4ac2e59/opentelemetry_resource_detector_azure-0.1.5.tar.gz", hash = "sha256:e0ba658a87c69eebc806e75398cd0e9f68a8898ea62de99bc1b7083136403710", size = 11503 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/ae/c26d8da88ba2e438e9653a408b0c2ad6f17267801250a8f3cc6405a93a72/opentelemetry_resource_detector_azure-0.1.5-py3-none-any.whl", hash = "sha256:4dcc5d54ab5c3b11226af39509bc98979a8b9e0f8a24c1b888783755d3bf00eb", size = 14252 },
]
[[package]]
name = "opentelemetry-sdk"
-version = "1.39.1"
+version = "1.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 }
+sdist = { url = "https://files.pythonhosted.org/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde", size = 171322 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 },
+ { url = "https://files.pythonhosted.org/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514", size = 132413 },
]
[[package]]
name = "opentelemetry-semantic-conventions"
-version = "0.60b1"
+version = "0.60b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 }
+sdist = { url = "https://files.pythonhosted.org/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f", size = 137935 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 },
+ { url = "https://files.pythonhosted.org/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a", size = 219981 },
]
[[package]]
@@ -1962,6 +2407,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080 },
]
+[[package]]
+name = "opentelemetry-util-http"
+version = "0.60b0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d", size = 10576 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe", size = 8742 },
+]
+
[[package]]
name = "orderedmultidict"
version = "1.0.2"
@@ -2096,6 +2550,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 },
]
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
+]
+
[[package]]
name = "ply"
version = "3.11"
@@ -2139,8 +2602,8 @@ name = "powerfx"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cffi" },
- { name = "pythonnet" },
+ { name = "cffi", marker = "python_full_version < '3.14'" },
+ { name = "pythonnet", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555 }
wheels = [
@@ -2257,6 +2720,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 },
]
+[[package]]
+name = "psutil"
+version = "7.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595 },
+ { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082 },
+ { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476 },
+ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062 },
+ { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893 },
+ { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589 },
+ { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664 },
+ { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087 },
+ { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383 },
+ { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210 },
+ { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228 },
+ { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284 },
+ { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090 },
+ { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859 },
+ { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560 },
+ { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997 },
+ { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972 },
+ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266 },
+ { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737 },
+ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617 },
+]
+
[[package]]
name = "pyarrow"
version = "22.0.0"
@@ -2414,6 +2905,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 },
]
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
+]
+
[[package]]
name = "pyjwt"
version = "2.10.1"
@@ -2428,6 +2928,47 @@ crypto = [
{ name = "cryptography" },
]
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 },
+]
+
+[[package]]
+name = "pytest-timeout"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -2472,7 +3013,7 @@ name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "clr-loader" },
+ { name = "clr-loader", marker = "python_full_version < '3.14'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 }
wheels = [
@@ -2610,6 +3151,94 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 },
]
+[[package]]
+name = "regex"
+version = "2026.1.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398 },
+ { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339 },
+ { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003 },
+ { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656 },
+ { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252 },
+ { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268 },
+ { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589 },
+ { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700 },
+ { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928 },
+ { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607 },
+ { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729 },
+ { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697 },
+ { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849 },
+ { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279 },
+ { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166 },
+ { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415 },
+ { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164 },
+ { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218 },
+ { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895 },
+ { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680 },
+ { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210 },
+ { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358 },
+ { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583 },
+ { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782 },
+ { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978 },
+ { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550 },
+ { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747 },
+ { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615 },
+ { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951 },
+ { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275 },
+ { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145 },
+ { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411 },
+ { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068 },
+ { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756 },
+ { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114 },
+ { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524 },
+ { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455 },
+ { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007 },
+ { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794 },
+ { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159 },
+ { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558 },
+ { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427 },
+ { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939 },
+ { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753 },
+ { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559 },
+ { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879 },
+ { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317 },
+ { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551 },
+ { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170 },
+ { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146 },
+ { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986 },
+ { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098 },
+ { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980 },
+ { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607 },
+ { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358 },
+ { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833 },
+ { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045 },
+ { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374 },
+ { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940 },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112 },
+ { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586 },
+ { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691 },
+ { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422 },
+ { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467 },
+ { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073 },
+ { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757 },
+ { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122 },
+ { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761 },
+ { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538 },
+ { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066 },
+ { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938 },
+ { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314 },
+ { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652 },
+ { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550 },
+ { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981 },
+ { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780 },
+ { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778 },
+ { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667 },
+ { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386 },
+ { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837 },
+]
+
[[package]]
name = "requests"
version = "2.32.4"
@@ -2625,6 +3254,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
]
+[[package]]
+name = "requests-oauthlib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "oauthlib" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 },
+]
+
[[package]]
name = "rpds-py"
version = "0.29.0"
@@ -2718,6 +3360,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
]
+[[package]]
+name = "ruamel-yaml"
+version = "0.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102 },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -3114,6 +3765,55 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]
+[[package]]
+name = "wrapt"
+version = "1.17.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 },
+ { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 },
+ { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 },
+ { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 },
+ { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 },
+ { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 },
+ { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 },
+ { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 },
+ { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 },
+ { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 },
+ { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 },
+ { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 },
+ { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 },
+ { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 },
+ { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 },
+ { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 },
+ { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 },
+ { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 },
+ { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 },
+ { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 },
+ { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 },
+ { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 },
+ { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 },
+ { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 },
+ { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 },
+ { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 },
+ { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 },
+ { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 },
+ { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 },
+ { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 },
+ { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 },
+ { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 },
+ { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 },
+ { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 },
+ { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 },
+ { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 },
+ { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 },
+ { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 },
+ { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 },
+ { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 },
+ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 },
+]
+
[[package]]
name = "yarl"
version = "1.22.0"
diff --git a/agentic_ai/evaluations/.gitignore b/agentic_ai/evaluations/.gitignore
new file mode 100644
index 000000000..1d2876728
--- /dev/null
+++ b/agentic_ai/evaluations/.gitignore
@@ -0,0 +1,10 @@
+# Evaluation results directory
+eval_results/
+*.pyc
+__pycache__/
+.pytest_cache/
+*.json.bak
+agent_traces.json
+
+# Generated evaluation data (created by run_agent_eval.py)
+evaluation_input_data.jsonl
diff --git a/agentic_ai/evaluations/README.md b/agentic_ai/evaluations/README.md
new file mode 100644
index 000000000..209542b43
--- /dev/null
+++ b/agentic_ai/evaluations/README.md
@@ -0,0 +1,621 @@
+# AI Agent Evaluation Framework
+
+A comprehensive evaluation system for testing AI agents in customer support scenarios. This framework provides both **local evaluation** with custom metrics and **remote evaluation** via Azure AI Foundry with LLM-as-judge capabilities.
+
+---
+
+## Table of Contents
+
+1. [Evaluation Methodology](#evaluation-methodology)
+ - [Why Evaluate AI Agents?](#why-evaluate-ai-agents)
+ - [Single-Turn vs Multi-Turn Evaluation](#single-turn-vs-multi-turn-evaluation)
+ - [Built-in vs Custom Evaluators](#built-in-vs-custom-evaluators)
+2. [Metrics Deep Dive](#metrics-deep-dive)
+ - [Single-Turn Metrics (Tool-Focused)](#single-turn-metrics-tool-focused)
+ - [Multi-Turn Metrics (Outcome-Focused)](#multi-turn-metrics-outcome-focused)
+3. [Setup Guide](#setup-guide)
+ - [Prerequisites](#prerequisites)
+ - [Step 1: Environment Setup](#step-1-environment-setup)
+ - [Step 2: Azure Configuration](#step-2-azure-configuration)
+ - [Step 3: Start Services](#step-3-start-services)
+4. [Running Evaluations](#running-evaluations)
+ - [Local Evaluation](#local-evaluation)
+ - [Remote Evaluation (Azure AI Foundry)](#remote-evaluation-azure-ai-foundry)
+ - [Comparing Agents](#comparing-agents)
+5. [Interpreting Results](#interpreting-results)
+6. [Extending the Framework](#extending-the-framework)
+7. [Troubleshooting](#troubleshooting)
+
+---
+
+## Evaluation Methodology
+
+### Why Evaluate AI Agents?
+
+AI agents that use tools (APIs, databases, external services) require evaluation that goes beyond traditional NLP metrics. Unlike simple chatbots, agents must:
+
+1. **Choose the right tools** - Select appropriate APIs for each task
+2. **Use tools correctly** - Pass correct parameters and handle responses
+3. **Maintain context** - Remember information across conversation turns
+4. **Achieve outcomes** - Actually solve the customer's problem
+5. **Communicate effectively** - Provide clear, helpful responses
+
+This framework addresses all these dimensions through a combination of **rule-based metrics** (deterministic, fast) and **LLM-as-judge evaluators** (semantic understanding, nuanced assessment).
+
+### Single-Turn vs Multi-Turn Evaluation
+
+We use fundamentally different evaluation strategies for single-turn and multi-turn conversations because they measure different capabilities:
+
+#### Single-Turn Evaluation (Tool-Focused)
+
+**Rationale**: In a single exchange, the agent must immediately demonstrate correct tool selection and usage. There's no opportunity for course correction, so we heavily weight tool-level accuracy.
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β SINGLE-TURN WEIGHTS β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β Tool Behavior (recall, precision, efficiency) β 10% β
+β Tool Call Accuracy (LLM-judge) β 15% β
+β Task Adherence (LLM-judge) β 10% β
+β Completeness (success criteria met) β 10% β
+β Response Quality - LLM β 10% β
+β Response Quality - Basic β 5% β
+β Grounded Accuracy β 10% β
+β Intent Resolution β 10% β
+β Coherence β 5% β
+β Fluency β 5% β
+β Relevance β 5% β
+β Solution Accuracy β 10% β β Ground truth match
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+**Use cases**: Quick lookups, simple queries, one-shot requests
+
+#### Multi-Turn Evaluation (Outcome-Focused)
+
+**Rationale**: In multi-turn conversations, what matters is the **final outcome**, not the path taken. An agent might take different tool sequences across turns but still successfully resolve the customer's issue. Penalizing intermediate tool choices would be counterproductive.
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β MULTI-TURN WEIGHTS β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β Solution Accuracy β 30% β β Did we solve it?
+β Task Adherence β 20% β β Proper procedure?
+β Intent Resolution β 20% β β All intents handled?
+β Coherence β 10% β β Logical conversation?
+β Fluency β 10% β β Quality communication?
+β Relevance β 10% β β Stayed on topic?
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β Tool metrics EXCLUDED - we care about outcomes β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+**Use cases**: Complex problem resolution, account changes requiring multiple steps, escalation flows
+
+### Built-in vs Custom Evaluators
+
+We combine two types of evaluators to get the best of both worlds:
+
+#### Azure AI Foundry Built-in Evaluators
+
+**Why use them:**
+- β
Industry-standard LLM-as-judge implementations
+- β
Consistent with Azure AI Foundry portal metrics
+- β
Maintained and improved by Microsoft
+- β
1-5 scale matching portal visualization
+
+| Evaluator | What it Measures |
+|-----------|------------------|
+| `IntentResolutionEvaluator` | Did the agent understand the customer's intent? |
+| `TaskAdherenceEvaluator` | Did the agent follow proper procedures? |
+| `ToolCallAccuracyEvaluator` | Were tool calls correct and appropriate? |
+| `CoherenceEvaluator` | Was the response logically structured? |
+| `FluencyEvaluator` | Was the language natural and grammatically correct? |
+| `RelevanceEvaluator` | Was the response relevant to the question? |
+
+#### Custom Evaluators
+
+**Why we need them:**
+- π§ Domain-specific logic (e.g., billing-specific success criteria)
+- π― Ground truth matching against expected solutions
+- β‘ Deterministic rules (fast, no API calls needed)
+- π Tool behavior metrics unique to our agent patterns
+
+| Evaluator | What it Measures | Why Custom? |
+|-----------|------------------|-------------|
+| `ToolBehaviorEvaluator` | Recall, precision, efficiency of tool usage | Requires domain knowledge of expected tools |
+| `CompletenessEvaluator` | Success criteria satisfaction | Maps criteria to specific tool requirements |
+| `GroundedAccuracyEvaluator` | Response grounded in tool outputs | Needs access to tool call results |
+| `SolutionAccuracyEvaluator` | Match against ground truth | Uses scenario-specific rubrics |
+
+#### Score Scale: 1-5
+
+All metrics use a **1-5 scale** with a **threshold of 3** for pass/fail:
+
+```
+Score Meaning Pass?
+βββββ βββββββββββββββββ βββββ
+ 5 Excellent β
+ 4 Good β
+ 3 Acceptable β (threshold)
+ 2 Below expectations β
+ 1 Poor β
+```
+
+This matches the Azure AI Foundry portal visualization, making local and remote evaluation results directly comparable.
+
+---
+
+## Metrics Deep Dive
+
+### Single-Turn Metrics (Tool-Focused)
+
+#### 1. Tool Behavior (10%)
+Combines three sub-metrics:
+- **Recall** (50%): Fraction of required tools actually used
+- **Precision** (30%): Fraction of used tools that were relevant
+- **Efficiency** (20%): Required tools / total tools used
+
+```python
+# Example: Required [get_billing_summary], Used [get_billing_summary, get_customer_detail]
+recall = 1.0 # 1/1 required tools used
+precision = 0.5 # 1/2 used tools were in expected set
+efficiency = 0.5 # 1/2 ratio
+```
+
+#### 2. Tool Call Accuracy (15%) - LLM Judge
+Azure AI Foundry's `ToolCallAccuracyEvaluator` assesses:
+- Were the correct tools selected?
+- Were parameters passed correctly?
+- Was the sequence appropriate?
+
+#### 3. Task Adherence (10%) - LLM Judge
+Evaluates whether the agent followed proper procedures and policies.
+
+#### 4. Completeness (10%)
+Checks if scenario-specific success criteria were met:
+```python
+TOOL_CRITERIA_MAP = {
+ "must_access_billing": ["get_billing_summary", "get_subscription_detail"],
+ "must_check_security_logs": ["get_security_logs"],
+ "must_check_promotions": ["get_eligible_promotions"],
+}
+```
+
+#### 5. Response Quality (15% total)
+- **LLM-based** (10%): Semantic quality assessment
+- **Basic** (5%): Length, formatting, structure checks
+
+#### 6. Grounded Accuracy (10%)
+Verifies response is consistent with tool outputs (no hallucination).
+
+#### 7. Intent Resolution (10%) - LLM Judge
+Did the agent correctly understand what the customer wanted?
+
+#### 8-10. Coherence, Fluency, Relevance (5% each)
+Standard NLG quality metrics via Azure AI Foundry evaluators.
+
+#### 11. Solution Accuracy (10%)
+Compares agent response against expected ground truth solution.
+
+### Multi-Turn Metrics (Outcome-Focused)
+
+For multi-turn conversations, we **exclude tool-level metrics** and focus on outcomes:
+
+| Metric | Weight | Rationale |
+|--------|--------|-----------|
+| Solution Accuracy | 30% | The ultimate measure - did we solve the problem? |
+| Task Adherence | 20% | Did we follow proper procedures throughout? |
+| Intent Resolution | 20% | Were all customer intents (across turns) resolved? |
+| Coherence | 10% | Was the overall conversation logical and consistent? |
+| Fluency | 10% | Was communication quality maintained? |
+| Relevance | 10% | Did responses stay relevant across all turns? |
+
+**Why exclude tool metrics for multi-turn?**
+
+Consider a billing dispute that spans 3 turns:
+1. Customer asks about high bill β Agent retrieves billing summary
+2. Customer asks about specific charge β Agent gets usage data
+3. Customer requests payment plan β Agent records payment
+
+Evaluating tool accuracy at each turn is misleading because:
+- The "expected" tools depend on previous turn outcomes
+- Alternative valid tool sequences exist
+- What matters is: **Was the dispute resolved?**
+
+---
+
+## Setup Guide
+
+### Prerequisites
+
+- Python 3.10+ with `uv` package manager
+- Azure CLI authenticated (`az login`)
+- Existing `.env` file configured (see main repo [SETUP.md](../../SETUP.md))
+- Azure subscription with:
+ - Azure OpenAI resource (already configured in your `.env`)
+ - Azure AI Project (for remote evaluation)
+
+### Step 1: Environment Setup
+
+If you haven't already set up the repository:
+
+```bash
+# Clone repository
+git clone https://github.com/microsoft/OpenAIWorkshop.git
+cd OpenAIWorkshop
+
+# Install dependencies
+uv sync
+```
+
+### Step 2: Configure Evaluation Variables
+
+Add these variables to your existing `.env` file in `agentic_ai/applications/`:
+
+```bash
+# ============================================================
+# EVALUATION-SPECIFIC CONFIGURATION (add to existing .env)
+# ============================================================
+
+# Azure AI Foundry Project Endpoint (Required for --remote evaluation)
+# Get this from: https://ai.azure.com β Your Project β Settings β Project details
+# Look for "Project endpoint" in the format:
+# https://.api.azureml.ms/... (older projects)
+# https://.services.ai.azure.com/api/projects/ (newer projects)
+AZURE_AI_PROJECT_ENDPOINT=https://your-account.services.ai.azure.com/api/projects/your-project
+
+# Evaluation Model (Optional - defaults to AZURE_OPENAI_CHAT_DEPLOYMENT)
+# Use a separate deployment for evaluation to avoid rate limiting
+# Recommended: gpt-4o or gpt-4o-mini for reliable LLM-as-judge evaluation
+AZURE_OPENAI_EVAL_DEPLOYMENT=gpt-4o-mini
+```
+
+**Where to find the Project Endpoint:**
+1. Go to [Azure AI Foundry](https://ai.azure.com)
+2. Select your project
+3. Click **Settings** β **Project details**
+4. Copy the **Project endpoint** URL
+
+> **Note**: The evaluation uses your existing `AZURE_OPENAI_CHAT_DEPLOYMENT` if `AZURE_OPENAI_EVAL_DEPLOYMENT` is not set. Consider using a separate deployment for evaluation to avoid rate limiting during heavy testing.
+
+**Assign required Azure roles:**
+```bash
+# Azure AI Developer role (required for remote evaluation)
+az role assignment create \
+ --assignee $(az ad signed-in-user show --query id -o tsv) \
+ --role "Azure AI Developer" \
+ --scope /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{ai-project}
+```
+
+### Step 3: Start Services
+
+Start services in this order:
+
+```bash
+# Terminal 1: MCP Server (provides customer data APIs)
+cd mcp
+uv run python mcp_service.py
+# Wait for: "MCP server running on http://localhost:8000"
+
+# Terminal 2: Agent Backend
+cd agentic_ai/applications
+uv run python -m uvicorn backend:app --port 7000 --reload
+# Wait for: "Application startup complete"
+```
+
+Verify services:
+```bash
+curl http://localhost:8000/health # MCP server
+curl http://localhost:7000/health # Backend
+```
+
+---
+
+## Running Evaluations
+
+### Command-Line Options
+
+```bash
+cd agentic_ai/applications
+
+uv run python ../evaluations/run_agent_eval.py [OPTIONS]
+```
+
+| Flag | Description |
+|------|-------------|
+| `--agent NAME` | Agent name for tracking (default: from AGENT_MODULE) |
+| `--backend-url URL` | Backend URL (default: http://localhost:7000) |
+| `--local` | Run local evaluation only (default if neither specified) |
+| `--remote` | Push results to Azure AI Foundry |
+| `--single-turn-only` | Run only single-turn test cases |
+| `--multi-turn-only` | Run only multi-turn test cases |
+| `--limit N` | Limit to N test cases (useful for testing) |
+
+### Local Evaluation
+
+Local evaluation runs custom metrics without Azure AI Foundry:
+
+```bash
+# Basic local evaluation (all test cases)
+uv run python ../evaluations/run_agent_eval.py --agent my_agent
+
+# Single-turn only
+uv run python ../evaluations/run_agent_eval.py --agent my_agent --single-turn-only
+
+# Multi-turn only
+uv run python ../evaluations/run_agent_eval.py --agent my_agent --multi-turn-only
+
+# Quick test with 2 cases
+uv run python ../evaluations/run_agent_eval.py --agent my_agent --limit 2
+```
+
+**Output:**
+```
+================================================================================
+EVALUATION SUMMARY - http://localhost:7000
+================================================================================
+Agent: my_agent
+Total Tests: 30
+Passed: 26 β
+Failed: 4 β
+Pass Rate: 86.7%
+Average Score: 4.12
+
+Metric Breakdown (1-5 scale, threshold: 3):
+ tool_behavior : 4.2/5 ββββββββββββββββ β
+ completeness : 4.5/5 ββββββββββββββββββ β
+ solution_accuracy : 3.8/5 βββββββββββββββ β
+ coherence : 4.6/5 ββββββββββββββββββ β
+ ...
+```
+
+### Remote Evaluation (Azure AI Foundry)
+
+Remote evaluation pushes results to Azure AI Foundry portal:
+
+```bash
+# Remote only (skip local evaluation)
+uv run python ../evaluations/run_agent_eval.py --agent my_agent --remote
+
+# Both local and remote
+uv run python ../evaluations/run_agent_eval.py --agent my_agent --local --remote
+```
+
+**What happens:**
+1. Runs test cases against agent backend
+2. Generates `evaluation_input_data.jsonl` in Foundry format
+3. Creates evaluation in Azure AI Foundry with built-in evaluators:
+ - `builtin.coherence`
+ - `builtin.fluency`
+ - `builtin.relevance`
+ - `builtin.groundedness`
+ - `builtin.task_adherence`
+ - `builtin.intent_resolution`
+ - Custom `label_model` for solution_accuracy
+
+**Portal naming convention:**
+- Evaluation: `my_agent - Single Turn | 2026-02-03 14:30`
+- Run: `my_agent | Single Turn | 2026-02-03 14:30`
+
+### Comparing Agents
+
+Compare different agent implementations:
+
+```bash
+# Compare single vs reflection agents
+uv run python ../evaluations/run_agent_eval.py --agent agent_single --remote
+# Restart backend with reflection agent
+uv run python ../evaluations/run_agent_eval.py --agent agent_reflection --remote
+```
+
+View comparison in Azure AI Foundry portal β Evaluations β Compare runs.
+
+---
+
+## Interpreting Results
+
+### Score Thresholds
+
+| Score Range | Meaning | Action |
+|-------------|---------|--------|
+| 4.5 - 5.0 | Excellent | Agent performing optimally |
+| 3.5 - 4.4 | Good | Minor improvements possible |
+| 3.0 - 3.4 | Acceptable | Investigate low-scoring metrics |
+| 2.0 - 2.9 | Below expectations | Requires attention |
+| 1.0 - 1.9 | Poor | Significant issues to fix |
+
+### Common Issues
+
+**Low Tool Behavior Score:**
+- Agent using unnecessary tools (low efficiency)
+- Missing required tools (low recall)
+- Fix: Review agent's tool selection logic
+
+**Low Solution Accuracy:**
+- Agent response doesn't match expected outcome
+- Fix: Check ground truth in dataset, verify agent logic
+
+**Low Coherence/Fluency:**
+- Response structure or language issues
+- Fix: Adjust system prompts for clearer formatting
+
+### Output Files
+
+| File | Description |
+|------|-------------|
+| `eval_results/evaluation_summary.json` | Aggregate scores and pass rates |
+| `eval_results/test_case_results.json` | Per-test-case detailed results |
+| `evaluation_input_data.jsonl` | Foundry-format data for remote evaluation |
+
+---
+
+## Extending the Framework
+
+### Adding Custom Metrics
+
+1. Create evaluator class in `metrics.py`:
+
+```python
+class MyCustomEvaluator:
+ def evaluate(self, response: str, expected: str) -> EvaluationResult:
+ # Your evaluation logic
+ score = ... # 1-5 scale
+ return EvaluationResult(
+ metric_name="my_metric",
+ metric_type=MetricType.ACCURACY,
+ score=score,
+ passed=score >= 3.0,
+ details={...},
+ explanation="..."
+ )
+```
+
+2. Add to `evaluator.py` weights:
+```python
+SINGLE_TURN_WEIGHTS = {
+ ...
+ "my_metric": 0.05, # 5% weight
+}
+```
+
+### Adding Test Cases
+
+Add to `eval_dataset.json`:
+
+```json
+{
+ "id": "billing_new_scenario",
+ "customer_query": "Your test query here",
+ "customer_id": 101,
+ "category": "billing",
+ "expected_tools": ["get_billing_summary"],
+ "required_tools": ["get_billing_summary"],
+ "success_criteria": {"must_access_billing": true},
+ "ground_truth_solution": "Expected agent response...",
+ "scoring_rubric": "5: Complete and accurate...",
+ "multi_turn": false
+}
+```
+
+---
+
+## Troubleshooting
+
+### "Missing AZURE_AI_PROJECT_ENDPOINT"
+```bash
+# Add the project endpoint to your .env file
+# Get it from: https://ai.azure.com β Your Project β Settings β Project details
+echo 'AZURE_AI_PROJECT_ENDPOINT=https://your-account.services.ai.azure.com/api/projects/your-project' >> agentic_ai/applications/.env
+```
+
+### "Failed to resolve hostname" / DNS Error
+```bash
+# Placeholder values in .env file - must use real URLs
+grep "AZURE_AI_PROJECT" agentic_ai/applications/.env
+# Should show your actual Azure endpoint, not placeholders like "your-account"
+```
+
+### "Authentication failed"
+```bash
+az login
+az account show
+# Verify Azure AI Developer role is assigned to your account
+```
+
+### "Cannot connect to backend"
+```bash
+# Check services are running
+curl http://localhost:8000/health # MCP
+curl http://localhost:7000/health # Backend
+```
+
+### "No evaluation results in Foundry"
+- Verify `--remote` flag was used
+- Check `AZURE_AI_PROJECT_ENDPOINT` is set correctly
+- Wait 1-2 minutes for portal to update
+
+### "Rate limiting" during evaluation
+```bash
+# Use a separate deployment for evaluation
+# Add to your .env:
+AZURE_OPENAI_EVAL_DEPLOYMENT=gpt-4o-mini-eval
+# This avoids sharing quota with your agent's chat deployment
+```
+
+### Low Scores on All Tests
+- Verify MCP server has test data loaded
+- Check agent can access tools (`DISABLE_AUTH=true` in dev)
+- Review agent logs for errors
+
+---
+
+## Environment Variables Reference
+
+| Variable | Required | Description |
+|----------|----------|-------------|
+| `AZURE_AI_PROJECT_ENDPOINT` | For `--remote` | Azure AI Foundry project endpoint URL |
+| `AZURE_OPENAI_EVAL_DEPLOYMENT` | No | Model deployment for LLM-as-judge (defaults to `AZURE_OPENAI_CHAT_DEPLOYMENT`) |
+| `AZURE_OPENAI_CHAT_DEPLOYMENT` | Yes | Default model deployment (used if eval deployment not set) |
+| `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI resource endpoint |
+| `AZURE_OPENAI_API_KEY` | Yes* | Azure OpenAI API key (*or use managed identity) |
+
+---
+
+## Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Evaluation Framework β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β run_agent_eval.py β
+β βββ Load eval_dataset.json (30 test cases) β
+β βββ Send queries to Agent Backend (HTTP) β
+β βββ Capture tool calls via WebSocket β
+β βββ Run evaluators β
+β β β
+β βββ LOCAL EVALUATION (evaluator.py + metrics.py) β
+β β βββ ToolBehaviorEvaluator (recall, precision, efficiency) β
+β β βββ CompletenessEvaluator (success criteria) β
+β β βββ ResponseQualityEvaluator (LLM + basic) β
+β β βββ GroundedAccuracyEvaluator β
+β β βββ AzureAIEvaluatorSuite (if SDK available) β
+β β βββ IntentResolutionEvaluator β
+β β βββ TaskAdherenceEvaluator β
+β β βββ ToolCallAccuracyEvaluator β
+β β βββ CoherenceEvaluator β
+β β βββ FluencyEvaluator β
+β β βββ RelevanceEvaluator β
+β β β
+β βββ REMOTE EVALUATION (Azure AI Foundry) β
+β βββ Upload evaluation_input_data.jsonl β
+β βββ Run builtin.* evaluators β
+β βββ Run label_model for solution_accuracy β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+---
+
+## File Reference
+
+| File | Purpose |
+|------|---------|
+| `run_agent_eval.py` | Main evaluation script - orchestrates tests, local eval, and remote push |
+| `evaluator.py` | Evaluation runner, weight definitions, result aggregation |
+| `metrics.py` | All metric implementations (custom + Azure AI wrappers) |
+| `eval_dataset.json` | 30 test cases with ground truth and rubrics |
+| `telemetry.py` | Azure Monitor tracing configuration |
+
+**Generated files** (in `.gitignore`):
+| File | Purpose |
+|------|---------|
+| `evaluation_input_data.jsonl` | Generated during `--remote` evaluation for Foundry upload |
+| `eval_results/` | Local evaluation results and reports |
+
+---
+
+## License
+
+This project is part of the Microsoft OpenAI Workshop. See [LICENSE](../../LICENSE) for details.
diff --git a/agentic_ai/evaluations/__init__.py b/agentic_ai/evaluations/__init__.py
new file mode 100644
index 000000000..6a3982e3e
--- /dev/null
+++ b/agentic_ai/evaluations/__init__.py
@@ -0,0 +1,29 @@
+"""
+AI Agent Evaluation Framework
+
+Evaluation toolkit for testing AI agents against business scenarios.
+"""
+
+from .evaluator import AgentEvaluationRunner, AgentTrace, TestCaseResult
+from .metrics import (
+ ToolBehaviorEvaluator,
+ CompletenessEvaluator,
+ ResponseQualityEvaluator,
+ GroundedAccuracyEvaluator,
+ EvaluationResult,
+ MetricType
+)
+
+__all__ = [
+ "AgentEvaluationRunner",
+ "AgentTrace",
+ "TestCaseResult",
+ "ToolBehaviorEvaluator",
+ "CompletenessEvaluator",
+ "ResponseQualityEvaluator",
+ "GroundedAccuracyEvaluator",
+ "EvaluationResult",
+ "MetricType"
+]
+
+__version__ = "1.0.0"
diff --git a/agentic_ai/evaluations/eval_dataset.json b/agentic_ai/evaluations/eval_dataset.json
new file mode 100644
index 000000000..9f6b2d1cd
--- /dev/null
+++ b/agentic_ai/evaluations/eval_dataset.json
@@ -0,0 +1,602 @@
+{
+ "version": "2.0",
+ "description": "Comprehensive evaluation dataset for Contoso customer support agents",
+ "metadata": {
+ "total_scenarios": 30,
+ "single_turn": 25,
+ "multi_turn": 5,
+ "categories": ["billing", "internet", "mobile", "account", "tv", "bundle", "support"]
+ },
+ "test_cases": [
+ {
+ "id": "billing_high_invoice",
+ "category": "billing",
+ "name": "Invoice Higher Than Usual",
+ "customer_query": "Hi, I'm customer 251. I noticed my last invoice was $150, which is much higher than usual. Can you help me understand why?",
+ "customer_id": 251,
+ "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_data_usage", "search_knowledge_base"],
+ "required_tools": ["get_billing_summary", "get_data_usage"],
+ "success_criteria": {
+ "must_access_billing": true,
+ "must_explain_charges": true
+ },
+ "ground_truth_solution": "The customer's invoice is $150 instead of the usual $60 because of data overage charges. Key facts: 1) The plan has a 10GB data cap, 2) Customer used 22GB (12GB over), 3) Overage charges of $7.50/GB apply, 4) Additional $90 explains the higher bill. Solutions: offer courtesy adjustment, recommend plan upgrade, set up usage alerts.",
+ "scoring_rubric": "5=Identifies overage (22GB vs 10GB), explains charges, offers adjustment AND upgrade; 4=Identifies overage, offers solution; 3=Identifies cause but missing details; 2=Vague explanation; 1=Incorrect or unhelpful",
+ "multi_turn": false
+ },
+ {
+ "id": "billing_payment_history",
+ "category": "billing",
+ "name": "Payment History Inquiry",
+ "customer_query": "Hi, I'm customer 5. Can you show me my recent payments? I want to make sure they all went through.",
+ "customer_id": 5,
+ "expected_tools": ["get_customer_detail", "get_billing_summary"],
+ "required_tools": ["get_billing_summary"],
+ "success_criteria": {
+ "must_access_billing": true
+ },
+ "ground_truth_solution": "Show payment history with dates, amounts, and methods. Confirm all payments were successful. Mention autopay option if not enabled. Offer to send payment receipts if needed.",
+ "scoring_rubric": "5=Shows history with dates/amounts/methods, confirms status; 4=Shows history and confirms; 3=Provides payment info but incomplete; 2=Vague response; 1=No payment info",
+ "multi_turn": false
+ },
+ {
+ "id": "billing_autopay_setup",
+ "category": "billing",
+ "name": "Autopay Setup Request",
+ "customer_query": "Hi, I'm customer 10. I keep forgetting to pay my bill on time. Can you help me set up autopay?",
+ "customer_id": 10,
+ "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "search_knowledge_base"],
+ "required_tools": ["get_billing_summary"],
+ "success_criteria": {
+ "must_access_billing": true,
+ "must_explain_autopay": true
+ },
+ "ground_truth_solution": "Check current autopay status. Explain $5 monthly discount for autopay. Guide through setup process. Confirm payment method on file.",
+ "scoring_rubric": "5=Checks status, mentions $5 discount, explains benefits, guides setup; 4=Explains benefits and setup; 3=Basic autopay info; 2=Generic response; 1=Doesn't help with autopay",
+ "multi_turn": false
+ },
+ {
+ "id": "billing_overdue_invoice",
+ "category": "billing",
+ "name": "Overdue Invoice Question",
+ "customer_query": "Hi, I'm customer 15. I received a notice about an overdue invoice. What happens if I don't pay soon?",
+ "customer_id": 15,
+ "expected_tools": ["get_customer_detail", "get_billing_summary", "search_knowledge_base"],
+ "required_tools": ["get_billing_summary"],
+ "success_criteria": {
+ "must_access_billing": true,
+ "must_explain_consequences": true
+ },
+ "ground_truth_solution": "Show overdue invoices with amounts and due dates. Explain late fee policy and potential service suspension after 30+ days. Offer payment plan if large amount. Process payment if customer wants.",
+ "scoring_rubric": "5=Shows overdue details, explains consequences, offers solutions; 4=Explains consequences and helps; 3=Addresses concern but missing specifics; 2=Generic response; 1=Doesn't address concern",
+ "multi_turn": false
+ },
+ {
+ "id": "billing_refund_request",
+ "category": "billing",
+ "name": "Refund Request for Service Issue",
+ "customer_query": "Hi, I'm customer 20. I was without internet for 3 days last week. Can I get a refund or credit for those days?",
+ "customer_id": 20,
+ "expected_tools": ["get_customer_detail", "get_support_tickets", "get_subscription_detail", "get_billing_summary"],
+ "required_tools": ["get_support_tickets", "get_billing_summary"],
+ "success_criteria": {
+ "must_verify_outage": true,
+ "must_offer_credit": true
+ },
+ "ground_truth_solution": "Verify outage via support tickets or service incidents. Calculate pro-rated credit for 3 days. Apply credit to next invoice. Apologize for inconvenience and confirm credit will appear on next bill.",
+ "scoring_rubric": "5=Verifies outage, calculates credit, applies and confirms; 4=Acknowledges and offers credit; 3=Offers help but missing verification; 2=Generic response; 1=Doesn't address refund",
+ "multi_turn": false
+ },
+ {
+ "id": "internet_slow",
+ "category": "internet",
+ "name": "Internet Slower Than Before",
+ "customer_query": "Hi, I'm customer 252. My internet has been really slow lately. I'm paying for 1Gbps but it feels much slower.",
+ "customer_id": 252,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_provide_troubleshooting": true
+ },
+ "ground_truth_solution": "Check subscription and service status. Look for existing service incidents. Acknowledge known issue if exists. Provide troubleshooting steps (restart router, check cables, test wired). Offer to escalate and mention potential service credit.",
+ "scoring_rubric": "5=Identifies incident, provides troubleshooting, offers escalation AND credit; 4=Identifies issue and troubleshoots; 3=Acknowledges and provides some steps; 2=Generic troubleshooting; 1=Unhelpful",
+ "multi_turn": false
+ },
+ {
+ "id": "internet_upgrade_inquiry",
+ "category": "internet",
+ "name": "Internet Speed Upgrade Options",
+ "customer_query": "Hi, I'm customer 25. I work from home and my current internet is too slow for video calls. What upgrade options do I have?",
+ "customer_id": 25,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"],
+ "required_tools": ["get_subscription_detail", "get_products"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_show_products": true
+ },
+ "ground_truth_solution": "Check current plan. Show upgrade options: Basic (100Mbps/$49.99), Pro (500Mbps/$79.99), Ultimate (1Gbps/$119.99). Recommend Pro for video calls. Explain benefits, show price difference, mention promotions. Upgrades take effect within 24 hours.",
+ "scoring_rubric": "5=Shows current plan, presents options with pricing, recommends, mentions promos; 4=Shows options with pricing and recommends; 3=Lists options but missing personalization; 2=Generic product info; 1=No helpful upgrade info",
+ "multi_turn": false
+ },
+ {
+ "id": "internet_router_reset",
+ "category": "internet",
+ "name": "Router Reset Help",
+ "customer_query": "Hi, I'm customer 30. My router isn't working and I think I need to reset it. How do I do that?",
+ "customer_id": 30,
+ "expected_tools": ["get_customer_detail", "search_knowledge_base"],
+ "required_tools": ["search_knowledge_base"],
+ "success_criteria": {
+ "must_search_knowledge": true,
+ "must_provide_steps": true
+ },
+ "ground_truth_solution": "Provide step-by-step: 1) Locate reset button on back, 2) Use paperclip to press and hold 10 seconds, 3) Wait for router to restart (lights blink), 4) Returns to factory settings, 5) Reconnect using default WiFi on label. Offer technician if uncomfortable.",
+ "scoring_rubric": "5=Complete steps, mentions factory settings warning, offers additional help; 4=Provides reset steps and guidance; 3=Gives instructions but incomplete; 2=Vague instructions; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "internet_outage_report",
+ "category": "internet",
+ "name": "Internet Outage Report",
+ "customer_query": "Hi, I'm customer 35. My internet is completely down! Nothing is working. Is there an outage in my area?",
+ "customer_id": 35,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_check_incidents": true
+ },
+ "ground_truth_solution": "Check subscription service status and existing incidents. If outage confirmed: apologize, provide ETA, offer notification when restored. If no known outage: create support ticket, provide troubleshooting, offer technician visit, mention service credit for extended outages.",
+ "scoring_rubric": "5=Checks outage status, creates ticket if needed, provides ETA, offers follow-up; 4=Checks status and takes action; 3=Acknowledges and offers help; 2=Generic response; 1=Doesn't address outage",
+ "multi_turn": false
+ },
+ {
+ "id": "internet_static_ip",
+ "category": "internet",
+ "name": "Static IP Request",
+ "customer_query": "Hi, I'm customer 40. I need a static IP address for my home server. Do you offer that?",
+ "customer_id": 40,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail", "get_products"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_explain_options": true
+ },
+ "ground_truth_solution": "Static IP included in: Pro ($79.99/month) - 1 static IP, Ultimate ($119.99/month) - 1 static IP, Business Enterprise ($299.99/month) - IP block. Basic plan does not include. Check current plan and recommend upgrade to Pro if on Basic.",
+ "scoring_rubric": "5=Checks plan, explains which include static IP, recommends option; 4=Explains availability and recommends upgrade; 3=Mentions static IP but missing plan details; 2=Generic response; 1=Doesn't address request",
+ "multi_turn": false
+ },
+ {
+ "id": "roaming_travel",
+ "category": "mobile",
+ "name": "Travelling Abroad - Needs Roaming",
+ "customer_query": "Hi, I'm customer 253. I'm traveling to Spain in 2 days and need to know about international roaming.",
+ "customer_id": 253,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_explain_roaming": true
+ },
+ "ground_truth_solution": "Roaming currently NOT enabled. Packages typically require 3 days to activate (cutting it close). Spain covered under European options. Urgently enable roaming, recommend appropriate package, warn about timeline, explain rates and usage alerts.",
+ "scoring_rubric": "5=Identifies roaming off, explains urgency, offers to enable AND recommends package; 4=Identifies status and urgency, offers to enable; 3=Identifies not enabled, offers to help; 2=Generic roaming info; 1=Doesn't address request",
+ "multi_turn": false
+ },
+ {
+ "id": "mobile_data_usage",
+ "category": "mobile",
+ "name": "Mobile Data Usage Check",
+ "customer_query": "Hi, I'm customer 45. How much data have I used this month? I don't want to go over my limit.",
+ "customer_id": 45,
+ "expected_tools": ["get_customer_detail", "get_data_usage", "get_subscription_detail"],
+ "required_tools": ["get_data_usage", "get_subscription_detail"],
+ "success_criteria": {
+ "must_check_usage": true,
+ "must_show_limit": true
+ },
+ "ground_truth_solution": "Show current data usage for billing cycle, data cap from plan, days remaining, percentage used. If close to limit: warn about overage, suggest data-saving tips, offer unlimited upgrade, explain usage alerts.",
+ "scoring_rubric": "5=Shows usage, cap, remaining, provides proactive advice; 4=Shows usage and limit clearly; 3=Provides data info; 2=Vague or incomplete; 1=Doesn't provide usage",
+ "multi_turn": false
+ },
+ {
+ "id": "mobile_upgrade_premium",
+ "category": "mobile",
+ "name": "Mobile Plan Upgrade",
+ "customer_query": "Hi, I'm customer 3. I keep running out of data. What mobile plans with more data do you have?",
+ "customer_id": 3,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail", "get_products"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_show_products": true
+ },
+ "ground_truth_solution": "Current plan: Essential (5GB/$29.99). Recommend Premium ($59.99/month): unlimited data, international roaming, 5G Priority, 50GB hotspot. Explain $30/month price difference, highlight unlimited benefit, offer to process upgrade.",
+ "scoring_rubric": "5=Shows current plan, recommends Premium with pricing, highlights benefits; 4=Provides options with comparison; 3=Mentions options but missing details; 2=Generic product info; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "mobile_hotspot_question",
+ "category": "mobile",
+ "name": "Mobile Hotspot Inquiry",
+ "customer_query": "Hi, I'm customer 8. Does my mobile plan include hotspot? I need to use it for my laptop.",
+ "customer_id": 8,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true
+ },
+ "ground_truth_solution": "Check current plan. Essential: Hotspot NOT included or limited. Premium: 50GB hotspot included. Explain availability based on their plan, provide usage info or offer Premium upgrade, give instructions on enabling if available.",
+ "scoring_rubric": "5=Checks plan, explains status, provides info or upgrade option; 4=Explains availability for their plan; 3=Addresses question generally; 2=Vague without checking plan; 1=Doesn't address hotspot",
+ "multi_turn": false
+ },
+ {
+ "id": "account_locked",
+ "category": "account",
+ "name": "Account Locked After Failed Logins",
+ "customer_query": "Hi, I'm customer 254. I can't log into my account - it says it's locked!",
+ "customer_id": 254,
+ "expected_tools": ["get_customer_detail", "get_security_logs", "unlock_account", "search_knowledge_base"],
+ "required_tools": ["get_security_logs", "unlock_account"],
+ "success_criteria": {
+ "must_check_security_logs": true,
+ "must_unlock_account": true
+ },
+ "ground_truth_solution": "Security logs show multiple failed login attempts triggering lockout. Security feature to protect account. Verify identity, unlock account using unlock_account tool, confirm accessible. Suggest password reset, recommend 2FA, advise password manager.",
+ "scoring_rubric": "5=Verifies identity, unlocks, confirms, provides security recommendations (password, 2FA); 4=Unlocks and provides one recommendation; 3=Unlocks and confirms; 2=Attempts but doesn't unlock; 1=Doesn't address lockout",
+ "multi_turn": false
+ },
+ {
+ "id": "account_security_check",
+ "category": "account",
+ "name": "Security Audit Request",
+ "customer_query": "Hi, I'm customer 12. I heard about data breaches in the news. Can you check if my account is secure?",
+ "customer_id": 12,
+ "expected_tools": ["get_customer_detail", "get_security_logs", "search_knowledge_base"],
+ "required_tools": ["get_security_logs"],
+ "success_criteria": {
+ "must_check_security_logs": true,
+ "must_provide_recommendations": true
+ },
+ "ground_truth_solution": "Review security logs for suspicious activity, failed logins from unknown locations, unauthorized access. Provide recommendations: enable 2FA, use strong unique password, update every 90 days, never share credentials, monitor account. Reassure and explain security measures.",
+ "scoring_rubric": "5=Reviews logs, reports findings, comprehensive recommendations; 4=Checks status and provides recommendations; 3=Reviews but limited recommendations; 2=Generic advice without checking; 1=Doesn't address concern",
+ "multi_turn": false
+ },
+ {
+ "id": "account_update_contact",
+ "category": "account",
+ "name": "Update Contact Information",
+ "customer_query": "Hi, I'm customer 18. I have a new email and phone number. Can you update my account information?",
+ "customer_id": 18,
+ "expected_tools": ["get_customer_detail"],
+ "required_tools": ["get_customer_detail"],
+ "success_criteria": {
+ "must_access_customer": true
+ },
+ "ground_truth_solution": "Retrieve current contact details. Verify identity. Collect new email and phone. Explain verification process for new contact info. Note: new email may require verification, update affects notifications/billing alerts, password reset links go to email on file.",
+ "scoring_rubric": "5=Shows current info, requests new details, explains verification, updates preferences; 4=Helps with update and explains process; 3=Acknowledges and provides guidance; 2=Generic without checking info; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "account_paperless_billing",
+ "category": "account",
+ "name": "Paperless Billing Setup",
+ "customer_query": "Hi, I'm customer 22. I want to go paperless and stop receiving paper bills. How do I do that?",
+ "customer_id": 22,
+ "expected_tools": ["get_customer_detail", "search_knowledge_base"],
+ "required_tools": ["get_customer_detail"],
+ "success_criteria": {
+ "must_access_customer": true
+ },
+ "ground_truth_solution": "Check current billing preferences. Verify email on file. Enable paperless billing. Confirm: bills sent to email, paper stops within 1-2 cycles, can view all bills online, email notifications for new bills.",
+ "scoring_rubric": "5=Checks settings, confirms email, enables paperless, explains benefits; 4=Enables and confirms; 3=Provides guidance; 2=Generic without checking; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "tv_channel_lineup",
+ "category": "tv",
+ "name": "TV Channel Lineup Question",
+ "customer_query": "Hi, I'm customer 28. What channels do I get with my TV streaming plan?",
+ "customer_id": 28,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true
+ },
+ "ground_truth_solution": "TV plans: Basic ($34.99/month): 50+ channels, 2 screens, 7-day replay. Premium ($64.99/month): 150+ channels, 4 screens, 30-day replay, sports, movies. Check current subscription, list features, mention upgrade if on Basic, explain streaming app access.",
+ "scoring_rubric": "5=Shows plan details, lists features, mentions upgrade if applicable; 4=Explains channels and features; 3=Provides plan info; 2=Generic TV info; 1=Doesn't address question",
+ "multi_turn": false
+ },
+ {
+ "id": "tv_add_sports",
+ "category": "tv",
+ "name": "Add Sports Package",
+ "customer_query": "Hi, I'm customer 32. I want to watch football games. Do you have a sports package I can add?",
+ "customer_id": 32,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail", "get_products"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_show_products": true
+ },
+ "ground_truth_solution": "Sports included in TV Streaming Premium ($64.99/month). Basic does not include sports. Check current subscription. If on Basic, offer upgrade to Premium which includes sports plus movie channels, 4 screens, 30-day replay. Calculate price difference.",
+ "scoring_rubric": "5=Checks plan, explains sports in Premium, shows pricing, offers upgrade; 4=Explains availability and upgrade; 3=Mentions sports info; 2=Generic without checking plan; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "bundle_inquiry",
+ "category": "bundle",
+ "name": "Bundle Package Inquiry",
+ "customer_query": "Hi, I'm customer 38. I have internet and mobile separately. Would I save money if I bundle them?",
+ "customer_id": 38,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"],
+ "required_tools": ["get_subscription_detail", "get_products"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_show_products": true
+ },
+ "ground_truth_solution": "Bundle - Family Complete: $199.99/month includes 500Mbps Internet, 150+ TV Channels, 2 Unlimited Mobile Lines, 20% discount vs individual. Check current subscriptions and total cost, calculate potential savings, explain bundle includes more, show value, offer to switch.",
+ "scoring_rubric": "5=Shows current cost, calculates savings, explains benefits, offers switch; 4=Explains options and potential savings; 3=Provides bundle info; 2=Generic bundle info; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "promotion_eligibility",
+ "category": "bundle",
+ "name": "Promotion Eligibility Check",
+ "customer_query": "Hi, I'm customer 42. Are there any promotions or discounts I'm eligible for?",
+ "customer_id": 42,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_eligible_promotions"],
+ "required_tools": ["get_customer_detail", "get_eligible_promotions"],
+ "success_criteria": {
+ "must_access_customer": true,
+ "must_check_promotions": true
+ },
+ "ground_truth_solution": "Available promotions: 1) New Customer 20% off (if new), 2) Bundle & Save $50/month (if 3+ services), 3) Loyalty Reward free speed upgrade (if Gold/Platinum), 4) Refer a Friend $100 credit. Check loyalty level and services, identify applicable promotions, explain how to take advantage.",
+ "scoring_rubric": "5=Checks eligibility, lists applicable promos, explains how to apply; 4=Identifies promotions customer qualifies for; 3=Mentions available promotions; 2=Generic list without checking; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "loyalty_benefits",
+ "category": "bundle",
+ "name": "Loyalty Program Benefits",
+ "customer_query": "Hi, I'm customer 48. I've been with you for years. What loyalty benefits do I get?",
+ "customer_id": 48,
+ "expected_tools": ["get_customer_detail", "get_products", "search_knowledge_base"],
+ "required_tools": ["get_customer_detail"],
+ "success_criteria": {
+ "must_access_customer": true
+ },
+ "ground_truth_solution": "Loyalty tiers: Bronze (basic support), Silver (priority support, occasional discounts), Gold (24/7 VIP support, free speed upgrades, special promotions), Platinum (all Gold plus dedicated account manager). Check current level, explain tier benefits, mention how to reach next tier, highlight current Gold/Platinum promotion.",
+ "scoring_rubric": "5=Shows level, explains tier benefits, mentions upgrade path and promos; 4=Explains benefits for their tier; 3=Provides loyalty info; 2=Generic without checking level; 1=Doesn't address question",
+ "multi_turn": false
+ },
+ {
+ "id": "support_ticket_status",
+ "category": "support",
+ "name": "Support Ticket Status Check",
+ "customer_query": "Hi, I'm customer 6. I opened a support ticket a few days ago. Can you check the status?",
+ "customer_id": 6,
+ "expected_tools": ["get_customer_detail", "get_support_tickets"],
+ "required_tools": ["get_support_tickets"],
+ "success_criteria": {
+ "must_check_tickets": true
+ },
+ "ground_truth_solution": "Look up open/pending tickets. Provide ticket number and status. Explain current stage of resolution. Provide expected timeline. If pending: explain what's being done, offer to escalate if delayed, provide contact for urgent issues.",
+ "scoring_rubric": "5=Finds ticket, shows status, explains next steps, offers escalation; 4=Provides status and explanation; 3=Finds and reports status; 2=Generic without checking; 1=Doesn't help",
+ "multi_turn": false
+ },
+ {
+ "id": "support_new_ticket",
+ "category": "support",
+ "name": "Create New Support Ticket",
+ "customer_query": "Hi, I'm customer 14. My cable box keeps rebooting randomly. I need someone to look at this.",
+ "customer_id": 14,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_offer_support": true
+ },
+ "ground_truth_solution": "Document cable box issue (random reboots). Check subscription for equipment details. Basic troubleshooting: unplug 30 seconds, check connections. If persists, create support ticket with equipment type, issue description, troubleshooting attempted, priority level. Offer technician visit.",
+ "scoring_rubric": "5=Documents issue, tries troubleshooting, creates ticket, offers technician; 4=Creates ticket and offers options; 3=Acknowledges and offers help; 2=Generic troubleshooting without ticket; 1=Doesn't address issue",
+ "multi_turn": false
+ },
+ {
+ "id": "multi_billing_dispute",
+ "category": "billing",
+ "name": "[Multi-Turn] Billing Dispute Resolution",
+ "multi_turn": true,
+ "turns": [
+ {
+ "turn_number": 1,
+ "customer_query": "Hi, I'm customer 7. There's a $50 charge on my bill I don't recognize. What is this for?",
+ "expected_tools": ["get_billing_summary"],
+ "expected_response_elements": ["charge", "invoice", "billing"]
+ },
+ {
+ "turn_number": 2,
+ "customer_query": "I didn't order any equipment or additional services. Can you remove this charge?",
+ "expected_tools": [],
+ "expected_response_elements": ["credit", "remove", "adjustment"]
+ },
+ {
+ "turn_number": 3,
+ "customer_query": "Thanks for the credit. While I have you, are there any promotions I qualify for?",
+ "expected_tools": ["get_customer_detail", "get_eligible_promotions"],
+ "expected_response_elements": ["promotion", "discount", "offer"]
+ }
+ ],
+ "customer_id": 7,
+ "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_eligible_promotions"],
+ "required_tools": ["get_billing_summary"],
+ "success_criteria": {
+ "must_access_billing": true,
+ "must_handle_credit": true,
+ "must_check_promotions": true
+ },
+ "ground_truth_solution": "Turn 1: Pull billing summary, identify $50 charge, explain what it's for. Turn 2: If erroneous, apply credit; if valid, explain but offer goodwill credit if appropriate, confirm adjustment on next bill. Turn 3: Review loyalty level and services, identify applicable promotions, recommend best options.",
+ "scoring_rubric": "5=Investigates thoroughly, handles credit appropriately, provides personalized promo info; 4=Addresses each turn adequately; 3=Responds but missing depth; 2=Misses context between turns; 1=Fails to address dispute or loses context"
+ },
+ {
+ "id": "multi_internet_troubleshoot",
+ "category": "internet",
+ "name": "[Multi-Turn] Internet Troubleshooting Flow",
+ "multi_turn": true,
+ "turns": [
+ {
+ "turn_number": 1,
+ "customer_query": "Hi, I'm customer 16. My internet keeps dropping every few minutes. It's really frustrating.",
+ "expected_tools": ["get_subscription_detail", "get_support_tickets"],
+ "expected_response_elements": ["internet", "issue", "connection"]
+ },
+ {
+ "turn_number": 2,
+ "customer_query": "I already tried restarting the router. It worked for a bit but started dropping again.",
+ "expected_tools": ["search_knowledge_base"],
+ "expected_response_elements": ["troubleshoot", "check", "cable"]
+ },
+ {
+ "turn_number": 3,
+ "customer_query": "I checked the cables and they look fine. I think there might be something wrong with the equipment.",
+ "expected_tools": [],
+ "expected_response_elements": ["technician", "appointment", "visit"]
+ },
+ {
+ "turn_number": 4,
+ "customer_query": "Yes, please schedule a technician. What times are available?",
+ "expected_tools": [],
+ "expected_response_elements": ["scheduled", "appointment", "confirm"]
+ }
+ ],
+ "customer_id": 16,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"],
+ "required_tools": ["get_subscription_detail"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_provide_troubleshooting": true,
+ "must_offer_technician": true
+ },
+ "ground_truth_solution": "Turn 1: Check subscription/incidents, acknowledge issue. Turn 2: Since router restart tried, suggest cable check, wired connection test, interference check. Turn 3: Acknowledge customer tried troubleshooting, agree equipment may need inspection, offer technician. Turn 4: Offer time slots, confirm details, provide arrival window.",
+ "scoring_rubric": "5=Progressive troubleshooting, builds on previous turns, smooth escalation; 4=Addresses each step, schedules technician; 3=Follows flow but may skip steps; 2=Repetitive or doesn't build on attempts; 1=Doesn't progress logically"
+ },
+ {
+ "id": "multi_service_cancellation",
+ "category": "account",
+ "name": "[Multi-Turn] Service Cancellation Retention",
+ "multi_turn": true,
+ "turns": [
+ {
+ "turn_number": 1,
+ "customer_query": "Hi, I'm customer 24. I want to cancel my internet service. It's too expensive.",
+ "expected_tools": ["get_subscription_detail", "get_billing_summary"],
+ "expected_response_elements": ["cancel", "service", "understand"]
+ },
+ {
+ "turn_number": 2,
+ "customer_query": "I've been paying $119 a month and I found a competitor offering $70 for similar speeds.",
+ "expected_tools": ["get_products"],
+ "expected_response_elements": ["offer", "discount", "match", "retention"]
+ },
+ {
+ "turn_number": 3,
+ "customer_query": "A 20% discount sounds good. What would my new monthly rate be?",
+ "expected_tools": [],
+ "expected_response_elements": ["$95", "monthly", "rate", "discount"]
+ }
+ ],
+ "customer_id": 24,
+ "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_billing_summary", "get_products"],
+ "required_tools": ["get_subscription_detail", "get_billing_summary"],
+ "success_criteria": {
+ "must_check_subscription": true,
+ "must_attempt_retention": true
+ },
+ "ground_truth_solution": "Turn 1: Pull subscription/billing, express understanding, ask about needs, don't immediately accept cancellation. Turn 2: Acknowledge competitor pricing, check retention offers, offer 20% loyalty discount or price match, highlight value-adds. Turn 3: Calculate new rate ($119 x 0.8 = $95.20), confirm discount applied, explain duration, thank for staying.",
+ "scoring_rubric": "5=Empathetic handling, competitive retention offer, calculates rate, secures retention; 4=Makes appropriate offer and calculates; 3=Attempts retention but may miss personalization; 2=Too quick to cancel or weak retention; 1=Processes cancellation without effort"
+ },
+ {
+ "id": "multi_new_customer_setup",
+ "category": "internet",
+ "name": "[Multi-Turn] New Service Setup Assistance",
+ "multi_turn": true,
+ "turns": [
+ {
+ "turn_number": 1,
+ "customer_query": "Hi, I'm customer 2. I just moved to a new apartment and need to set up internet. What are my options?",
+ "expected_tools": ["get_products"],
+ "expected_response_elements": ["internet", "plans", "options"]
+ },
+ {
+ "turn_number": 2,
+ "customer_query": "I work from home and need reliable internet for video calls. Which plan do you recommend?",
+ "expected_tools": ["get_subscription_detail"],
+ "expected_response_elements": ["Pro", "500Mbps", "recommend"]
+ },
+ {
+ "turn_number": 3,
+ "customer_query": "The Pro plan sounds good. Do you have any current promotions for new setups?",
+ "expected_tools": ["get_eligible_promotions"],
+ "expected_response_elements": ["promotion", "discount", "new customer"]
+ },
+ {
+ "turn_number": 4,
+ "customer_query": "Great! Please set me up with the Pro plan and the new customer discount.",
+ "expected_tools": [],
+ "expected_response_elements": ["confirm", "order", "setup", "welcome"]
+ }
+ ],
+ "customer_id": 2,
+ "expected_tools": ["get_customer_detail", "get_products", "get_subscription_detail", "get_eligible_promotions"],
+ "required_tools": ["get_products"],
+ "success_criteria": {
+ "must_show_products": true,
+ "must_recommend_plan": true,
+ "must_check_promotions": true
+ },
+ "ground_truth_solution": "Turn 1: List plans (Basic, Pro, Ultimate) with speeds and pricing. Turn 2: Recommend Pro (500Mbps) for WFH video calls, explain why suitable. Turn 3: New Customer 20% off first 3 months, WiFi 6 router included, installation options. Turn 4: Confirm Pro @ $79.99, apply 20% (first 3 months = $63.99), set installation, welcome.",
+ "scoring_rubric": "5=Natural sales flow, personalized recommendation, applies promo, completes smoothly; 4=Guides through selection and setup; 3=Completes but may lack personalization; 2=Disjointed or missing steps; 1=Doesn't complete setup"
+ },
+ {
+ "id": "multi_complex_account_issue",
+ "category": "account",
+ "name": "[Multi-Turn] Complex Account Resolution",
+ "multi_turn": true,
+ "turns": [
+ {
+ "turn_number": 1,
+ "customer_query": "Hi, I'm customer 11. I have several issues. First, I was charged for a service I cancelled last month.",
+ "expected_tools": ["get_billing_summary", "get_subscription_detail"],
+ "expected_response_elements": ["charge", "cancelled", "billing"]
+ },
+ {
+ "turn_number": 2,
+ "customer_query": "Also, my internet has been slow for the past week. Are there any known issues?",
+ "expected_tools": ["get_support_tickets"],
+ "expected_response_elements": ["slow", "internet", "incident", "issue"]
+ },
+ {
+ "turn_number": 3,
+ "customer_query": "One more thing - I want to downgrade my TV package. I don't watch that much anymore.",
+ "expected_tools": ["get_products"],
+ "expected_response_elements": ["downgrade", "TV", "package", "change"]
+ },
+ {
+ "turn_number": 4,
+ "customer_query": "Can you summarize all the changes you're making to my account?",
+ "expected_tools": [],
+ "expected_response_elements": ["summary", "credit", "downgrade", "changes"]
+ }
+ ],
+ "customer_id": 11,
+ "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_support_tickets", "get_products"],
+ "required_tools": ["get_billing_summary", "get_subscription_detail"],
+ "success_criteria": {
+ "must_access_billing": true,
+ "must_check_subscription": true,
+ "must_handle_multiple_issues": true
+ },
+ "ground_truth_solution": "Turn 1: Check billing for cancelled service charge, identify erroneous charge, apply credit/refund. Turn 2: Check service incidents and subscription status, provide status/ETA or troubleshooting. Turn 3: Show current TV package, explain downgrade options (Premium to Basic), calculate savings, process change. Turn 4: Recap all changes: credit applied, internet issue status, TV downgrade and savings.",
+ "scoring_rubric": "5=Handles all 3 issues effectively, provides clear summary, maintains context; 4=Addresses all with reasonable resolution; 3=Handles most but may miss one or lack summary; 2=Loses track or incomplete resolution; 1=Unable to handle multiple issues"
+ }
+ ]
+}
diff --git a/agentic_ai/evaluations/evaluator.py b/agentic_ai/evaluations/evaluator.py
new file mode 100644
index 000000000..c0028f1bf
--- /dev/null
+++ b/agentic_ai/evaluations/evaluator.py
@@ -0,0 +1,458 @@
+"""
+Evaluation runner for AI Agent testing.
+Tests agents against the evaluation dataset and generates reports.
+Supports multi-turn conversations and Azure AI Foundry evaluators.
+"""
+
+import json
+import os
+from typing import Dict, List, Any, Optional
+from datetime import datetime
+from dataclasses import dataclass, asdict, field
+import sys
+
+from metrics import (
+ ToolBehaviorEvaluator,
+ CompletenessEvaluator,
+ ResponseQualityEvaluator,
+ GroundedAccuracyEvaluator,
+ AzureAIEvaluatorSuite,
+ EvaluationResult,
+ AZURE_EVALUATORS_AVAILABLE,
+)
+
+
+@dataclass
+class AgentTrace:
+ """Captured trace of agent execution."""
+ query: str
+ response: str
+ tool_calls: List[Dict[str, Any]]
+ metadata: Dict[str, Any]
+
+
+@dataclass
+class ConversationTurn:
+ """A single turn in a multi-turn conversation."""
+ query: str
+ response: str
+ tool_calls: List[Dict[str, Any]] = field(default_factory=list)
+
+
+@dataclass
+class MultiTurnTrace:
+ """Captured trace of a multi-turn conversation."""
+ turns: List[ConversationTurn]
+ metadata: Dict[str, Any]
+
+ @property
+ def full_response(self) -> str:
+ """Concatenate all responses for evaluation."""
+ return "\n\n".join(t.response for t in self.turns)
+
+ @property
+ def all_tool_calls(self) -> List[Dict[str, Any]]:
+ """Aggregate all tool calls across turns."""
+ return [call for turn in self.turns for call in turn.tool_calls]
+
+ @property
+ def first_query(self) -> str:
+ """Get the first query for matching."""
+ return self.turns[0].query if self.turns else ""
+
+
+@dataclass
+class TestCaseResult:
+ """Result of evaluating a single test case."""
+ test_case_id: str
+ query: str
+ agent_response: str
+ metrics: List[EvaluationResult]
+ overall_score: float
+ passed: bool
+ timestamp: str
+ is_multi_turn: bool = False
+ turn_count: int = 1
+
+
+class AgentEvaluationRunner:
+ """Main evaluation runner for agent testing."""
+
+ # Weights for SINGLE-TURN evaluation (tool-focused)
+ SINGLE_TURN_WEIGHTS = {
+ "tool_behavior": 0.10,
+ "tool_call_accuracy": 0.15,
+ "task_adherence": 0.10,
+ "completeness": 0.10,
+ "response_quality_llm": 0.10,
+ "response_quality_basic": 0.05,
+ "grounded_accuracy": 0.10,
+ "intent_resolution": 0.10,
+ "coherence": 0.05,
+ "fluency": 0.05,
+ "relevance": 0.05,
+ "solution_accuracy": 0.10,
+ }
+
+ # Weights for MULTI-TURN evaluation (outcome-focused)
+ # De-emphasizes tool-level metrics, focuses on overall outcome
+ MULTI_TURN_WEIGHTS = {
+ "solution_accuracy": 0.30, # Did we achieve the right outcome?
+ "task_adherence": 0.20, # Did we follow proper procedures?
+ "intent_resolution": 0.20, # Were all intents resolved?
+ "coherence": 0.10, # Was conversation logical?
+ "fluency": 0.10, # Was communication quality good?
+ "relevance": 0.10, # Were responses relevant throughout?
+ # Tool metrics excluded from multi-turn
+ }
+
+ def __init__(
+ self,
+ dataset_path: str = "eval_dataset.json",
+ azure_openai_client=None,
+ use_azure_evaluators: bool = True,
+ ):
+ """
+ Initialize evaluation runner.
+
+ Args:
+ dataset_path: Path to evaluation dataset JSON
+ azure_openai_client: Optional Azure OpenAI client for LLM-as-judge
+ use_azure_evaluators: Whether to use Azure AI Foundry evaluators
+ """
+ self.dataset_path = dataset_path
+ self.test_cases = self._load_dataset()
+ self.llm_client = azure_openai_client
+
+ # Initialize evaluators
+ self.tool_evaluator = ToolBehaviorEvaluator()
+ self.completeness_evaluator = CompletenessEvaluator()
+ self.quality_evaluator = ResponseQualityEvaluator(azure_openai_client)
+ self.accuracy_evaluator = GroundedAccuracyEvaluator(azure_openai_client)
+
+ # Initialize Azure AI evaluators if available and enabled
+ self.azure_evaluators = None
+ if use_azure_evaluators and AZURE_EVALUATORS_AVAILABLE:
+ self.azure_evaluators = AzureAIEvaluatorSuite()
+ if not self.azure_evaluators.available:
+ self.azure_evaluators = None
+
+ def _load_dataset(self) -> List[Dict[str, Any]]:
+ """Load evaluation dataset from JSON."""
+ with open(self.dataset_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ return data.get("test_cases", [])
+
+ def evaluate_agent_response(
+ self,
+ test_case: Dict[str, Any],
+ agent_trace: AgentTrace,
+ ) -> TestCaseResult:
+ """
+ Evaluate a single agent response against test case.
+
+ Args:
+ test_case: Test case from dataset
+ agent_trace: Captured agent execution trace
+
+ Returns:
+ TestCaseResult with all evaluation metrics
+ """
+ metrics: List[EvaluationResult] = []
+ is_multi_turn = test_case.get("multi_turn", False)
+
+ # 1. Evaluate tool usage
+ tool_names = [call.get("name", "") for call in agent_trace.tool_calls]
+
+ # Use required_tools if specified, otherwise fall back to expected_tools
+ required_tools = test_case.get("required_tools")
+ if required_tools is None:
+ required_tools = test_case.get("expected_tools", [])
+
+ tool_result = self.tool_evaluator.evaluate(
+ expected_tools=test_case.get("expected_tools", []),
+ actual_tools=tool_names,
+ required_tools=required_tools
+ )
+ metrics.append(tool_result)
+
+ # 2. Evaluate completeness
+ completeness_result = self.completeness_evaluator.evaluate(
+ success_criteria=test_case.get("success_criteria", {}),
+ tool_calls=agent_trace.tool_calls
+ )
+ metrics.append(completeness_result)
+
+ # 3. Evaluate response quality
+ tool_summary = f"Tools used: {', '.join(tool_names)}" if tool_names else "No tools used"
+ quality_result = self.quality_evaluator.evaluate(
+ query=agent_trace.query,
+ response=agent_trace.response,
+ tool_summary=tool_summary
+ )
+ metrics.append(quality_result)
+
+ # 4. Evaluate accuracy (if ground truth available)
+ tool_outputs = "; ".join([str(call.get("result", "")) for call in agent_trace.tool_calls if call.get("result")])
+ accuracy_result = self.accuracy_evaluator.evaluate(
+ response=agent_trace.response,
+ tool_outputs=tool_outputs if tool_outputs else None
+ )
+ metrics.append(accuracy_result)
+
+ # 5. Azure AI Foundry evaluators (if available)
+ if self.azure_evaluators:
+ azure_results = self.azure_evaluators.evaluate_all(
+ query=agent_trace.query,
+ response=agent_trace.response,
+ ground_truth=test_case.get("ground_truth_solution"),
+ scoring_rubric=test_case.get("scoring_rubric"),
+ tool_calls=agent_trace.tool_calls if not is_multi_turn else None, # Skip tool eval for multi-turn
+ llm_client=self.llm_client,
+ )
+ metrics.extend(azure_results)
+
+ # Use different weights based on single-turn vs multi-turn
+ if is_multi_turn:
+ weights = self.MULTI_TURN_WEIGHTS
+ # For multi-turn, only require outcome metrics to pass
+ required_pass_metrics = [] # No strict requirements, use overall score
+ else:
+ weights = self.SINGLE_TURN_WEIGHTS
+ required_pass_metrics = ["tool_behavior", "completeness"]
+
+ total_score = 0.0
+ total_weight = 0.0
+
+ for metric in metrics:
+ weight = weights.get(metric.metric_name, 0.0) # 0 weight = excluded
+ if weight > 0:
+ total_score += metric.score * weight
+ total_weight += weight
+
+ # Overall score is weighted average (on 1-5 scale)
+ overall_score = total_score / total_weight if total_weight > 0 else 0.0
+ # Threshold: 3/5 to pass
+ if is_multi_turn:
+ passed = overall_score >= 3.0 # Outcome-based pass for multi-turn
+ else:
+ passed = overall_score >= 3.0 and all(m.passed for m in metrics if m.metric_name in required_pass_metrics)
+
+ return TestCaseResult(
+ test_case_id=test_case.get("id", "unknown"),
+ query=agent_trace.query,
+ agent_response=agent_trace.response,
+ metrics=metrics,
+ overall_score=overall_score,
+ passed=passed,
+ timestamp=datetime.now().isoformat(),
+ is_multi_turn=is_multi_turn,
+ turn_count=len(test_case.get("turns", [])) if is_multi_turn else 1,
+ )
+
+ def run_evaluation(
+ self,
+ agent_traces: List[AgentTrace],
+ output_dir: str = "eval_results"
+ ) -> Dict[str, Any]:
+ """
+ Run evaluation on all agent traces.
+
+ Args:
+ agent_traces: List of captured agent execution traces
+ output_dir: Directory to save evaluation results
+
+ Returns:
+ Summary of evaluation results
+ """
+ os.makedirs(output_dir, exist_ok=True)
+
+ results: List[TestCaseResult] = []
+
+ # Match traces to test cases
+ for test_case in self.test_cases:
+ # Find matching trace by test_id in metadata or by query
+ matching_trace = None
+ test_id = test_case.get("id", "")
+
+ # Get customer query - for multi-turn, use first turn's query
+ if test_case.get("multi_turn", False):
+ turns = test_case.get("turns", [])
+ customer_query = turns[0]["customer_query"] if turns else ""
+ else:
+ customer_query = test_case.get("customer_query", "")
+
+ for trace in agent_traces:
+ # Match by test_id in metadata first
+ if trace.metadata.get("test_id") == test_id:
+ matching_trace = trace
+ break
+ # Fallback to query matching
+ if customer_query and trace.query.lower().strip() == customer_query.lower().strip():
+ matching_trace = trace
+ break
+
+ if not matching_trace:
+ print(f"β Warning: No trace found for test case {test_case['id']}")
+ continue
+
+ # Evaluate
+ result = self.evaluate_agent_response(test_case, matching_trace)
+ results.append(result)
+
+ # Print progress
+ status = "β PASS" if result.passed else "β FAIL"
+ print(f"{status} {result.test_case_id}: {result.overall_score:.2f}")
+
+ # Generate summary
+ summary = self._generate_summary(results)
+
+ # Save results
+ self._save_results(results, summary, output_dir)
+
+ return summary
+
+ def _generate_summary(self, results: List[TestCaseResult]) -> Dict[str, Any]:
+ """Generate summary statistics."""
+ total = len(results)
+ passed = sum(1 for r in results if r.passed)
+
+ avg_score = sum(r.overall_score for r in results) / total if total > 0 else 0.0
+
+ # Metric breakdowns
+ metric_scores = {}
+ for result in results:
+ for metric in result.metrics:
+ if metric.metric_name not in metric_scores:
+ metric_scores[metric.metric_name] = []
+ metric_scores[metric.metric_name].append(metric.score)
+
+ metric_averages = {
+ name: sum(scores) / len(scores) if scores else 0.0
+ for name, scores in metric_scores.items()
+ }
+
+ return {
+ "timestamp": datetime.now().isoformat(),
+ "total_tests": total,
+ "passed": passed,
+ "failed": total - passed,
+ "pass_rate": passed / total if total > 0 else 0.0,
+ "average_score": avg_score,
+ "metric_averages": metric_averages
+ }
+
+ def _save_results(
+ self,
+ results: List[TestCaseResult],
+ summary: Dict[str, Any],
+ output_dir: str
+ ):
+ """Save evaluation results to files."""
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+
+ # Save detailed results
+ results_file = os.path.join(output_dir, f"eval_results_{timestamp}.json")
+ with open(results_file, 'w') as f:
+ json.dump({
+ "results": [self._result_to_dict(r) for r in results],
+ "summary": summary
+ }, f, indent=2)
+
+ # Save summary report
+ report_file = os.path.join(output_dir, f"eval_report_{timestamp}.txt")
+ with open(report_file, 'w', encoding='utf-8') as f:
+ f.write(self._generate_text_report(results, summary))
+
+ print(f"\nβ Results saved to: {results_file}")
+ print(f"β Report saved to: {report_file}")
+
+ def _result_to_dict(self, result: TestCaseResult) -> Dict[str, Any]:
+ """Convert TestCaseResult to dictionary."""
+ return {
+ "test_case_id": result.test_case_id,
+ "query": result.query,
+ "agent_response": result.agent_response,
+ "overall_score": result.overall_score,
+ "passed": result.passed,
+ "timestamp": result.timestamp,
+ "metrics": [
+ {
+ "name": m.metric_name,
+ "type": m.metric_type.value,
+ "score": m.score,
+ "passed": m.passed,
+ "explanation": m.explanation,
+ "details": m.details
+ }
+ for m in result.metrics
+ ]
+ }
+
+ def _generate_text_report(
+ self,
+ results: List[TestCaseResult],
+ summary: Dict[str, Any]
+ ) -> str:
+ """Generate human-readable text report."""
+ lines = []
+ lines.append("=" * 80)
+ lines.append("AI AGENT EVALUATION REPORT")
+ lines.append("=" * 80)
+ lines.append(f"\nTimestamp: {summary['timestamp']}")
+ lines.append(f"Total Tests: {summary['total_tests']}")
+ lines.append(f"Passed: {summary['passed']}")
+ lines.append(f"Failed: {summary['failed']}")
+ lines.append(f"Pass Rate: {summary['pass_rate']:.1%}")
+ lines.append(f"Average Score: {summary['average_score']:.2f}")
+
+ lines.append("\n" + "=" * 80)
+ lines.append("METRIC AVERAGES")
+ lines.append("=" * 80)
+ for metric, avg in summary['metric_averages'].items():
+ lines.append(f"{metric:30s}: {avg:.2f}")
+
+ lines.append("\n" + "=" * 80)
+ lines.append("DETAILED RESULTS")
+ lines.append("=" * 80)
+
+ for result in results:
+ status = "β PASS" if result.passed else "β FAIL"
+ lines.append(f"\n{status} {result.test_case_id} (Score: {result.overall_score:.2f})")
+ lines.append(f"Query: {result.query}")
+ lines.append("\nMetrics:")
+ for metric in result.metrics:
+ lines.append(f" - {metric.metric_name}: {metric.score:.2f} - {metric.explanation}")
+
+ return "\n".join(lines)
+
+
+def example_usage():
+ """Example of how to use the evaluation runner."""
+
+ # This is an example - in practice, you'd capture real agent traces
+ example_traces = [
+ AgentTrace(
+ query="I noticed my last invoice was higher than usualβcan you help me understand why and what can be done about it?",
+ response="I've checked your billing history and found that your invoice increased due to a plan upgrade last month. According to our billing policy, you can request a review within 30 days.",
+ tool_calls=[
+ {"name": "get_customer_detail", "args": {"customer_id": 1001}, "result": {}},
+ {"name": "get_billing_summary", "args": {"customer_id": 1001}, "result": {}},
+ {"name": "search_knowledge_base", "args": {"query": "billing policy"}, "result": {}}
+ ],
+ metadata={"agent_type": "single_agent", "duration_ms": 2500}
+ )
+ ]
+
+ # Run evaluation
+ runner = AgentEvaluationRunner(dataset_path="eval_dataset.json")
+ summary = runner.run_evaluation(example_traces, output_dir="eval_results")
+
+ print("\n" + "=" * 80)
+ print("EVALUATION SUMMARY")
+ print("=" * 80)
+ print(json.dumps(summary, indent=2))
+
+
+if __name__ == "__main__":
+ example_usage()
diff --git a/agentic_ai/evaluations/metrics.py b/agentic_ai/evaluations/metrics.py
new file mode 100644
index 000000000..2820ed96d
--- /dev/null
+++ b/agentic_ai/evaluations/metrics.py
@@ -0,0 +1,1106 @@
+"""
+Evaluation metrics for AI Agent performance assessment.
+Pattern-agnostic metrics that work across:
+- single agents
+- handoff agents
+- reflection agents
+- research/magentic agents
+
+Includes Azure AI Foundry evaluators for LLM-as-judge evaluation.
+"""
+
+import os
+from typing import Dict, List, Any, Optional
+from dataclasses import dataclass
+from enum import Enum
+
+# Azure AI Foundry Evaluators (optional - graceful degradation if not available)
+try:
+ from azure.ai.evaluation import (
+ IntentResolutionEvaluator,
+ TaskAdherenceEvaluator,
+ ToolCallAccuracyEvaluator,
+ CoherenceEvaluator,
+ FluencyEvaluator,
+ RelevanceEvaluator,
+ )
+ AZURE_EVALUATORS_AVAILABLE = True
+except ImportError:
+ AZURE_EVALUATORS_AVAILABLE = False
+ IntentResolutionEvaluator = None
+ TaskAdherenceEvaluator = None
+ ToolCallAccuracyEvaluator = None
+ CoherenceEvaluator = None
+ FluencyEvaluator = None
+ RelevanceEvaluator = None
+
+
+# =========================
+# Metric Types
+# =========================
+
+class MetricType(Enum):
+ TOOL_BEHAVIOR = "tool_behavior"
+ RESPONSE_QUALITY = "response_quality"
+ ACCURACY = "accuracy"
+ COMPLETENESS = "completeness"
+ EFFICIENCY = "efficiency"
+ SAFETY = "safety"
+ INTENT = "intent"
+ COHERENCE = "coherence"
+ FLUENCY = "fluency"
+ RELEVANCE = "relevance"
+ SOLUTION_ACCURACY = "solution_accuracy"
+ TASK_COMPLETION = "task_completion"
+
+
+# =========================
+# Result Container
+# =========================
+
+@dataclass
+class EvaluationResult:
+ metric_name: str
+ metric_type: MetricType
+ score: float # 1.0 β 5.0 scale (matching Foundry portal)
+ passed: bool
+ details: Dict[str, Any]
+ explanation: str
+
+
+# =========================
+# Tool Behavior Evaluator (Upgraded)
+# =========================
+
+class ToolBehaviorEvaluator:
+ """
+ Pattern-agnostic tool scoring:
+ - recall (required tools used)
+ - precision (relevant vs total)
+ - efficiency (minimal sufficiency)
+ """
+
+ def evaluate(
+ self,
+ expected_tools: List[str],
+ actual_tools: List[str],
+ required_tools: Optional[List[str]] = None,
+ ) -> EvaluationResult:
+
+ required_tools = required_tools or expected_tools
+
+ actual_set = set(actual_tools)
+ expected_set = set(expected_tools)
+ required_set = set(required_tools)
+
+ required_hit = required_set & actual_set
+ missing_required = required_set - actual_set
+ extra_tools = actual_set - expected_set
+ relevant_used = actual_set & expected_set
+
+ # --- Scores ---
+
+ recall = len(required_hit) / len(required_set) if required_set else 1.0
+ precision = len(relevant_used) / len(actual_set) if actual_set else 1.0
+ efficiency = len(required_set) / len(actual_set) if actual_set else 1.0
+ efficiency = min(efficiency, 1.0)
+
+ # Combined ratio (0-1), then scale to 1-5
+ combined = (recall * 0.5) + (precision * 0.3) + (efficiency * 0.2)
+ score = 1.0 + (combined * 4.0) # Maps 0-1 to 1-5 scale
+
+ passed = recall == 1.0 # All required tools must be used
+
+ details = {
+ "recall": recall,
+ "precision": precision,
+ "efficiency": efficiency,
+ "missing_required": list(missing_required),
+ "extra_tools": list(extra_tools),
+ "required_hit": list(required_hit),
+ }
+
+ explanation = (
+ f"Recall={recall:.2f} Precision={precision:.2f} "
+ f"Efficiency={efficiency:.2f} Score={score:.1f}/5"
+ )
+
+ return EvaluationResult(
+ metric_name="tool_behavior",
+ metric_type=MetricType.TOOL_BEHAVIOR,
+ score=score,
+ passed=passed,
+ details=details,
+ explanation=explanation,
+ )
+
+
+# =========================
+# Completeness Evaluator (Hybrid)
+# =========================
+
+class CompletenessEvaluator:
+ """
+ Deterministic tool checks + optional LLM semantic checks.
+ """
+
+ TOOL_CRITERIA_MAP = {
+ "must_access_billing": ["get_billing_summary", "get_subscription_detail"],
+ "must_check_subscription": ["get_subscription_detail"],
+ "must_check_security_logs": ["get_security_logs"],
+ "must_check_promotions": ["get_eligible_promotions"],
+ "must_check_orders": ["get_customer_orders"],
+ }
+
+ def evaluate(
+ self,
+ success_criteria: Dict[str, bool],
+ tool_calls: List[Dict[str, Any]],
+ ) -> EvaluationResult:
+
+ tool_names = [c.get("name", "") for c in tool_calls]
+ results = {}
+
+ for criterion, required in success_criteria.items():
+
+ if not required:
+ results[criterion] = True
+ continue
+
+ if criterion in self.TOOL_CRITERIA_MAP:
+ needed = self.TOOL_CRITERIA_MAP[criterion]
+ results[criterion] = any(t in tool_names for t in needed)
+ else:
+ # semantic criteria handled by LLM judge metric
+ results[criterion] = True
+
+ required_count = sum(success_criteria.values())
+ met_count = sum(
+ 1 for k, v in results.items()
+ if v and success_criteria.get(k)
+ )
+
+ # Scale to 1-5 (0 if no requirements, otherwise proportional)
+ ratio = met_count / required_count if required_count else 1.0
+ score = 1.0 + (ratio * 4.0) # Maps 0-1 ratio to 1-5 scale
+ passed = met_count == required_count # All requirements must be met
+
+ return EvaluationResult(
+ metric_name="completeness",
+ metric_type=MetricType.COMPLETENESS,
+ score=score,
+ passed=passed,
+ details=results,
+ explanation=f"{met_count}/{required_count} required criteria met",
+ )
+
+
+# =========================
+# Efficiency Evaluator (NEW)
+# =========================
+
+class EfficiencyEvaluator:
+ """
+ Pattern-agnostic step efficiency metric.
+ """
+
+ def evaluate(
+ self,
+ actual_tool_calls: int,
+ required_tools: int,
+ ) -> EvaluationResult:
+
+ baseline = max(required_tools, 1)
+ efficiency = baseline / max(actual_tool_calls, 1)
+ efficiency = min(efficiency, 1.0)
+
+ # Scale to 1-5
+ score = 1.0 + (efficiency * 4.0)
+
+ return EvaluationResult(
+ metric_name="step_efficiency",
+ metric_type=MetricType.EFFICIENCY,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details={
+ "actual_calls": actual_tool_calls,
+ "baseline_required": baseline,
+ },
+ explanation=f"Efficiency {score:.1f}/5",
+ )
+
+
+# =========================
+# LLM Judge Evaluator (Upgraded)
+# =========================
+
+class ResponseQualityEvaluator:
+
+ def __init__(self, llm_client=None):
+ self.client = llm_client
+
+ def evaluate(
+ self,
+ query: str,
+ response: str,
+ tool_summary: Optional[str] = None,
+ ) -> EvaluationResult:
+
+ if not self.client:
+ return self._basic(response)
+
+ prompt = f"""
+Evaluate this customer support response.
+
+Query: {query}
+Response: {response}
+Tool Evidence: {tool_summary}
+
+Score 0β10:
+- relevance
+- clarity
+- completeness
+- professionalism
+- actionability
+- groundedness (uses evidence, not guesses)
+- safety (no over-promising)
+
+Return JSON with overall_score and explanation.
+"""
+
+ try:
+ r = self.client.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[
+ {"role": "system", "content": "Expert evaluator."},
+ {"role": "user", "content": prompt},
+ ],
+ response_format={"type": "json_object"},
+ )
+
+ import json
+ data = json.loads(r.choices[0].message.content)
+
+ # Convert 0-10 to 1-5 scale
+ raw_score = data["overall_score"]
+ score = 1.0 + (raw_score / 10.0) * 4.0 # Maps 0-10 to 1-5
+
+ return EvaluationResult(
+ metric_name="response_quality",
+ metric_type=MetricType.RESPONSE_QUALITY,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=data,
+ explanation=data.get("explanation", ""),
+ )
+
+ except Exception:
+ return self._basic(response)
+
+ def _basic(self, response: str) -> EvaluationResult:
+ ok = len(response.split()) > 15
+ score = 5.0 if ok else 1.0 # 5/5 for good, 1/5 for bad
+ return EvaluationResult(
+ metric_name="response_quality_basic",
+ metric_type=MetricType.RESPONSE_QUALITY,
+ score=score,
+ passed=ok,
+ details={},
+ explanation="Basic length check",
+ )
+
+
+# =========================
+# Grounded Accuracy Evaluator (NEW)
+# =========================
+
+class GroundedAccuracyEvaluator:
+ """
+ Checks if response contradicts tool outputs (LLM-assisted).
+ """
+
+ def __init__(self, llm_client=None):
+ self.client = llm_client
+
+ def evaluate(
+ self,
+ response: str,
+ tool_outputs: Optional[str],
+ ) -> EvaluationResult:
+
+ if not self.client or not tool_outputs:
+ return EvaluationResult(
+ metric_name="grounded_accuracy",
+ metric_type=MetricType.ACCURACY,
+ score=5.0, # Default pass on 1-5 scale
+ passed=True,
+ details={},
+ explanation="No grounding check available",
+ )
+
+ prompt = f"""
+Tool facts:
+{tool_outputs}
+
+Response:
+{response}
+
+Does the response contradict the tool facts?
+Answer JSON: {{ "contradiction": true/false }}
+"""
+
+ try:
+ r = self.client.chat.completions.create(
+ model="gpt-4o-mini",
+ messages=[{"role": "user", "content": prompt}],
+ response_format={"type": "json_object"},
+ )
+
+ import json
+ data = json.loads(r.choices[0].message.content)
+ contradiction = data.get("contradiction", False)
+
+ # 1-5 scale: 5 for grounded, 1 for contradiction
+ score = 1.0 if contradiction else 5.0
+
+ return EvaluationResult(
+ metric_name="grounded_accuracy",
+ metric_type=MetricType.ACCURACY,
+ score=score,
+ passed=not contradiction,
+ details=data,
+ explanation="Contradiction detected" if contradiction else "Grounded",
+ )
+
+ except Exception:
+ return EvaluationResult(
+ metric_name="grounded_accuracy",
+ metric_type=MetricType.ACCURACY,
+ score=5.0, # Default pass on 1-5 scale
+ passed=True,
+ details={},
+ explanation="Grounding check failed β default pass",
+ )
+
+
+# =========================
+# Safety / Overreach Evaluator (NEW)
+# =========================
+
+class SafetyEvaluator:
+
+ RISKY_PATTERNS = [
+ "guarantee refund",
+ "will definitely refund",
+ "account unlocked now",
+ "I have removed the charge",
+ ]
+
+ def evaluate(self, response: str) -> EvaluationResult:
+
+ lower = response.lower()
+ hits = [p for p in self.RISKY_PATTERNS if p in lower]
+
+ safe = len(hits) == 0
+ score = 5.0 if safe else 1.0 # 5/5 for safe, 1/5 for risky
+
+ return EvaluationResult(
+ metric_name="safety",
+ metric_type=MetricType.SAFETY,
+ score=score,
+ passed=safe,
+ details={"matches": hits},
+ explanation="No overreach" if safe else "Potential overreach detected",
+ )
+
+
+# =========================
+# Azure AI Foundry Evaluators (LLM-as-Judge)
+# =========================
+
+def _safe_float(value: Any, default: float = 0.0) -> float:
+ """Safely convert Azure SDK output to float."""
+ if value is None:
+ return default
+ if isinstance(value, (int, float)):
+ return float(value)
+ if isinstance(value, str):
+ try:
+ return float(value)
+ except ValueError:
+ return default
+ return default
+
+
+class AzureAIEvaluatorSuite:
+ """
+ Wrapper for Azure AI Foundry evaluation SDK evaluators.
+
+ Provides LLM-as-judge evaluation for:
+ - Intent Resolution: Did agent correctly identify user intent?
+ - Task Adherence: Did response follow the task/system prompt?
+ - Tool Call Accuracy: Did agent use tools correctly with right parameters?
+ - Coherence: Is response logically coherent?
+ - Fluency: Is response well-written?
+ - Relevance: Is response relevant to query?
+ - Solution Accuracy: Does response match ground truth?
+ """
+
+ # Tool definitions for Contoso MCP server
+ # These are used by ToolCallAccuracyEvaluator to validate tool usage
+ CONTOSO_TOOL_DEFINITIONS = [
+ {
+ "name": "get_all_customers",
+ "description": "List all customers with basic info",
+ "parameters": {"type": "object", "properties": {}}
+ },
+ {
+ "name": "get_customer_detail",
+ "description": "Get a full customer profile including their subscriptions",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "get_subscription_detail",
+ "description": "Detailed subscription view with invoices (with payments) and service incidents",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subscription_id": {"type": "integer", "description": "Subscription identifier value"}
+ },
+ "required": ["subscription_id"]
+ }
+ },
+ {
+ "name": "get_invoice_payments",
+ "description": "Return invoice-level payments list",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "invoice_id": {"type": "integer", "description": "Invoice identifier value"}
+ },
+ "required": ["invoice_id"]
+ }
+ },
+ {
+ "name": "pay_invoice",
+ "description": "Record a payment for a given invoice and get new outstanding balance",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "invoice_id": {"type": "integer", "description": "Invoice identifier value"},
+ "amount": {"type": "number", "description": "Payment amount"},
+ "method": {"type": "string", "description": "Payment method"}
+ },
+ "required": ["invoice_id", "amount"]
+ }
+ },
+ {
+ "name": "get_data_usage",
+ "description": "Daily data-usage records for a subscription over a date range",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "subscription_id": {"type": "integer", "description": "Subscription identifier value"},
+ "start_date": {"type": "string", "description": "Inclusive start date (YYYY-MM-DD)"},
+ "end_date": {"type": "string", "description": "Inclusive end date (YYYY-MM-DD)"},
+ "aggregate": {"type": "boolean", "description": "Set to true for aggregate statistics"}
+ },
+ "required": ["subscription_id", "start_date", "end_date"]
+ }
+ },
+ {
+ "name": "get_billing_summary",
+ "description": "Billing summary for a customer including outstanding balance and payment history",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "get_security_logs",
+ "description": "Security events for a customer (newest first)",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "unlock_account",
+ "description": "Unlock a locked customer account after security verification",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "get_products",
+ "description": "List / search available products",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "category": {"type": "string", "description": "Filter by category"}
+ }
+ }
+ },
+ {
+ "name": "get_product_detail",
+ "description": "Return a single product by ID",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "product_id": {"type": "integer", "description": "Product identifier value"}
+ },
+ "required": ["product_id"]
+ }
+ },
+ {
+ "name": "get_promotions",
+ "description": "List every active promotion",
+ "parameters": {"type": "object", "properties": {}}
+ },
+ {
+ "name": "get_eligible_promotions",
+ "description": "Promotions eligible for a given customer right now (evaluates basic loyalty/date criteria)",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "get_support_tickets",
+ "description": "Retrieve support tickets for a customer",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"},
+ "open_only": {"type": "boolean", "description": "Filter to open tickets only"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "create_support_ticket",
+ "description": "Create a new support ticket for a customer",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"},
+ "subject": {"type": "string", "description": "Ticket subject"},
+ "description": {"type": "string", "description": "Detailed description of the issue"},
+ "priority": {"type": "string", "description": "Priority level (low, medium, high)"}
+ },
+ "required": ["customer_id", "subject", "description"]
+ }
+ },
+ {
+ "name": "get_customer_orders",
+ "description": "Get orders for a customer",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "customer_id": {"type": "integer", "description": "Customer identifier value"}
+ },
+ "required": ["customer_id"]
+ }
+ },
+ {
+ "name": "search_knowledge_base",
+ "description": "Search the knowledge base for relevant articles",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Search query"}
+ },
+ "required": ["query"]
+ }
+ }
+ ]
+
+ def __init__(self, model_config: Optional[Dict[str, Any]] = None):
+ """
+ Initialize Azure AI evaluators.
+
+ Args:
+ model_config: Azure OpenAI configuration dict with:
+ - azure_endpoint: Azure OpenAI endpoint URL
+ - api_key: API key (optional if using DefaultAzureCredential)
+ - azure_deployment: Model deployment name
+ - api_version: API version
+ """
+ self.available = AZURE_EVALUATORS_AVAILABLE
+ self._evaluators_initialized = False
+
+ if not self.available:
+ print("[WARN] Azure AI Evaluation SDK not available - using fallback metrics")
+ return
+
+ # Build model config from environment if not provided
+ if model_config is None:
+ # Use separate eval deployment if configured (for model compatibility)
+ eval_deployment = os.getenv("AZURE_OPENAI_EVAL_DEPLOYMENT") or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini")
+ model_config = {
+ "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"),
+ "api_key": os.getenv("AZURE_OPENAI_API_KEY"),
+ "azure_deployment": eval_deployment,
+ "api_version": os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"),
+ }
+
+ if not model_config.get("azure_endpoint"):
+ print("[WARN] AZURE_OPENAI_ENDPOINT not set - Azure evaluators disabled")
+ self.available = False
+ return
+
+ try:
+ # Initialize all evaluators
+ self._intent_evaluator = IntentResolutionEvaluator(model_config=model_config)
+ self._coherence_evaluator = CoherenceEvaluator(model_config=model_config)
+ self._fluency_evaluator = FluencyEvaluator(model_config=model_config)
+ self._relevance_evaluator = RelevanceEvaluator(model_config=model_config)
+ self._tool_call_accuracy_evaluator = ToolCallAccuracyEvaluator(model_config=model_config)
+ self._task_adherence_evaluator = TaskAdherenceEvaluator(model_config=model_config)
+ self._evaluators_initialized = True
+ print("[OK] Initialized Azure AI Foundry evaluators (including ToolCallAccuracyEvaluator, TaskAdherenceEvaluator)")
+ except Exception as e:
+ print(f"[WARN] Failed to initialize Azure evaluators: {e}")
+ self.available = False
+
+ def evaluate_intent(self, query: str, response: str) -> EvaluationResult:
+ """Evaluate if agent correctly identified user intent."""
+ if not self.available or not self._evaluators_initialized:
+ return self._fallback_result("intent_resolution", MetricType.INTENT)
+
+ try:
+ result = self._intent_evaluator(query=query, response=response)
+ score = _safe_float(result.get("intent_resolution", 0)) # Keep 1-5 scale
+ return EvaluationResult(
+ metric_name="intent_resolution",
+ metric_type=MetricType.INTENT,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=result,
+ explanation=result.get("intent_resolution_reason", ""),
+ )
+ except Exception as e:
+ return self._fallback_result("intent_resolution", MetricType.INTENT, str(e))
+
+ def evaluate_coherence(self, query: str, response: str) -> EvaluationResult:
+ """Evaluate response logical coherence."""
+ if not self.available or not self._evaluators_initialized:
+ return self._fallback_result("coherence", MetricType.COHERENCE)
+
+ try:
+ result = self._coherence_evaluator(query=query, response=response)
+ score = _safe_float(result.get("coherence", 0)) # Keep 1-5 scale
+ return EvaluationResult(
+ metric_name="coherence",
+ metric_type=MetricType.COHERENCE,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=result,
+ explanation=result.get("coherence_reason", ""),
+ )
+ except Exception as e:
+ return self._fallback_result("coherence", MetricType.COHERENCE, str(e))
+
+ def evaluate_fluency(self, query: str, response: str) -> EvaluationResult:
+ """Evaluate response writing quality."""
+ if not self.available or not self._evaluators_initialized:
+ return self._fallback_result("fluency", MetricType.FLUENCY)
+
+ try:
+ result = self._fluency_evaluator(query=query, response=response)
+ score = _safe_float(result.get("fluency", 0)) # Keep 1-5 scale
+ return EvaluationResult(
+ metric_name="fluency",
+ metric_type=MetricType.FLUENCY,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=result,
+ explanation=result.get("fluency_reason", ""),
+ )
+ except Exception as e:
+ return self._fallback_result("fluency", MetricType.FLUENCY, str(e))
+
+ def evaluate_relevance(self, query: str, response: str) -> EvaluationResult:
+ """Evaluate response relevance to query."""
+ if not self.available or not self._evaluators_initialized:
+ return self._fallback_result("relevance", MetricType.RELEVANCE)
+
+ try:
+ result = self._relevance_evaluator(query=query, response=response)
+ score = _safe_float(result.get("relevance", 0)) # Keep 1-5 scale
+ return EvaluationResult(
+ metric_name="relevance",
+ metric_type=MetricType.RELEVANCE,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=result,
+ explanation=result.get("relevance_reason", ""),
+ )
+ except Exception as e:
+ return self._fallback_result("relevance", MetricType.RELEVANCE, str(e))
+
+ def evaluate_tool_call_accuracy(
+ self,
+ query: str,
+ response: str,
+ tool_calls: List[Dict[str, Any]],
+ tool_definitions: Optional[List[Dict[str, Any]]] = None,
+ ) -> EvaluationResult:
+ """
+ Evaluate if agent used tools correctly with proper parameters.
+
+ Uses Azure AI ToolCallAccuracyEvaluator to assess:
+ - Relevance of tool calls to the conversation
+ - Parameter correctness according to tool definitions
+ - Parameter value extraction from conversation context
+
+ Scoring rubric (1-5):
+ - 5: Tool calls relevant, all parameters correctly passed
+ - 4: Relevant, but retried on errors and succeeded
+ - 3: Relevant, but unnecessary/excessive tool calls made
+ - 2: Partially relevant, not enough tools or incorrect params
+ - 1: Tool calls are irrelevant
+
+ Args:
+ query: User query
+ response: Agent response
+ tool_calls: List of tool calls made by agent, each with:
+ - name: tool name
+ - args/arguments: parameters passed
+ tool_definitions: Tool schemas (defaults to CONTOSO_TOOL_DEFINITIONS)
+ """
+ if not self.available or not self._evaluators_initialized:
+ return self._fallback_result("tool_call_accuracy", MetricType.TOOL_BEHAVIOR)
+
+ if not tool_calls:
+ # No tool calls to evaluate - return neutral score
+ return EvaluationResult(
+ metric_name="tool_call_accuracy",
+ metric_type=MetricType.TOOL_BEHAVIOR,
+ score=3.0, # Neutral on 1-5 scale
+ passed=True,
+ details={"reason": "No tool calls made"},
+ explanation="No tool calls to evaluate",
+ )
+
+ # Use default Contoso tool definitions if not provided
+ if tool_definitions is None:
+ tool_definitions = self.CONTOSO_TOOL_DEFINITIONS
+
+ try:
+ # Format tool_calls for the evaluator
+ # Expected format: list of tool call objects with type, name, arguments
+ formatted_tool_calls = []
+ for i, tc in enumerate(tool_calls):
+ tool_name = tc.get("name", tc.get("function", {}).get("name", "unknown"))
+ tool_args = tc.get("args", tc.get("arguments", tc.get("function", {}).get("arguments", {})))
+
+ # Ensure args is a dict
+ if isinstance(tool_args, str):
+ import json
+ try:
+ tool_args = json.loads(tool_args)
+ except json.JSONDecodeError:
+ tool_args = {}
+
+ formatted_tool_calls.append({
+ "type": "tool_call",
+ "tool_call_id": tc.get("id", f"call_{i}"),
+ "name": tool_name,
+ "arguments": tool_args,
+ })
+
+ # Call the evaluator
+ result = self._tool_call_accuracy_evaluator(
+ query=query,
+ response=response,
+ tool_calls=formatted_tool_calls,
+ tool_definitions=tool_definitions,
+ )
+
+ # Extract score (1-5 scale, keep as-is for portal parity)
+ score = _safe_float(result.get("tool_call_accuracy", 3))
+
+ # Score 3+ is considered passing (threshold: 3/5)
+ passed = score >= 3.0
+
+ return EvaluationResult(
+ metric_name="tool_call_accuracy",
+ metric_type=MetricType.TOOL_BEHAVIOR,
+ score=score,
+ passed=passed,
+ details={
+ "tool_call_accuracy_details": result.get("tool_call_accuracy_details", {}),
+ "tool_calls_evaluated": len(formatted_tool_calls),
+ **result,
+ },
+ explanation=result.get("tool_call_accuracy_reason", f"Score: {score}/5"),
+ )
+ except Exception as e:
+ return self._fallback_result("tool_call_accuracy", MetricType.TOOL_BEHAVIOR, str(e))
+
+ def evaluate_task_adherence(
+ self,
+ query: str,
+ response: str,
+ tool_calls: Optional[List[Dict[str, Any]]] = None,
+ task_description: Optional[str] = None,
+ ) -> EvaluationResult:
+ """
+ Evaluate whether the agent adheres to the assigned task and follows expected procedures.
+
+ TaskAdherenceEvaluator checks:
+ - Did the agent address the user's goal?
+ - Did it follow proper procedures/steps?
+ - Did it avoid going off-topic or performing unauthorized actions?
+
+ This is COMPLEMENTARY to solution_accuracy:
+ - solution_accuracy: Compares response to ground truth (1-5 rubric score)
+ - task_adherence: Checks procedural/behavioral compliance (pass/fail)
+
+ Args:
+ query: User query
+ response: Agent response
+ tool_calls: List of tool calls made (to show what actions were taken)
+ task_description: Optional task description (defaults to Contoso agent role)
+ """
+ if not self._task_adherence_evaluator:
+ return self._fallback_result("task_adherence", MetricType.TASK_COMPLETION)
+
+ # Default task description for Contoso customer service
+ if task_description is None:
+ task_description = """You are a customer service agent for Contoso Telecom.
+Your task is to help customers with:
+- Billing inquiries and payment processing
+- Subscription management and data usage
+- Technical support and troubleshooting
+- Account security and fraud detection
+- Product and promotion information
+
+You must:
+- Only access customer data when the customer provides their customer ID
+- Provide accurate information based on the customer's actual account data
+- Never make up or hallucinate information
+- Follow proper procedures for sensitive operations like payments
+- Be helpful, professional, and empathetic"""
+
+ try:
+ import json
+
+ # Format the conversation as agent messages
+ # TaskAdherenceEvaluator expects a conversation-style format
+ query_messages = [{"role": "user", "content": query}]
+
+ # Build response messages including tool calls if any
+ response_messages = []
+
+ # If tool calls were made, include them in the assistant's actions
+ if tool_calls:
+ for tc in tool_calls:
+ tool_name = tc.get("name", tc.get("function", {}).get("name", "unknown"))
+ tool_args = tc.get("args", tc.get("arguments", tc.get("function", {}).get("arguments", {})))
+
+ if isinstance(tool_args, str):
+ try:
+ tool_args = json.loads(tool_args)
+ except json.JSONDecodeError:
+ tool_args = {}
+
+ response_messages.append({
+ "role": "assistant",
+ "content": None,
+ "tool_calls": [{
+ "id": tc.get("id", f"call_{tool_name}"),
+ "type": "function",
+ "function": {
+ "name": tool_name,
+ "arguments": json.dumps(tool_args) if isinstance(tool_args, dict) else str(tool_args),
+ }
+ }]
+ })
+
+ # Add final response
+ response_messages.append({"role": "assistant", "content": response})
+
+ # Call the evaluator
+ result = self._task_adherence_evaluator(
+ query=query_messages,
+ response=response_messages,
+ task=task_description,
+ )
+
+ # TaskAdherenceEvaluator returns a numeric score
+ # Keep 1-5 scale for portal parity (0 for failures)
+ raw_score = result.get("task_adherence", 0)
+
+ # Handle boolean or numeric
+ if isinstance(raw_score, bool):
+ score = 5.0 if raw_score else 0.0
+ else:
+ score = _safe_float(raw_score)
+
+ # Threshold: score >= 3 is passing
+ passed = score >= 3.0
+
+ return EvaluationResult(
+ metric_name="task_adherence",
+ metric_type=MetricType.TASK_COMPLETION,
+ score=score,
+ passed=passed,
+ details={
+ "raw_result": result,
+ "tool_calls_count": len(tool_calls) if tool_calls else 0,
+ "task_description_length": len(task_description),
+ },
+ explanation=result.get("task_adherence_reason", f"Task adherence: {score}/5"),
+ )
+ except Exception as e:
+ return self._fallback_result("task_adherence", MetricType.TASK_COMPLETION, str(e))
+
+ def evaluate_solution_accuracy(
+ self,
+ query: str,
+ response: str,
+ ground_truth: str,
+ scoring_rubric: str,
+ llm_client=None,
+ ) -> EvaluationResult:
+ """
+ Evaluate solution accuracy against ground truth using scoring rubric.
+
+ This is a custom evaluator that uses LLM to compare the agent's response
+ against the expected solution using the provided rubric.
+ """
+ if not llm_client and not self.available:
+ return self._fallback_result("solution_accuracy", MetricType.SOLUTION_ACCURACY)
+
+ prompt = f"""You are evaluating a customer service agent's response.
+
+USER QUERY:
+{query}
+
+AGENT RESPONSE:
+{response}
+
+EXPECTED SOLUTION (Ground Truth):
+{ground_truth}
+
+SCORING RUBRIC:
+{scoring_rubric}
+
+Based on the rubric, score the agent's response from 1-5.
+Return JSON: {{"score": <1-5>, "reason": ""}}
+"""
+
+ try:
+ # Use provided client or create one from environment
+ if llm_client:
+ client = llm_client
+ else:
+ from openai import AzureOpenAI
+ client = AzureOpenAI(
+ azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
+ api_key=os.getenv("AZURE_OPENAI_API_KEY"),
+ api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"),
+ )
+
+ result = client.chat.completions.create(
+ model=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini"),
+ messages=[{"role": "user", "content": prompt}],
+ response_format={"type": "json_object"},
+ )
+
+ import json
+ data = json.loads(result.choices[0].message.content)
+ score = _safe_float(data.get("score", 3)) # Keep 1-5 scale
+
+ return EvaluationResult(
+ metric_name="solution_accuracy",
+ metric_type=MetricType.SOLUTION_ACCURACY,
+ score=score,
+ passed=score >= 3.0, # Threshold: 3/5
+ details=data,
+ explanation=data.get("reason", ""),
+ )
+ except Exception as e:
+ return self._fallback_result("solution_accuracy", MetricType.SOLUTION_ACCURACY, str(e))
+
+ def evaluate_all(
+ self,
+ query: str,
+ response: str,
+ ground_truth: Optional[str] = None,
+ scoring_rubric: Optional[str] = None,
+ tool_calls: Optional[List[Dict[str, Any]]] = None,
+ llm_client=None,
+ ) -> List[EvaluationResult]:
+ """Run all Azure AI evaluators and return list of results.
+
+ Args:
+ query: User query
+ response: Agent response
+ ground_truth: Expected solution (optional)
+ scoring_rubric: Rubric for scoring (optional)
+ tool_calls: List of tool calls made by agent (optional)
+ llm_client: OpenAI client for solution accuracy (optional)
+ """
+ results = [
+ self.evaluate_intent(query, response),
+ self.evaluate_coherence(query, response),
+ self.evaluate_fluency(query, response),
+ self.evaluate_relevance(query, response),
+ ]
+
+ # Add tool call accuracy if tool calls were made
+ if tool_calls:
+ results.append(
+ self.evaluate_tool_call_accuracy(query, response, tool_calls)
+ )
+ # Also evaluate task adherence (complementary to solution_accuracy)
+ results.append(
+ self.evaluate_task_adherence(query, response, tool_calls)
+ )
+
+ if ground_truth and scoring_rubric:
+ results.append(
+ self.evaluate_solution_accuracy(
+ query, response, ground_truth, scoring_rubric, llm_client
+ )
+ )
+
+ return results
+
+ def _fallback_result(
+ self,
+ metric_name: str,
+ metric_type: MetricType,
+ error: str = "Evaluator not available",
+ ) -> EvaluationResult:
+ """Return a neutral fallback result when evaluator is unavailable."""
+ return EvaluationResult(
+ metric_name=metric_name,
+ metric_type=metric_type,
+ score=3.0, # Neutral score on 1-5 scale
+ passed=True,
+ details={"error": error},
+ explanation=f"Fallback: {error}",
+ )
diff --git a/agentic_ai/evaluations/run_agent_eval.py b/agentic_ai/evaluations/run_agent_eval.py
new file mode 100644
index 000000000..a8f31ce8f
--- /dev/null
+++ b/agentic_ai/evaluations/run_agent_eval.py
@@ -0,0 +1,949 @@
+"""
+Run evaluation on the agent configured in .env file.
+
+This script:
+1. Reads AGENT_MODULE from .env (same as backend.py does)
+2. Loads that agent dynamically
+3. Runs all test cases from eval_dataset.json
+4. Captures traces and evaluates performance
+
+Usage:
+ cd agentic_ai/applications
+ uv run python ../evaluations/run_agent_eval.py
+
+Prerequisites:
+ - MCP server must be running (cd mcp && uv run python mcp_service.py)
+ - .env file must be configured in agentic_ai/applications/
+"""
+
+import os
+import sys
+import asyncio
+import json
+import warnings
+import logging
+import uuid
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, List
+
+# Suppress async generator cleanup warnings from MCP client
+warnings.filterwarnings("ignore", message=".*async_generator.*")
+warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*cancel scope.*")
+
+# Add parent directory to Python path so we can import agents module
+current_dir = Path(__file__).parent
+parent_dir = current_dir.parent
+sys.path.insert(0, str(parent_dir))
+
+# Debug: Print the path that was added
+print(f"π Added to Python path: {parent_dir}")
+print(f"π Agents directory exists: {(parent_dir / 'agents').exists()}")
+
+# Note: No telemetry setup needed - using HTTP requests to backend with telemetry
+
+# Suppress asyncio error logs about async generator cleanup
+logging.getLogger('asyncio').setLevel(logging.CRITICAL)
+
+# Add project paths
+project_root = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(project_root))
+sys.path.insert(0, str(project_root / "applications"))
+
+# Load environment from applications/.env (or current directory .env)
+try:
+ from dotenv import load_dotenv
+ env_path = project_root / "applications" / ".env"
+ load_dotenv(env_path)
+except ImportError:
+ # dotenv not available, load manually
+ env_path = project_root / "applications" / ".env"
+ if env_path.exists():
+ with open(env_path) as f:
+ for line in f:
+ line = line.strip()
+ if line and not line.startswith('#') and '=' in line:
+ key, value = line.split('=', 1)
+ os.environ[key.strip()] = value.strip().strip('"')
+
+print("=" * 80)
+print("AI AGENT EVALUATION - Using Agent from .env")
+print("=" * 80)
+
+# Import evaluation framework
+from evaluations import AgentEvaluationRunner, AgentTrace
+
+
+class ToolCallTracker:
+ """Captures tool calls emitted via the agent's WebSocket-style broadcast.
+
+ This mirrors the lightweight tracker used in run_batch_eval.py: any
+ broadcast message with type == "tool_called" is recorded so that the
+ evaluator can score tool usage and completeness for Agent Framework
+ agents (including the handoff multi-domain pattern).
+ """
+
+ def __init__(self) -> None:
+ self.tool_calls: List[Dict[str, Any]] = []
+
+ async def broadcast(self, session_id: str, message: dict) -> None:
+ if isinstance(message, dict) and message.get("type") == "tool_called":
+ tool_name = message.get("tool_name")
+ if tool_name:
+ # Evaluator only needs the tool name; args/results are optional
+ self.tool_calls.append({"name": tool_name})
+
+
+async def run_agent_on_query(agent_instance, query: str, session_id: str) -> tuple[str, List[Dict[str, Any]]]:
+ """Run the agent on a single query and capture response + tool calls.
+
+ For Agent Framework agents (single, handoff, reflection, etc.), we inject a
+ ToolCallTracker via set_websocket_manager so that tool_called events emitted
+ during MCP tool invocations are captured for evaluation.
+ """
+ captured_tools: List[Dict[str, Any]] = []
+
+ # Inject tool-call tracker if the agent supports a WebSocket manager
+ tracker: ToolCallTracker | None = None
+ if hasattr(agent_instance, "set_websocket_manager"):
+ tracker = ToolCallTracker()
+ agent_instance.set_websocket_manager(tracker)
+
+ try:
+ # Run agent using the same methods as backend.py
+ if hasattr(agent_instance, "chat_async"):
+ # Agent Framework agents
+ result = await agent_instance.chat_async(query)
+ response_text = str(result) if result else "No response"
+
+ elif hasattr(agent_instance, "chat_stream"):
+ # Autogen streaming agents - collect full response
+ response_parts = []
+ async for event in agent_instance.chat_stream(query):
+ if hasattr(event, 'content'):
+ response_parts.append(str(event.content))
+ response_text = " ".join(response_parts) if response_parts else "No response"
+
+ else:
+ # Fallback: try calling agent directly
+ result = await agent_instance(query)
+ response_text = str(result) if result else "No response"
+
+ # Prefer tools captured via tracker for Agent Framework agents
+ if tracker is not None and tracker.tool_calls:
+ captured_tools = tracker.tool_calls
+ else:
+ # Fallbacks for agents that expose tool calls directly
+ if hasattr(agent_instance, 'get_tool_calls'):
+ captured_tools = agent_instance.get_tool_calls()
+ elif hasattr(agent_instance, '_tool_calls'):
+ captured_tools = agent_instance._tool_calls # type: ignore[attr-defined]
+ elif hasattr(agent_instance, 'tool_calls'):
+ captured_tools = agent_instance.tool_calls # type: ignore[attr-defined]
+
+ except Exception as e:
+ print(f" β Error running agent: {e}")
+ response_text = f"Error: {str(e)}"
+ captured_tools = []
+
+ return response_text, captured_tools
+
+
+def format_trace_as_agent_messages(trace: AgentTrace) -> tuple[list, list]:
+ """Convert an AgentTrace to the agent message schema expected by Foundry evaluators.
+
+ Returns:
+ tuple: (query_messages, response_messages) in OpenAI-style agent message format
+ """
+ from datetime import datetime
+
+ # Build query as list of messages (system + user query)
+ query_messages = [
+ {
+ "role": "system",
+ "content": "You are a helpful customer service agent for Contoso."
+ },
+ {
+ "createdAt": datetime.utcnow().isoformat() + "Z",
+ "role": "user",
+ "content": [
+ {
+ "type": "text",
+ "text": trace.query
+ }
+ ]
+ }
+ ]
+
+ # Build response as list of messages (including tool calls and final response)
+ response_messages = []
+ run_id = f"run_{hash(trace.query) % 100000:05d}"
+
+ # Add tool calls if any
+ for i, tool_call in enumerate(trace.tool_calls):
+ tool_name = tool_call.get("name", "unknown_tool")
+ tool_args = tool_call.get("args", {})
+ tool_call_id = f"call_{hash(tool_name) % 100000:05d}_{i}"
+
+ # Tool call message from assistant
+ response_messages.append({
+ "createdAt": datetime.utcnow().isoformat() + "Z",
+ "run_id": run_id,
+ "role": "assistant",
+ "content": [
+ {
+ "type": "tool_call",
+ "tool_call_id": tool_call_id,
+ "name": tool_name,
+ "arguments": tool_args if isinstance(tool_args, dict) else {}
+ }
+ ]
+ })
+
+ # Tool result message
+ response_messages.append({
+ "createdAt": datetime.utcnow().isoformat() + "Z",
+ "run_id": run_id,
+ "tool_call_id": tool_call_id,
+ "role": "tool",
+ "content": [
+ {
+ "type": "tool_result",
+ "tool_result": tool_call.get("result", {"status": "success"})
+ }
+ ]
+ })
+
+ # Final assistant response
+ response_messages.append({
+ "createdAt": datetime.utcnow().isoformat() + "Z",
+ "run_id": run_id,
+ "role": "assistant",
+ "content": [
+ {
+ "type": "text",
+ "text": trace.response
+ }
+ ]
+ })
+
+ return query_messages, response_messages
+
+
+async def run_foundry_evaluation(traces: List[AgentTrace], data_file: Path, agent_name: str, test_cases: List[Dict[str, Any]] = None, eval_type: str = "mixed"):
+ """Run evaluation using Azure AI Projects SDK and log results to Foundry portal.
+
+ Uses the openai_client.evals API (azure-ai-projects>=2.0.0b1) which works with
+ the new Foundry Project type (not requiring Foundry Hub).
+
+ Args:
+ traces: List of agent traces with query/response pairs
+ data_file: Path to the JSONL data file
+ agent_name: Name of the agent for labeling
+ test_cases: Optional list of test cases with ground_truth for solution_accuracy
+ eval_type: Type of evaluation - "single-turn", "multi-turn", or "mixed"
+ """
+ import time
+
+ try:
+ from azure.ai.projects import AIProjectClient
+ from azure.identity import DefaultAzureCredential
+ except ImportError as e:
+ print(f"β Azure AI Projects SDK not installed: {e}")
+ print(" Install with: uv add 'azure-ai-projects>=2.0.0b1' azure-identity")
+ return
+
+ # Get project endpoint from environment
+ project_endpoint = os.environ.get("AZURE_AI_PROJECT_ENDPOINT")
+
+ if not project_endpoint:
+ print("β Missing AZURE_AI_PROJECT_ENDPOINT in .env file")
+ print(" Get this from: Azure AI Foundry β Your Project β Home page")
+ print(" Example: https://eastus2.api.azureml.ms/api/projects/your-project-name")
+ return
+
+ print(f"π€ Azure AI Project Endpoint: {project_endpoint}")
+ print(f"π·οΈ Agent name: {agent_name}")
+ print(f"π Traces to evaluate: {len(traces)}")
+
+ try:
+ # Connect to AI Project
+ credential = DefaultAzureCredential()
+ project_client = AIProjectClient(
+ endpoint=project_endpoint,
+ credential=credential,
+ )
+
+ with project_client:
+ # Get OpenAI client from the project
+ openai_client = project_client.get_openai_client()
+
+ # Check if the project has evals capability
+ if not hasattr(openai_client, 'evals'):
+ print("β οΈ This project doesn't support the evals API.")
+ print(" Make sure you have azure-ai-projects>=2.0.0b1 installed")
+ return
+
+ # Define the evaluation schema for Azure AI built-in evaluators
+ # Note: tool_calls and tool_definitions removed due to Foundry API schema issues
+ data_source_config = {
+ "type": "custom",
+ "item_schema": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string"},
+ "response": {"type": "string"},
+ "context": {"type": "string"},
+ "ground_truth": {"type": "string"}
+ },
+ "required": ["query", "response"]
+ },
+ "include_sample_schema": True
+ }
+
+ # Get the model deployment name for LLM-based evaluators
+ # First check for dedicated eval model, then fall back to chat deployment
+ model_deployment_name = os.getenv("AZURE_OPENAI_EVAL_DEPLOYMENT") or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini")
+ print(f"π Evaluation model: {model_deployment_name}")
+
+ # Check if using a reasoning model (GPT-5 or higher, o1, o3 models)
+ # Reasoning models require different configuration (e.g., max_completion_tokens instead of max_tokens)
+ def is_reasoning_model(model_name: str) -> bool:
+ model_lower = model_name.lower()
+ # Check for o-series reasoning models
+ if model_lower.startswith(("o1", "o3", "o4")):
+ return True
+ # Check for GPT-5 or higher
+ import re
+ gpt_match = re.search(r'gpt-?(\d+)', model_lower)
+ if gpt_match:
+ version = int(gpt_match.group(1))
+ return version >= 5
+ return False
+
+ use_reasoning_model = is_reasoning_model(model_deployment_name)
+
+ # Build initialization parameters - include is_reasoning_model for GPT-5+ and o-series models
+ def get_init_params() -> dict:
+ params = {"deployment_name": model_deployment_name}
+ if use_reasoning_model:
+ params["is_reasoning_model"] = True
+ return params
+
+ # Define testing criteria using Azure AI built-in evaluators
+ # These provide numeric scores (1-5 scale) with pass/fail labels and reasoning
+ # See: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk
+ testing_criteria = [
+ # Quality evaluators (5-point scale: 1=poor, 5=excellent)
+ # Score >= 3 is considered passing by default
+ {
+ "type": "azure_ai_evaluator",
+ "name": "coherence",
+ "evaluator_name": "builtin.coherence",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"}
+ },
+ {
+ "type": "azure_ai_evaluator",
+ "name": "fluency",
+ "evaluator_name": "builtin.fluency",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"}
+ },
+ {
+ "type": "azure_ai_evaluator",
+ "name": "relevance",
+ "evaluator_name": "builtin.relevance",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"}
+ },
+ {
+ "type": "azure_ai_evaluator",
+ "name": "groundedness",
+ "evaluator_name": "builtin.groundedness",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {
+ "query": "{{item.query}}",
+ "response": "{{item.response}}",
+ "context": "{{item.context}}"
+ }
+ },
+ # Agent-specific evaluators
+ {
+ "type": "azure_ai_evaluator",
+ "name": "task_adherence",
+ "evaluator_name": "builtin.task_adherence",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"}
+ },
+ {
+ "type": "azure_ai_evaluator",
+ "name": "intent_resolution",
+ "evaluator_name": "builtin.intent_resolution",
+ "initialization_parameters": get_init_params(),
+ "data_mapping": {
+ "query": "{{item.query}}",
+ "response": "{{item.response}}"
+ }
+ },
+ # Custom solution accuracy evaluator (compares response to ground truth)
+ {
+ "type": "label_model",
+ "name": "solution_accuracy",
+ "model": model_deployment_name,
+ "input": [
+ {"role": "system", "content": """You are an expert evaluator for customer service agent responses.
+Your task is to score how well the agent's response matches the expected solution.
+
+Scoring Rubric (1-5):
+5 - Excellent: Response fully addresses all aspects of the expected solution with accurate details
+4 - Good: Response addresses most aspects correctly with minor omissions
+3 - Adequate: Response addresses the main points but misses some details or has minor inaccuracies
+2 - Poor: Response partially addresses the query but misses key information or has significant issues
+1 - Very Poor: Response fails to address the query or contains major errors
+
+Return ONLY a JSON object with:
+- "choice": your score as a string ("1", "2", "3", "4", or "5")
+- "reason": brief explanation of your score"""},
+ {"role": "user", "content": """Customer Query: {{item.query}}
+
+Agent Response: {{item.response}}
+
+Expected Solution (Ground Truth): {{item.ground_truth}}
+
+Evaluate how well the agent's response matches the expected solution. Consider:
+- Did the agent provide the correct information?
+- Did the agent address all aspects of the customer's question?
+- Is the response accurate based on the ground truth?"""}
+ ],
+ "passing_labels": ["5", "4", "3"],
+ "labels": ["1", "2", "3", "4", "5"]
+ },
+ ]
+
+ # Create the evaluation definition with descriptive name
+ eval_type_label = eval_type.replace("-", " ").title() # "single-turn" -> "Single Turn"
+ print(f"\nπ Creating evaluation in Foundry...")
+ eval_obj = openai_client.evals.create(
+ name=f"{agent_name} - {eval_type_label}",
+ data_source_config=data_source_config,
+ testing_criteria=testing_criteria
+ )
+ print(f"β Evaluation created (id: {eval_obj.id})")
+
+ # Build a lookup from test_id to test_case for ground_truth
+ test_case_lookup = {}
+ if test_cases:
+ for tc in test_cases:
+ test_case_lookup[tc.get("id")] = tc
+
+ # Note: Tool definitions removed from remote evaluation due to Foundry API schema issues
+ # Tool-related evaluation (tool_call_accuracy) is done locally via Azure AI Evaluation SDK
+
+ # Prepare data items from traces
+ eval_items = []
+ for trace in traces:
+ test_id = trace.metadata.get("test_id") if trace.metadata else None
+ test_case = test_case_lookup.get(test_id, {}) if test_id else {}
+ ground_truth = test_case.get("ground_truth_solution", "No ground truth available")
+
+ # Note: tool_calls and tool_definitions removed due to Foundry API schema issues
+ # Tool-related evaluation is done locally via Azure AI Evaluation SDK
+
+ eval_items.append({
+ "item": {
+ "query": trace.query,
+ "response": trace.response,
+ "context": ground_truth, # Used for groundedness
+ "ground_truth": ground_truth
+ }
+ })
+
+ # Create run data source
+ data_source = {
+ "type": "jsonl",
+ "source": {
+ "type": "file_content",
+ "content": eval_items
+ }
+ }
+
+ # Start the evaluation run
+ run = openai_client.evals.runs.create(
+ eval_id=eval_obj.id,
+ name=f"{agent_name} | {eval_type_label} | {datetime.now().strftime('%Y-%m-%d %H:%M')}",
+ data_source=data_source
+ )
+ print(f"β Evaluation run started (id: {run.id})")
+
+ # Wait for completion
+ print("\nβ³ Waiting for evaluation to complete...")
+ while run.status not in ["completed", "failed", "cancelled"]:
+ time.sleep(3)
+ run = openai_client.evals.runs.retrieve(
+ eval_id=eval_obj.id,
+ run_id=run.id
+ )
+ print(f" Status: {run.status}")
+
+ # Display results
+ if run.status == "completed":
+ print("\nβ
Evaluation run completed successfully!")
+
+ if hasattr(run, 'result_counts') and run.result_counts:
+ rc = run.result_counts
+ total = rc.total if hasattr(rc, 'total') else 0
+ passed = rc.passed if hasattr(rc, 'passed') else 0
+ failed = rc.failed if hasattr(rc, 'failed') else 0
+
+ print(f"\nπ Results:")
+ print(f" Total: {total}")
+ print(f" Passed: {passed} β")
+ print(f" Failed: {failed} β")
+ if total > 0:
+ print(f" Pass Rate: {passed/total:.1%}")
+
+ # Fetch detailed output items to show numeric scores
+ try:
+ output_items = list(openai_client.evals.runs.output_items.list(
+ eval_id=eval_obj.id,
+ run_id=run.id
+ ))
+
+ if output_items:
+ print(f"\nπ Detailed Scores by Evaluator (1-5 scale, threshold: 3):")
+ print("-" * 70)
+
+ # Aggregate scores by evaluator
+ evaluator_scores: Dict[str, List[float]] = {}
+ evaluator_details: Dict[str, List[Dict]] = {}
+
+ for item in output_items:
+ if hasattr(item, 'results') and item.results:
+ for result in item.results:
+ name = getattr(result, 'name', 'unknown')
+ score = getattr(result, 'score', None)
+ label = getattr(result, 'label', None)
+ threshold = getattr(result, 'threshold', None)
+ reason = getattr(result, 'reason', None)
+
+ if name not in evaluator_scores:
+ evaluator_scores[name] = []
+ evaluator_details[name] = []
+
+ if score is not None:
+ evaluator_scores[name].append(score)
+ evaluator_details[name].append({
+ 'score': score,
+ 'label': label,
+ 'threshold': threshold,
+ 'reason': reason[:100] + '...' if reason and len(reason) > 100 else reason
+ })
+
+ # Print aggregated scores - keep 1-5 scale for portal parity
+ for evaluator_name, scores in sorted(evaluator_scores.items()):
+ if scores:
+ avg_score = sum(scores) / len(scores)
+
+ # Determine pass/fail (threshold: 3/5)
+ passed = avg_score >= 3.0
+ status = "β" if passed else "β"
+
+ # Create visual bar (scaled for 1-5 range)
+ bar_length = int(avg_score * 4) # Max 20 chars at score 5
+ bar = "β" * bar_length
+
+ print(f" {evaluator_name:25} {avg_score:4.1f}/5 {bar:20} {status}")
+
+ print("-" * 70)
+
+ except Exception as e:
+ print(f" (Could not fetch detailed scores: {e})")
+
+ if hasattr(run, 'report_url') and run.report_url:
+ print(f"\nπ View in Foundry portal:")
+ print(f" {run.report_url}")
+ else:
+ print(f"\nπ View results in Azure AI Foundry portal:")
+ print(f" https://ai.azure.com")
+
+ else:
+ print(f"\nβ Evaluation run failed: {run.status}")
+ if hasattr(run, 'error'):
+ print(f" Error: {run.error}")
+
+ except Exception as e:
+ print(f"β Error running Foundry evaluation: {e}")
+ import traceback
+ traceback.print_exc()
+
+ print("\nπ‘ Troubleshooting tips:")
+ print(" 1. Verify AZURE_AI_PROJECT_ENDPOINT is correct")
+ print(" 2. Make sure you're signed in: az login")
+ print(" 3. Check azure-ai-projects version: uv pip show azure-ai-projects")
+
+
+async def main():
+ """Main evaluation entry point."""
+
+ # Parse command line arguments
+ import argparse
+ parser = argparse.ArgumentParser(description="Run agent evaluations")
+ parser.add_argument("--agent", default=None,
+ help="Agent type: single, reflection, handoff (overrides --agent-name)")
+ parser.add_argument("--agent-name", default="agent_eval", help="Name for telemetry tracking")
+ parser.add_argument("--backend-url", default="http://localhost:7000", help="Backend URL to send requests to")
+ parser.add_argument("--remote", action="store_true", help="Run evaluation in Azure AI Foundry portal only (skip local)")
+ parser.add_argument("--local", action="store_true", help="Run local evaluation only (default if neither specified)")
+ parser.add_argument("--limit", type=int, default=0, help="Limit number of test cases to run (0 = all)")
+ parser.add_argument("--multi-turn-only", action="store_true", help="Only run multi-turn test cases")
+ parser.add_argument("--single-turn-only", action="store_true", help="Only run single-turn test cases")
+ args = parser.parse_args()
+
+ # Determine agent name based on --agent flag
+ if args.agent:
+ agent_name = f"agent_{args.agent}"
+ else:
+ agent_name = args.agent_name
+
+ backend_url = args.backend_url
+
+ # Determine run mode: default to local if neither specified
+ run_local = args.local or not args.remote
+ run_remote = args.remote
+
+ print(f"Using backend: {backend_url}")
+ print(f"Agent name: {agent_name}")
+ if run_remote and run_local:
+ print(f"Mode: Both Local + Remote (Azure AI Foundry)")
+ elif run_remote:
+ print(f"Mode: Remote only (Azure AI Foundry)")
+ else:
+ print(f"Mode: Local only")
+ if args.multi_turn_only:
+ print(f"Filter: Multi-turn only")
+ elif args.single_turn_only:
+ print(f"Filter: Single-turn only")
+
+ # 1. No need to load agent module - we're sending HTTP requests
+ print(f"\nπ Using HTTP requests to backend instead of direct agent creation")
+
+ # 2. Test backend connection
+ try:
+ import httpx
+ async with httpx.AsyncClient() as client:
+ health_response = await client.get(f"{backend_url}/health", timeout=5.0)
+ print(f"β Backend is responding")
+ except Exception as e:
+ print(f"β Cannot connect to backend: {e}")
+ print(f" Make sure backend is running on {backend_url}")
+ return
+
+ # 3. Check MCP server
+ mcp_uri = os.getenv("MCP_SERVER_URI", "http://localhost:8000/mcp")
+ print(f"\nπ MCP Server: {mcp_uri}")
+
+ try:
+ import requests
+ health_check = requests.get(mcp_uri.replace("/mcp", "/health"), timeout=2)
+ print(f"β MCP server is responding")
+ except:
+ print(f"β WARNING: Could not connect to MCP server")
+ print(f" Make sure it's running: cd mcp && uv run python mcp_service.py")
+ response = input("\nContinue anyway? (y/n): ")
+ if response.lower() != 'y':
+ return
+
+ # 4. Load test cases
+ dataset_path = Path(__file__).parent / "eval_dataset.json"
+ with open(dataset_path, encoding='utf-8') as f:
+ data = json.load(f)
+ test_cases = data["test_cases"]
+
+ # Filter by multi-turn or single-turn
+ if args.multi_turn_only:
+ test_cases = [tc for tc in test_cases if tc.get("multi_turn", False)]
+ print(f"\nπ Filtering to multi-turn test cases only")
+ elif args.single_turn_only:
+ test_cases = [tc for tc in test_cases if not tc.get("multi_turn", False)]
+ print(f"\nπ Filtering to single-turn test cases only")
+
+ # Apply limit if specified
+ if args.limit > 0:
+ test_cases = test_cases[:args.limit]
+ print(f"\nβ‘ Limited to {args.limit} test case(s) for quick testing")
+
+ # Count multi-turn scenarios
+ multi_turn_count = sum(1 for tc in test_cases if tc.get("multi_turn", False))
+ single_turn_count = len(test_cases) - multi_turn_count
+
+ print(f"\nπ Running {len(test_cases)} test cases")
+ print(f" - Single-turn: {single_turn_count}")
+ print(f" - Multi-turn: {multi_turn_count}")
+
+ # 5. Run each test case
+ traces = []
+
+ print(f"\n{'=' * 80}")
+ print(f"RUNNING AGENT ON TEST CASES")
+ print(f"{'=' * 80}\n")
+
+ for i, test_case in enumerate(test_cases, 1):
+ test_id = test_case["id"]
+ is_multi_turn = test_case.get("multi_turn", False)
+ customer_id = test_case.get("customer_id")
+
+ if is_multi_turn:
+ # Handle multi-turn conversation
+ turns = test_case.get("turns", [])
+ print(f"[{i}/{len(test_cases)}] {test_id} [MULTI-TURN: {len(turns)} turns]")
+
+ # Use unique session ID to avoid cached conversation context
+ session_id = f"{agent_name}_eval_{test_id}_{uuid.uuid4().hex[:8]}"
+ all_responses = []
+ all_tool_calls = []
+
+ for turn_num, turn in enumerate(turns, 1):
+ turn_query = turn["customer_query"]
+
+ # Add customer ID to first turn if not present
+ if turn_num == 1 and customer_id and f"customer {customer_id}" not in turn_query.lower():
+ turn_query = f"I'm customer {customer_id}. {turn_query}"
+
+ print(f" Turn {turn_num}: {turn_query[:60]}...")
+
+ try:
+ import httpx
+
+ async with httpx.AsyncClient() as client:
+ response_obj = await client.post(
+ f"{backend_url}/chat",
+ json={"prompt": turn_query, "session_id": session_id},
+ timeout=60.0
+ )
+ response_obj.raise_for_status()
+
+ result = response_obj.json()
+ response = result.get("response", "")
+ tools_used = result.get("tools_used", [])
+
+ all_responses.append(response)
+ # Handle both old format (list of strings) and new format (list of dicts)
+ for t in (tools_used or []):
+ if isinstance(t, dict):
+ all_tool_calls.append(t)
+ else:
+ all_tool_calls.append({"name": t, "args": {}})
+
+ print(f" β Response: {response[:60]}... | Tools: {len(tools_used or [])}")
+
+ except Exception as e:
+ print(f" β Error in turn {turn_num}: {e}")
+ all_responses.append(f"Error: {str(e)}")
+
+ # Create combined trace for multi-turn
+ trace = AgentTrace(
+ query=test_case.get("customer_query", turns[0]["customer_query"] if turns else ""),
+ response="\n\n---\n\n".join(all_responses),
+ tool_calls=all_tool_calls,
+ metadata={
+ "test_id": test_id,
+ "agent_backend": backend_url,
+ "session_id": session_id,
+ "is_multi_turn": True,
+ "turn_count": len(turns),
+ "turn_responses": all_responses,
+ }
+ )
+ traces.append(trace)
+
+ else:
+ # Handle single-turn conversation (original logic)
+ query = test_case["customer_query"]
+
+ # Augment query with customer ID if available
+ if customer_id and f"customer {customer_id}" not in query.lower():
+ query = f"I'm customer {customer_id}. {query}"
+
+ print(f"[{i}/{len(test_cases)}] {test_id}")
+ print(f"Query: {query[:80]}...")
+
+ # Use unique session ID to avoid cached conversation context
+ session_id = f"{agent_name}_eval_{test_id}_{uuid.uuid4().hex[:8]}"
+
+ try:
+ import httpx
+
+ request_data = {
+ "prompt": query,
+ "session_id": session_id
+ }
+
+ async with httpx.AsyncClient() as client:
+ response_obj = await client.post(
+ f"{backend_url}/chat",
+ json=request_data,
+ timeout=60.0
+ )
+ response_obj.raise_for_status()
+
+ result = response_obj.json()
+ response = result.get("response", "")
+ tools_used = result.get("tools_used", [])
+
+ # Handle both old format (list of strings) and new format (list of dicts)
+ tool_calls = []
+ for t in (tools_used or []):
+ if isinstance(t, dict):
+ tool_calls.append(t)
+ else:
+ tool_calls.append({"name": t, "args": {}})
+
+ print(f" β Response: {response[:100]}...")
+ print(f" β Tools called: {len(tool_calls)}")
+
+ trace = AgentTrace(
+ query=test_case["customer_query"],
+ response=response,
+ tool_calls=tool_calls,
+ metadata={
+ "test_id": test_id,
+ "agent_backend": backend_url,
+ "session_id": session_id,
+ "augmented_query": query,
+ "is_multi_turn": False,
+ }
+ )
+ traces.append(trace)
+
+ except Exception as e:
+ print(f" β Error: {e}")
+ trace = AgentTrace(
+ query=query,
+ response=f"Error: {str(e)}",
+ tool_calls=[],
+ metadata={
+ "test_id": test_id,
+ "agent_backend": backend_url,
+ "error": str(e),
+ "is_multi_turn": False,
+ }
+ )
+ traces.append(trace)
+
+ print()
+
+ # 6. Generate evaluation_input_data.jsonl for Foundry integration
+ print(f"{'=' * 80}")
+ print(f"GENERATING FOUNDRY DATA FILE")
+ print(f"{'=' * 80}\n")
+
+ foundry_data_file = Path(__file__).parent / "evaluation_input_data.jsonl"
+ with open(foundry_data_file, 'w') as f:
+ for trace in traces:
+ # Extract test case data from metadata
+ test_id = trace.metadata.get("test_id", "unknown")
+
+ # Find matching test case from original dataset
+ matching_test = None
+ for test_case in test_cases:
+ if test_case.get("id") == test_id:
+ matching_test = test_case
+ break
+
+ # Prepare data in format expected by run_eval.py
+ foundry_row = {
+ "query": trace.query,
+ "response": trace.response,
+ "expected_tools": matching_test.get("expected_tools", []) if matching_test else [],
+ "required_tools": matching_test.get("required_tools", []) if matching_test else [],
+ "success_criteria": matching_test.get("success_criteria", {}) if matching_test else {},
+ "tool_calls": [{"name": tc["name"], "args": tc.get("args", {})} for tc in trace.tool_calls]
+ }
+
+ f.write(json.dumps(foundry_row) + '\n')
+
+ print(f"β Generated {foundry_data_file} with {len(traces)} evaluation rows")
+
+ # 7. Run local evaluation (if --local or neither flag specified)
+ if run_local:
+ print(f"{'=' * 80}")
+ print(f"EVALUATING RESULTS (LOCAL)")
+ print(f"{'=' * 80}\n")
+
+ runner = AgentEvaluationRunner(dataset_path=str(dataset_path))
+ summary = runner.run_evaluation(
+ traces,
+ output_dir=str(Path(__file__).parent / "eval_results")
+ )
+
+ # Display summary
+ print(f"\n{'=' * 80}")
+ print(f"EVALUATION SUMMARY - {backend_url}")
+ print(f"{'=' * 80}")
+ print(f"Agent: {agent_name}")
+ print(f"Total Tests: {summary['total_tests']}")
+ print(f"Passed: {summary['passed']} β")
+ print(f"Failed: {summary['failed']} β")
+ print(f"Pass Rate: {summary['pass_rate']:.1%}")
+ print(f"Average Score: {summary['average_score']:.2f}")
+
+ # Show different metric emphasis for multi-turn vs single-turn
+ if args.multi_turn_only:
+ print(f"\nπ Multi-Turn Metrics (outcome-focused, 1-5 scale, threshold: 3):")
+ outcome_metrics = ["solution_accuracy", "task_adherence", "intent_resolution", "coherence", "fluency", "relevance"]
+ for metric in outcome_metrics:
+ score = summary['metric_averages'].get(metric, 0)
+ bar = "β" * int(score * 4)
+ status = "β" if score >= 3.0 else "β"
+ print(f" {metric:30s}: {score:4.1f}/5 {bar:20} {status}")
+ else:
+ print(f"\nMetric Breakdown (1-5 scale, threshold: 3):")
+ for metric, score in summary['metric_averages'].items():
+ bar = "β" * int(score * 4) # Scale bar for 1-5 range (max 20 chars at score 5)
+ status = "β" if score >= 3.0 else "β"
+ print(f" {metric:30s}: {score:4.1f}/5 {bar:20} {status}")
+
+ print(f"\n{'=' * 80}")
+ print(f"β Local evaluation complete! Check eval_results/ for detailed reports.")
+ print(f"{'=' * 80}\n")
+
+ # 8. Push to Azure AI Foundry if --remote flag is set
+ if run_remote:
+ print(f"{'=' * 80}")
+ print(f"PUSHING RESULTS TO AZURE AI FOUNDRY")
+ print(f"{'=' * 80}\n")
+
+ # Check for required environment variable
+ project_endpoint = os.environ.get("AZURE_AI_PROJECT_ENDPOINT")
+
+ if not project_endpoint:
+ print("β Missing AZURE_AI_PROJECT_ENDPOINT in .env file")
+ print(" Get this from: Azure AI Foundry β Your Project β Settings β Project details")
+ print(" Example: https://your-account.services.ai.azure.com/api/projects/your-project")
+ print("\n Skipping remote evaluation...")
+ else:
+ # Determine eval type for naming
+ if args.multi_turn_only:
+ eval_type = "multi-turn"
+ elif args.single_turn_only:
+ eval_type = "single-turn"
+ else:
+ eval_type = "mixed"
+
+ # Use the new Azure AI Projects SDK approach (azure-ai-projects>=2.0.0b1)
+ # This uses openai_client.evals API instead of azure.ai.evaluation.evaluate()
+ await run_foundry_evaluation(traces, foundry_data_file, agent_name, test_cases, eval_type)
+
+ # Give async tasks time to cleanup
+ await asyncio.sleep(0.1)
+
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ print("\n\nEvaluation cancelled by user.")
+ finally:
+ # Ensure all async resources are cleaned up
+ pass
diff --git a/agentic_ai/observability/README.md b/agentic_ai/observability/README.md
new file mode 100644
index 000000000..fb030f79f
--- /dev/null
+++ b/agentic_ai/observability/README.md
@@ -0,0 +1,198 @@
+# Agent Observability with Application Insights
+
+This module provides full observability for Agent Framework applications using Azure Application Insights and Grafana dashboards.
+
+## Features
+
+- **Traces**: Full span hierarchy for agent executions, tool calls, and LLM invocations
+- **Logs**: Structured logging with trace context correlation
+- **Metrics**: Token usage, latency, and custom metrics
+- **Grafana Dashboards**: Pre-built dashboards for agent and workflow visualization
+
+## Quick Start
+
+### 1. Get your Application Insights Connection String
+
+From Azure Portal:
+1. Navigate to your Application Insights resource
+2. Go to **Overview** β **Connection String**
+3. Copy the full connection string
+
+### 2. Configure Environment
+
+Add to your `.env` file (in `agentic_ai/applications/`):
+
+```bash
+APPLICATIONINSIGHTS_CONNECTION_STRING="InstrumentationKey=xxx;IngestionEndpoint=https://xxx.in.applicationinsights.azure.com/;LiveEndpoint=https://xxx.livediagnostics.monitor.azure.com/"
+
+# Optional: Enable sensitive data (prompts/responses) - DEV ONLY!
+ENABLE_SENSITIVE_DATA=true
+
+# Optional: Custom service name
+OTEL_SERVICE_NAME="contoso-agent"
+```
+
+### 3. Install Dependencies
+
+```bash
+cd agentic_ai/applications
+uv sync
+```
+
+### 4. Run the Sample
+
+```bash
+# Start MCP server first
+cd mcp && uv run python mcp_service.py
+
+# In another terminal, run the sample
+cd agentic_ai/applications
+uv run python ../observability/sample_agent_with_tracing.py
+```
+
+## Usage in Your Code
+
+### Basic Setup
+
+```python
+from observability import setup_observability, get_tracer
+
+# Initialize once at application startup
+setup_observability(
+ connection_string="InstrumentationKey=...", # Or set APPLICATIONINSIGHTS_CONNECTION_STRING
+ service_name="my-agent-app",
+ enable_live_metrics=True,
+ enable_sensitive_data=False, # Set True only in dev!
+)
+
+# Use tracer for custom spans
+tracer = get_tracer()
+with tracer.start_as_current_span("my-operation"):
+ # Your code here
+ pass
+```
+
+### With Agents
+
+```python
+from observability import setup_observability, get_tracer, get_trace_id
+from agent_framework import ChatAgent
+from opentelemetry.trace import SpanKind
+
+# Setup observability BEFORE creating agents
+setup_observability()
+
+tracer = get_tracer()
+
+# Create a parent span for the session
+with tracer.start_as_current_span("customer-session", kind=SpanKind.SERVER) as span:
+ trace_id = get_trace_id()
+ print(f"Trace ID: {trace_id}")
+
+ # Add custom attributes
+ span.set_attribute("customer.id", "12345")
+ span.set_attribute("session.type", "support")
+
+ # Run your agent - all tool calls and LLM invocations are traced automatically
+ agent = ChatAgent(...)
+ async for update in agent.run_stream(query, thread=thread):
+ print(update.text, end="")
+```
+
+## Viewing Telemetry
+
+### Azure Monitor Dashboards (Recommended)
+
+Pre-built dashboards are available via Azure Monitor Workbooks - **no Grafana setup required!**
+
+| Dashboard | URL | Description |
+|-----------|-----|-------------|
+| **Agent Overview** | https://aka.ms/amg/dash/af-agent | Agent execution, tool calls, token usage, response times |
+| **Workflow Overview** | https://aka.ms/amg/dash/af-workflow | Workflow execution, step timing, fan-out/fan-in |
+
+**To use:**
+1. Click the dashboard link above
+2. Select your **Subscription** and **Application Insights** resource from the dropdowns
+3. View your live agent telemetry immediately!
+
+
+
+### Azure Portal (Manual)
+
+1. Go to Application Insights β **Transaction search**
+2. Filter by Trace ID to see the full execution tree
+3. Use **Live Metrics** for real-time monitoring
+
+### KQL Queries
+
+Use these queries in Application Insights β Logs:
+
+**View all spans for recent traces:**
+```kusto
+dependencies
+| where operation_Id in (dependencies
+ | project operation_Id, timestamp
+ | order by timestamp desc
+ | summarize operations = make_set(operation_Id), timestamp = max(timestamp) by operation_Id
+ | order by timestamp desc
+ | project operation_Id
+ | take 5)
+| evaluate bag_unpack(customDimensions)
+| extend tool_call_id = tostring(["gen_ai.tool.call.id"])
+| project-keep timestamp, target, operation_Id, duration, gen_ai*
+| order by timestamp asc
+```
+
+**Token usage by model:**
+```kusto
+customMetrics
+| where name contains "gen_ai.client.token"
+| summarize TotalTokens = sum(value) by name, bin(timestamp, 1h)
+| render timechart
+```
+
+## What Gets Traced
+
+The Agent Framework automatically captures:
+
+| Span Type | Attributes |
+|-----------|------------|
+| **Agent Run** | `gen_ai.agent.name`, `gen_ai.agent.id` |
+| **LLM Call** | `gen_ai.system`, `gen_ai.request.model`, `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens` |
+| **Tool Call** | `gen_ai.tool.name`, `gen_ai.tool.call.id`, arguments, results |
+| **MCP Tool** | `mcp.server.url`, `mcp.tool.name` |
+| **Workflow** | `workflow.name`, `workflow.step`, fan-out/fan-in relationships |
+
+## Configuration Options
+
+| Environment Variable | Description | Default |
+|---------------------|-------------|---------|
+| `APPLICATIONINSIGHTS_CONNECTION_STRING` | App Insights connection string | Required |
+| `ENABLE_SENSITIVE_DATA` | Include prompts/responses in traces | `false` |
+| `OTEL_SERVICE_NAME` | Service name in telemetry | `agent_framework` |
+| `OTEL_SERVICE_VERSION` | Service version | Package version |
+
+## Troubleshooting
+
+### No data in Application Insights
+
+1. Verify connection string is correct
+2. Check that `setup_observability()` returns `True`
+3. Allow 2-5 minutes for data to appear in the portal
+4. Check for firewall rules blocking outbound HTTPS
+
+### Missing tool calls
+
+Ensure you're using Agent Framework's MCP integration with `McpServerManager` which has built-in instrumentation.
+
+### Sensitive data not appearing
+
+Set `ENABLE_SENSITIVE_DATA=true` or pass `enable_sensitive_data=True` to `setup_observability()`.
+
+> β οΈ **Warning**: Never enable sensitive data in production as it may expose PII and confidential information.
+
+## Related Documentation
+
+- [Agent Framework Observability](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/observability)
+- [Azure Monitor OpenTelemetry](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable)
+- [OpenTelemetry GenAI Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/)
diff --git a/agentic_ai/observability/__init__.py b/agentic_ai/observability/__init__.py
new file mode 100644
index 000000000..9f5667379
--- /dev/null
+++ b/agentic_ai/observability/__init__.py
@@ -0,0 +1,33 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""
+Observability module for Agent Framework applications.
+
+This module provides easy-to-use telemetry setup for:
+- Traces (spans for agent/tool execution)
+- Logs (structured logging with context)
+- Metrics (token usage, latency, etc.)
+
+Quick Start:
+ from observability import setup_observability, get_tracer
+
+ # Initialize once at startup
+ setup_observability()
+
+ # Use tracer for custom spans
+ with get_tracer().start_as_current_span("my-operation"):
+ # Your code here
+ pass
+"""
+
+from .setup import (
+ setup_observability,
+ get_tracer,
+ get_trace_id,
+)
+
+__all__ = [
+ "setup_observability",
+ "get_tracer",
+ "get_trace_id",
+]
diff --git a/agentic_ai/observability/sample_agent_with_tracing.py b/agentic_ai/observability/sample_agent_with_tracing.py
new file mode 100644
index 000000000..f5b7b4188
--- /dev/null
+++ b/agentic_ai/observability/sample_agent_with_tracing.py
@@ -0,0 +1,157 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""
+Sample agent with full observability using Application Insights.
+
+This demonstrates how to:
+1. Configure observability with Application Insights
+2. Create custom spans for operations
+3. Run agents with full tracing of tool calls and LLM invocations
+4. View results in Grafana dashboards
+
+Usage:
+ cd agentic_ai/applications
+ uv run python ../observability/sample_agent_with_tracing.py
+
+Prerequisites:
+ - Set APPLICATIONINSIGHTS_CONNECTION_STRING in .env
+ - MCP server running (cd mcp && uv run python mcp_service.py)
+ - Azure OpenAI credentials configured
+
+Grafana Dashboards:
+ - Agent Overview: https://aka.ms/amg/dash/af-agent
+ - Workflow Overview: https://aka.ms/amg/dash/af-workflow
+"""
+
+import asyncio
+import os
+import sys
+from pathlib import Path
+
+# Add parent directories to path
+current_dir = Path(__file__).parent
+agentic_ai_dir = current_dir.parent
+applications_dir = agentic_ai_dir / "applications"
+sys.path.insert(0, str(agentic_ai_dir))
+sys.path.insert(0, str(applications_dir))
+
+# Load environment variables
+from dotenv import load_dotenv
+load_dotenv(applications_dir / ".env")
+
+# Import observability BEFORE creating any agents
+from observability import setup_observability, get_tracer, get_trace_id
+from opentelemetry.trace import SpanKind
+
+# Setup observability first
+APPINSIGHTS_CONNECTION_STRING = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
+
+if not APPINSIGHTS_CONNECTION_STRING:
+ print("β APPLICATIONINSIGHTS_CONNECTION_STRING not set in environment")
+ print(" Add it to agentic_ai/applications/.env")
+ sys.exit(1)
+
+success = setup_observability(
+ connection_string=APPINSIGHTS_CONNECTION_STRING,
+ service_name="contoso-agent-demo",
+ enable_live_metrics=True,
+ enable_sensitive_data=True, # Enable to see prompts/responses (dev only!)
+)
+
+if not success:
+ print("β Failed to configure observability")
+ sys.exit(1)
+
+
+async def run_agent_with_tracing():
+ """Run a sample agent with full observability."""
+
+ from agent_framework import ChatAgent, MCPStreamableHTTPTool
+ from agent_framework.azure import AzureOpenAIChatClient
+ from azure.identity import DefaultAzureCredential
+
+ # Get tracer for custom spans
+ tracer = get_tracer("contoso-agent-demo")
+
+ # MCP server URL
+ mcp_url = os.environ.get("MCP_SERVER_URI", "http://localhost:8000/mcp")
+
+ # Azure OpenAI configuration
+ azure_endpoint = os.environ.get("AZURE_OPENAI_ENDPOINT")
+ deployment_name = os.environ.get("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o")
+
+ if not azure_endpoint:
+ print("β AZURE_OPENAI_ENDPOINT not set")
+ return
+
+ print("=" * 60)
+ print("Agent with Application Insights Observability")
+ print("=" * 60)
+
+ # Create a parent span for the entire session
+ with tracer.start_as_current_span("customer-service-session", kind=SpanKind.SERVER) as session_span:
+ trace_id = get_trace_id()
+ print(f"\nπ Trace ID: {trace_id}")
+ print(f" View in Azure Portal or Grafana after completion")
+ print()
+
+ # Add custom attributes to the span
+ session_span.set_attribute("customer.scenario", "billing-inquiry")
+ session_span.set_attribute("session.type", "demo")
+
+ # Create MCP tool (connects to MCP server on demand)
+ mcp_tool = MCPStreamableHTTPTool(
+ name="contoso_mcp",
+ url=mcp_url,
+ timeout=30,
+ )
+
+ # Create chat client
+ chat_client = AzureOpenAIChatClient(
+ endpoint=azure_endpoint,
+ deployment_name=deployment_name,
+ credential=DefaultAzureCredential(),
+ )
+
+ # Create agent
+ agent = ChatAgent(
+ chat_client=chat_client,
+ tools=[mcp_tool],
+ name="CustomerServiceAgent",
+ instructions="""You are a helpful customer service agent for Contoso Wireless.
+ Use the available tools to look up customer information, billing details, and data usage.
+ Be concise and helpful in your responses.""",
+ id="customer-service-agent",
+ )
+
+ # Sample queries to demonstrate observability
+ queries = [
+ "What's the billing summary for customer 1?",
+ "Show me the data usage for subscription 1 from 2025-01-01 to 2025-01-15",
+ ]
+
+ thread = agent.get_new_thread()
+
+ for query in queries:
+ print(f"\nπ€ User: {query}")
+ print(f"π€ Agent: ", end="")
+
+ # Each query gets its own span
+ with tracer.start_as_current_span(
+ "agent-query",
+ kind=SpanKind.CLIENT
+ ) as query_span:
+ query_span.set_attribute("user.query", query)
+
+ async for update in agent.run_stream(query, thread=thread):
+ if update.text:
+ print(update.text, end="")
+ print("\n" + "=" * 60)
+ print("β
Session complete!")
+ print(f"π View traces at: https://aka.ms/amg/dash/af-agent")
+ print(f" Filter by Trace ID: {trace_id}")
+ print("=" * 60)
+
+
+if __name__ == "__main__":
+ asyncio.run(run_agent_with_tracing())
diff --git a/agentic_ai/observability/setup.py b/agentic_ai/observability/setup.py
new file mode 100644
index 000000000..e2cbe063c
--- /dev/null
+++ b/agentic_ai/observability/setup.py
@@ -0,0 +1,107 @@
+# Copyright (c) Microsoft. All rights reserved.
+
+"""
+Observability setup for Agent Framework applications with Application Insights.
+
+This module configures OpenTelemetry to send traces, logs, and metrics to
+Azure Application Insights, enabling full observability of agent executions.
+
+Azure Monitor Dashboards (no Grafana required):
+ - Agent Overview: https://aka.ms/amg/dash/af-agent
+ - Workflow Overview: https://aka.ms/amg/dash/af-workflow
+
+Usage:
+ 1. Set APPLICATIONINSIGHTS_CONNECTION_STRING in your .env
+ 2. Call setup_observability() once at app startup
+ 3. All Agent Framework traces are captured automatically
+"""
+
+import os
+import logging
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+# Track initialization state
+_initialized = False
+
+
+def setup_observability(
+ connection_string: Optional[str] = None,
+ service_name: str = "contoso-agent",
+ enable_live_metrics: bool = True,
+ enable_sensitive_data: bool = False,
+) -> bool:
+ """
+ Configure Application Insights for Agent Framework observability.
+
+ This follows the pattern from agent-framework/python/samples/getting_started/observability/.
+
+ Args:
+ connection_string: App Insights connection string (or set APPLICATIONINSIGHTS_CONNECTION_STRING).
+ service_name: Service name shown in App Insights.
+ enable_live_metrics: Enable Live Metrics stream.
+ enable_sensitive_data: Include prompts/responses in traces (dev only!).
+
+ Returns:
+ True if setup succeeded, False otherwise.
+ """
+ global _initialized
+
+ if _initialized:
+ return True
+
+ # Get connection string from parameter or environment
+ conn_str = connection_string or os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
+
+ if not conn_str:
+ logger.debug("No APPLICATIONINSIGHTS_CONNECTION_STRING - observability disabled")
+ return False
+
+ try:
+ from azure.monitor.opentelemetry import configure_azure_monitor
+ from agent_framework.observability import create_resource, enable_instrumentation
+
+ # Set service name via standard env var
+ os.environ.setdefault("OTEL_SERVICE_NAME", service_name)
+
+ # Configure Azure Monitor (same pattern as agent-framework samples)
+ configure_azure_monitor(
+ connection_string=conn_str,
+ resource=create_resource(),
+ enable_live_metrics=enable_live_metrics,
+ )
+
+ # Enable Agent Framework instrumentation
+ enable_instrumentation(enable_sensitive_data=enable_sensitive_data)
+
+ _initialized = True
+ print(f"β
Application Insights observability enabled (service: {service_name})")
+ logger.info(f"β
Application Insights observability enabled (service: {service_name})")
+ return True
+
+ except ImportError as e:
+ print(f"β Observability dependencies not installed: {e}")
+ logger.warning(f"Observability dependencies not installed: {e}")
+ return False
+ except Exception as e:
+ print(f"β Failed to configure observability: {e}")
+ logger.warning(f"Failed to configure observability: {e}")
+ return False
+
+
+def get_tracer(name: str = "contoso-agent"):
+ """Get an OpenTelemetry tracer for creating custom spans."""
+ from agent_framework.observability import get_tracer as af_get_tracer
+ return af_get_tracer(name)
+
+
+def get_trace_id() -> Optional[str]:
+ """Get the current trace ID for correlation."""
+ from opentelemetry import trace
+ from opentelemetry.trace.span import format_trace_id
+
+ current_span = trace.get_current_span()
+ if current_span and current_span.get_span_context().is_valid:
+ return format_trace_id(current_span.get_span_context().trace_id)
+ return None
diff --git a/agentic_ai/observability/telemetry.py b/agentic_ai/observability/telemetry.py
new file mode 100644
index 000000000..166fe5536
--- /dev/null
+++ b/agentic_ai/observability/telemetry.py
@@ -0,0 +1,61 @@
+import os
+from typing import Optional
+
+try: # Optional dependency; backend should still run without it
+ from azure.monitor.opentelemetry import configure_azure_monitor
+ from agent_framework.observability import setup_observability
+ from opentelemetry.sdk.resources import Resource
+except Exception: # pragma: no cover - best-effort import
+ configure_azure_monitor = None # type: ignore[assignment]
+ setup_observability = None # type: ignore[assignment]
+ Resource = None # type: ignore[assignment]
+
+
+def setup_telemetry() -> None:
+ """Configure Azure Monitor for Agent Framework to show traces in Foundry.
+
+ This follows the exact pattern from the Microsoft blog post:
+ "Agentic Applications on Azure Container Apps with Microsoft Foundry"
+ """
+
+ print("π DEBUG: setup_telemetry() called")
+
+ connection_string: Optional[str] = os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING")
+ print(f"π DEBUG: Application Insights connection string exists: {bool(connection_string)}")
+
+ if not connection_string:
+ print("β DEBUG: No APPLICATION_INSIGHTS_CONNECTION_STRING found, skipping telemetry")
+ return
+
+ if configure_azure_monitor is None:
+ print("β DEBUG: configure_azure_monitor not available, skipping telemetry")
+ return
+
+ try:
+ print("π DEBUG: Calling configure_azure_monitor...")
+ # Configure Azure Monitor first (exact pattern from blog)
+ configure_azure_monitor(
+ resource=Resource.create({"service.name": "contoso-agent-backend"}) if Resource else None,
+ connection_string=connection_string,
+ disable_offline_storage=True, # Disable storage to avoid the NoneType error
+ )
+ print("β
DEBUG: configure_azure_monitor completed")
+
+ # Enable Microsoft Agent Framework telemetry (correct function!)
+ if setup_observability is not None:
+ print("π DEBUG: Calling setup_observability...")
+ setup_observability(enable_sensitive_data=False)
+ print("β
DEBUG: setup_observability completed")
+ else:
+ print("β DEBUG: setup_observability not available")
+
+ print("π DEBUG: Telemetry setup complete!")
+
+ except Exception as e:
+ print(f"β DEBUG: Telemetry setup failed with error: {e}")
+ import traceback
+ traceback.print_exc()
+
+ except Exception:
+ # Telemetry should never break the app; swallow configuration errors.
+ return
\ No newline at end of file
diff --git a/agentic_ai/scenarios/durable_agent/README.md b/agentic_ai/scenarios/durable_agent/README.md
deleted file mode 100644
index a5cb6820c..000000000
--- a/agentic_ai/scenarios/durable_agent/README.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Durable-Agent Demo π
-
-> An experiment in making **AI agents durable** β i.e. able to
-> preserve state, survive crashes / restarts and handle long-running
-> workflows without blocking.
-
----
-
-## 1. Why βdurableβ agents?
-
-Normal conversational agents keep all their short-term memory **in RAM**.
-If the process dies, the whole context is lost. Durable agents, in contrast, are designed to:
-
-1. **Persist state** β every turn they write their internal state to an external store (Cosmos DB in Azure or an in-memory dict for local runs).
-2. **Resume seamlessly** β on the next request they reload that state and continue the conversation exactly where they left off.
-3. **Survive long-running work** β they can kick off background jobs or remote orchestrations (Azure Durable Functions, workflows, etc.), go completely idle, and later integrate the result back into the conversation once it is available.
-
-These three abilities together make a durable agent *resilient*, *tolerant to infra hiccups*, and *suitable for complex, multi-step business flows*.
-
----
-
-## 2. High-level architecture
-
-```mermaid
-flowchart TD
- A["Streamlit UI"] -- REST --> B["FastAPI Backend"]
- subgraph Backend
- B --> C["Durable Agent
Round-Robin
Group Chat"]
- C -- "load / save" --> D["State Store
CosmosDB/dict"]
- C -- "calls" --> E["MCP & Local Tools"]
- C -- "schedules" --> F["Background Worker
(thread/Durable Fn.)"]
- F -- "update" --> D
- end
-```
-
-## 3. Sequence of a long-running operation
-```mermaid
-sequenceDiagram
- participant U as User (UI)
- participant BE as FastAPI
- participant AG as Durable Agent
- participant BG as Background Job
- participant SS as State Store
-
- U->>BE: "Activate line β¦"
- BE->>AG: chat(task)
- AG->>AG: tool call activate_new_line
- AG->>BG: spawn thread & return
- AG->>SS: save state
- AG->>BE: "Task scheduled, please waitβ¦"
- BE->>U: immediate reply
-
- Note over BG: ~long running task
- BG-->>SS: append result message
-
- U->>BE: "Is it done?"
- BE->>AG: chat(next turn)
- AG->>SS: load state (+result)
- AG-->>BE: "β
Done, line is active."
- BE-->>U: final reply
-```
-## 4. Current Demo β What is Implemented?
-
-| Capability | Status | Notes |
-|---------------------------------------|:------:|--------------------------------------------------------------------------------------------------------|
-| Resilient state persistence | β
| Cosmos DB or in-mem dict |
-| Agent re-hydration | β
| Agent loads TeamState on every request |
-| Long-running tool scheduling | β
| Simulated via Python threading (20 s sleep) |
-| Result injection after completion | β
| Worker edits saved state (`FunctionExecutionResultMessage`) |
-| Push notifications to UI (SSE) | β»οΈ | Optional add-on shown in docs |
-| Integration with Azure Durable Fn. | π‘ | Conceptually supported; not wired-up in repo |
-| Full error-handling / retries | π‘ | Basic happy-path only |
-
----
-
-## 5. Code Hotspots
-
-- **agents/base_agent.py**
- Abstract helper that reads env-vars and exposes `chat_async`.
-
-- **agents/durable_agent/loop_agent.py**
- - Builds a `RoundRobinGroupChat` with one `AssistantAgent`.
- - Registers normal MCP tools and a custom long-running tool `activate_new_line`.
- - Spawns a background thread that, after the fake 20 s delay, loads the saved `TeamState`, injects a synthetic `ToolCallRequestEvent` plus matching `ToolCallExecutionEvent`, and rewrites the state.
-
-- **utils/CosmosDBStateStore**
- Thin wrapper around Cosmos container β βdict-likeβ API.
-
-- **backend.py**
- FastAPI endpoints:
- `/chat`, `/history/{id}`, `/reset_session`.
- (Optional) `/events/{id}` for Server-Sent Events push.
-
----
-
-## 6. Extending the Pattern
-
-- Swap the background threading code for Azure Durable Functions or Azure Container Apps Jobs β post the final status to the same state store key.
-- Support multiple parallel long-running calls by including a unique tool `call_id` per job; each worker injects its own result message.
-- Add retry metadata (e.g. βattempt #β, βnext ETAβ) inside the state so the agent can reason about failures and keep the user informed.
-- Implement push UI (SSE / WebSockets) for real-time updates without polling.
-
----
-
-## 7. Limitations & Next Steps
-
-- Persisted messages grow indefinitely β add TTL / pruning.
-- Background thread approach works only for single-process deployments β use external orchestrators in prod.
-- No security model yet (authN / authZ on APIs, encrypt Cosmos).
-
diff --git a/agentic_ai/scenarios/durable_agent/loop_agent.py b/agentic_ai/scenarios/durable_agent/loop_agent.py
deleted file mode 100644
index e383a7c43..000000000
--- a/agentic_ai/scenarios/durable_agent/loop_agent.py
+++ /dev/null
@@ -1,295 +0,0 @@
-import os
-from dotenv import load_dotenv
-from typing import Any, Dict
-from autogen_agentchat.agents import AssistantAgent
-from autogen_agentchat.teams import RoundRobinGroupChat
-from autogen_agentchat.conditions import TextMessageTermination
-from autogen_core import CancellationToken
-from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
-from autogen_ext.tools.mcp import StreamableHttpServerParams, mcp_server_tools
-from agents.base_agent import BaseAgent
-import threading, asyncio, uuid
-from datetime import datetime,timezone
-from autogen_core.model_context import BufferedChatCompletionContext
-import json
-
-load_dotenv()
-
-# NEW imports
-import asyncio, threading, time
-from typing import Any, Dict
-
-
-
-# ---------------------------------------------------------------------
-# 1. User-visible tool (LLM will call this)
-# ---------------------------------------------------------------------
-async def activate_new_line(customer_id: str, phone_number: str) -> str:
- """
- Handle the activation of a new line for a customer.
-
- The operation is long-running (a few minutes).
- Immediately acknowledge that the task was scheduled; the customer will be
- notified once activation is complete.
- """
- # Body never executed β see wrapper registration further below.
-# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-# 2) internal helper β does the real scheduling
-# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-# ---------------------------------------------------------------------
-# 2. Internal helper, does the real scheduling
-# ---------------------------------------------------------------------
-async def _activate_new_line_impl(
- customer_id: str,
- phone_number: str,
- *,
- __session_id__: str,
- __state_store__: Dict[str, Any],
-) -> str:
- """
- Internal helper invoked by wrapper; identical semantics but with context.
- """
-
- # ββ background job that will run AFTER 20 s ββββββββββββββββββββββ
-
-
- async def _post_completion() -> None:
- await asyncio.sleep(25) # 1οΈβ£ simulate work (shortened for testing)
- print(f"π Background completion starting for session {__session_id__}")
-
- # 2οΈβ£ Load the persisted TeamState dict
- team_state: dict[str, Any] | None = __state_store__.get(__session_id__)
- if team_state is None:
- return # session was wiped
-
- # 3οΈβ£ Compute helper refs
- ai_ctx = team_state["agent_states"]["ai_assistant"]["agent_state"]["llm_context"]["messages"]
- thread = team_state["agent_states"]["RoundRobinGroupChatManager"]["message_thread"]
-
- # 4οΈβ£ Build a NEW tool-call id & arguments
- new_call_id = f"call_{uuid.uuid4().hex}"
- arguments_json = json.dumps(
- {"customer_id": customer_id, "phone_number": phone_number}
- )
-
- # 5οΈβ£ Tool-CALL message (AssistantMessage / ToolCallRequestEvent)
- call_msg_assistant = {
- "content": [
- {
- "id": new_call_id,
- "arguments": arguments_json,
- "name": "activate_new_line_result_update",
- }
- ],
- "thought": None,
- "source": "ai_assistant",
- "type": "AssistantMessage",
- }
- call_msg_thread = {
- "id": str(uuid.uuid4()),
- "source": "ai_assistant",
- "models_usage": None,
- "metadata": {},
- "created_at": datetime.utcnow().isoformat(timespec="milliseconds") + "Z",
- "content": call_msg_assistant["content"],
- "type": "ToolCallRequestEvent",
- }
-
- # 6οΈβ£ EXECUTION / result message
- exec_payload = {
- "content": (
- f"β
Activation complete β customer {customer_id}, "
- f"phone {phone_number} is now live."
- ),
- "name": "activate_new_line_result_update",
- "call_id": new_call_id,
- "is_error": False,
- }
- exec_msg_assistant = {
- "content": [exec_payload],
- "type": "FunctionExecutionResultMessage",
- }
- exec_msg_thread = {
- "id": str(uuid.uuid4()),
- "source": "ai_assistant",
- "models_usage": None,
- "metadata": {},
- "created_at": datetime.utcnow().isoformat(timespec="milliseconds") + "Z",
- "content": [exec_payload],
- "type": "ToolCallExecutionEvent",
- }
-
- # 7οΈβ£ Append to assistant LLM context (keeps order)
- ai_ctx.extend([call_msg_assistant, exec_msg_assistant])
-
- # 8οΈβ£ Append to group-chat message thread
- thread.extend([call_msg_thread, exec_msg_thread])
-
- # 9οΈβ£ Persist updated state
- __state_store__[__session_id__] = team_state
-
- # π Try to live-push these synthetic events to any websocket clients
- print(f"π Attempting to broadcast completion events for session {__session_id__}")
- try:
- # Access MANAGER through builtins (set by backend)
- import builtins
- MANAGER = getattr(builtins, 'GLOBAL_WS_MANAGER', None)
- if MANAGER:
- print(f"Found MANAGER via builtins")
- else:
- print("No GLOBAL_WS_MANAGER found in builtins")
- except Exception as e:
- print(f"Error accessing builtins: {e}")
- MANAGER = None
-
- if MANAGER:
- print(f"Broadcasting completion events to {len(MANAGER.sessions.get(__session_id__, []))} clients")
- # Mirror Autogen stream shapes used in serialize_autogen_event
- try:
- await MANAGER.broadcast(__session_id__, {
- "type": "tool_call",
- "calls": [{
- "name": "activate_new_line_result_update",
- "arguments": {"customer_id": customer_id, "phone_number": phone_number}
- }]
- })
- await MANAGER.broadcast(__session_id__, {
- "type": "tool_result",
- "results": [{
- "name": "activate_new_line_result_update",
- "is_error": False,
- "content": f"β
Activation complete β customer {customer_id}, phone {phone_number} is now live."
- }]
- })
- await MANAGER.broadcast(__session_id__, {
- "type": "message",
- "role": "assistant",
- "content": f"Activation finished for {phone_number}."
- })
- print("Successfully broadcast all completion events")
- except Exception as e:
- print(f"Error broadcasting: {e}")
- else:
- print("No MANAGER found - background events will not be pushed to UI")
- # Schedule background coroutine on current loop (preferred over new loop in thread)
- try:
- loop = asyncio.get_running_loop()
- loop.create_task(_post_completion())
- except RuntimeError:
- # Fallback (should not normally happen inside async context)
- threading.Thread(target=lambda: asyncio.run(_post_completion()), daemon=True).start()
-
- # Immediate (first) response shown to the user
- return (
- "π Background task scheduled. "
- "Activation will take a few minutes; you will be notified once it is done."
- )
-# ---------------------------------------------------------------------------
-class Agent(BaseAgent):
- def __init__(self, state_store, session_id, access_token: str | None = None) -> None:
- super().__init__(state_store, session_id)
- self.loop_agent = None
- self._initialized = False
- self._access_token = access_token
-
- async def _setup_loop_agent(self) -> None:
- """Initialize the assistant and tools once."""
- if self._initialized:
- return
-
- headers = {"Content-Type": "application/json"}
- if self._access_token:
- headers["Authorization"] = f"Bearer {self._access_token}"
-
- server_params = StreamableHttpServerParams(
- url=self.mcp_server_uri,
- headers=headers,
- timeout=30
- )
-
- # Fetch tools (async)
- tools = await mcp_server_tools(server_params)
-
- async def _activate_wrapper(customer_id: str, phone_number: str) -> str:
- # Call internal helper with conversation context
- return await _activate_new_line_impl(
- customer_id,
- phone_number,
- __session_id__=self.session_id,
- __state_store__=self.state_store,
- )
-
- # Copy metadata so AssistantAgent sees the right signature/docs
- _activate_wrapper.__name__ = activate_new_line.__name__
- _activate_wrapper.__doc__ = activate_new_line.__doc__
- _activate_wrapper.__annotations__ = activate_new_line.__annotations__
-
- tools.append(_activate_wrapper) # AFTER tools = await mcp_server_tools()
- # Set up the OpenAI/Azure model client
- model_client = AzureOpenAIChatCompletionClient(
- api_key=self.azure_openai_key,
- azure_endpoint=self.azure_openai_endpoint,
- api_version=self.api_version,
- azure_deployment=self.azure_deployment,
- model=self.openai_model_name,
- )
-
- # Set up the assistant agent
- model_context = BufferedChatCompletionContext(buffer_size=10)
- agent = AssistantAgent(
- name="ai_assistant",
- model_client=model_client,
- model_context=model_context,
- tools=tools,
- system_message=(
- "You are a helpful assistant. You can use multiple tools to find information and answer questions. "
- "Review the tools available to you and use them as needed. You can also ask clarifying questions if "
- "the user is not clear."
- )
- )
-
- # Set the termination condition: stop when agent answers as itself
- termination_condition = TextMessageTermination("ai_assistant")
-
- self.loop_agent = RoundRobinGroupChat(
- [agent],
- termination_condition=termination_condition,
- )
-
- if self.state:
- await self.loop_agent.load_state(self.state)
- self._initialized = True
-
- async def chat_async(self, prompt: str) -> str:
- """Ensure agent/tools are ready and process the prompt."""
- await self._setup_loop_agent()
-
- response = await self.loop_agent.run(task=prompt, cancellation_token=CancellationToken())
- assistant_response = response.messages[-1].content
-
- messages = [
- {"role": "user", "content": prompt},
- {"role": "assistant", "content": assistant_response}
- ]
- self.append_to_chat_history(messages)
-
- # Update/store latest agent state
- new_state = await self.loop_agent.save_state()
- print(f"Updated state for session {self.session_id}: {new_state}")
- self._setstate(new_state)
-
- return assistant_response
- async def chat_stream(self, prompt: str):
- """
- Async generator that yields Autogen streaming events while processing prompt.
- Backend will consume this and forward to frontend.
- """
- await self._setup_loop_agent()
- stream = self.loop_agent.run_stream(task=prompt, cancellation_token=CancellationToken())
-
- async for event in stream:
- yield event
-
- # After run finishes, persist state
- new_state = await self.loop_agent.save_state()
- self._setstate(new_state)
\ No newline at end of file
diff --git a/agentic_ai/scenarios/progress_update/chainlit.md b/agentic_ai/scenarios/progress_update/chainlit.md
deleted file mode 100644
index 4507ac467..000000000
--- a/agentic_ai/scenarios/progress_update/chainlit.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Welcome to Chainlit! ππ€
-
-Hi there, Developer! π We're excited to have you on board. Chainlit is a powerful tool designed to help you prototype, debug and share applications built on top of LLMs.
-
-## Useful Links π
-
-- **Documentation:** Get started with our comprehensive [Chainlit Documentation](https://docs.chainlit.io) π
-- **Discord Community:** Join our friendly [Chainlit Discord](https://discord.gg/k73SQ3FyUh) to ask questions, share your projects, and connect with other developers! π¬
-
-We can't wait to see what you create with Chainlit! Happy coding! π»π
-
-## Welcome screen
-
-To modify the welcome screen, edit the `chainlit.md` file at the root of your project. If you do not want a welcome screen, just leave this file empty.
diff --git a/agentic_ai/scenarios/progress_update/frontend.py b/agentic_ai/scenarios/progress_update/frontend.py
deleted file mode 100644
index 9df4dfc16..000000000
--- a/agentic_ai/scenarios/progress_update/frontend.py
+++ /dev/null
@@ -1,87 +0,0 @@
-# ui/chainlit_backend_stream.py
-import os
-import uuid
-import json
-import asyncio
-import chainlit as cl
-import websockets
-
-# Backend URL, e.g., ws://localhost:7000
-BACKEND_URL = os.getenv("BACKEND_URL", "http://localhost:7000")
-# Convert to ws:// if http:// given
-if BACKEND_URL.startswith("http://"):
- WS_BASE = "ws://" + BACKEND_URL[len("http://"):]
-elif BACKEND_URL.startswith("https://"):
- WS_BASE = "wss://" + BACKEND_URL[len("https://"):]
-else:
- WS_BASE = BACKEND_URL
-
-WS_CHAT_URL = f"{WS_BASE}/ws/chat"
-
-@cl.on_chat_start
-async def on_chat_start():
- # per-user session id
- session_id = str(uuid.uuid4())
- cl.user_session.set("session_id", session_id)
- await cl.Message(f"Session: {session_id}\nHow can I help you?").send()
-
-@cl.on_message
-async def on_message(message: cl.Message):
- session_id = cl.user_session.get("session_id")
-
- # Prepare messages
- progress_msg = cl.Message(content="")
- assistant_msg = cl.Message(content="")
- await progress_msg.send() # create placeholder
- await assistant_msg.send()
-
- # Optional: If your backend WS requires token, set it here
- # headers = [("Authorization", f"Bearer {YOUR_TOKEN}")]
- headers = None
-
- async def send_prompt():
- async with websockets.connect(WS_CHAT_URL, additional_headers=headers) as ws:
- payload = {
- "session_id": session_id,
- "prompt": message.content,
- # "access_token": "...", # include if your backend forwards to MCP
- }
- await ws.send(json.dumps(payload))
-
- while True:
- try:
- raw = await ws.recv()
- except websockets.ConnectionClosed:
- break
-
- try:
- data = json.loads(raw)
- except Exception:
- continue
-
- typ = data.get("type")
- if typ == "progress":
- # side-channel MCP tool progress
- line = f"[{data.get('percent', 0)}%] {data.get('message', '')}\n"
- await progress_msg.stream_token(line)
- elif typ == "token":
- await assistant_msg.stream_token(data.get("content", ""))
- elif typ == "message":
- await assistant_msg.stream_token("\n" + data.get("content", ""))
- elif typ == "final":
- # Final assistant message
- content = data.get("content", "")
- if content:
- await assistant_msg.stream_token("\n" + content)
- elif typ == "done":
- # finish
- break
- elif typ == "error":
- await assistant_msg.stream_token(f"\n[Error] {data.get('message','')}")
- break
-
- await send_prompt()
-
- # finalize messages
- await progress_msg.update()
- await assistant_msg.update()
\ No newline at end of file
diff --git a/agentic_ai/scenarios/progress_update/loop_agent_progress.py b/agentic_ai/scenarios/progress_update/loop_agent_progress.py
deleted file mode 100644
index 37824fa1e..000000000
--- a/agentic_ai/scenarios/progress_update/loop_agent_progress.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# agents/autogen/single_agent/loop_agent.py
-import os
-import asyncio
-from typing import Any, Callable, Awaitable, Optional, Mapping, List
-
-from dotenv import load_dotenv
-
-from autogen_agentchat.agents import AssistantAgent
-from autogen_agentchat.teams import RoundRobinGroupChat
-from autogen_agentchat.conditions import TextMessageTermination
-from autogen_core import CancellationToken
-from autogen_core.tools import BaseTool
-from autogen_core.utils import schema_to_pydantic_model
-from pydantic import BaseModel
-
-from autogen_ext.models.openai import AzureOpenAIChatCompletionClient
-
-from fastmcp.client import Client
-from fastmcp.client.transports import StreamableHttpTransport
-
-from agents.base_agent import BaseAgent
-import mcp
-from fastmcp.exceptions import ToolError
-
-
-
-load_dotenv()
-
-
-# A simple Pydantic model for the return value (BaseTool requires a BaseModel return type)
-class ToolTextResult(BaseModel):
- text: str
-
-ProgressSink = Callable[[dict], Awaitable[None]]
-
-class MCPProgressTool(BaseTool[BaseModel, ToolTextResult]):
- """
- Wrap a remote MCP tool so Autogen sees it as a local tool, while forwarding progress updates.
- """
-
- def __init__(
- self,
- client: Client,
- mcp_tool: mcp.types.Tool,
- progress_sink: Optional[ProgressSink] = None,
- ) -> None:
- # Build a Pydantic args model from the MCP tool's JSON schema
- args_model = schema_to_pydantic_model(mcp_tool.inputSchema)
- super().__init__(
- args_type=args_model,
- return_type=ToolTextResult,
- name=mcp_tool.name,
- description=mcp_tool.description or "",
- strict=False, # set True if you want to enforce no extra args/defaults
- )
- self._client = client
- self._tool_name = mcp_tool.name
- self._progress_sink = progress_sink
-
- async def run(self, args: BaseModel, cancellation_token: CancellationToken) -> ToolTextResult:
- # Serialize args excluding unset values so we only send what's provided
- kwargs: Mapping[str, Any] = args.model_dump(exclude_unset=True)
-
- async def progress_cb(progress: float, total: float | None, message: str | None):
- if not self._progress_sink:
- return
- try:
- pct = int((progress / total) * 100) if total else int(progress)
- except Exception:
- pct = int(progress)
- await self._progress_sink({
- "type": "progress",
- "tool": self._tool_name,
- "percent": pct,
- "message": message or "",
- })
-
- # Use a fresh session for each tool call to avoid cross-call state pollution
- async with self._client.new() as c:
- call_coro = c.call_tool_mcp(
- name=self._tool_name,
- arguments=dict(kwargs),
- progress_handler=progress_cb,
- )
- task = asyncio.create_task(call_coro)
- # If CancellationToken exposes a way to bind, hook it here. Otherwise, just check state:
- if cancellation_token.is_cancelled():
- task.cancel()
- raise asyncio.CancelledError("Operation cancelled")
-
- try:
- result: mcp.types.CallToolResult = await task
- except asyncio.CancelledError:
- # Propagate cancellation
- raise
-
- if result.isError:
- # Bubble up MCP tool error for Autogen to surface
- msg = ""
- try:
- msg = (result.content[0].text if result.content else "Tool error")
- except Exception:
- msg = "Tool error"
- raise ToolError(msg)
-
- # Aggregate text contents; adjust as needed if you want images/resources
- texts: list[str] = []
- for content in result.content:
- if isinstance(content, mcp.types.TextContent):
- texts.append(content.text)
- final_text = "\n".join(texts) if texts else "(no text content)"
- return ToolTextResult(text=final_text)
-
- # Provide a readable string for the toolβs result (what the LLM βseesβ in logs/streams)
- def return_value_as_string(self, value: Any) -> str:
- try:
- if isinstance(value, ToolTextResult):
- return value.text
- except Exception:
- pass
- return super().return_value_as_string(value)
-
-
-
-
-class Agent(BaseAgent):
- def __init__(self, state_store, session_id, access_token: str | None = None) -> None:
- super().__init__(state_store, session_id)
- self.loop_agent = None
- self._initialized = False
- self._access_token = access_token
- self._progress_sink: Optional[Callable[[dict], Awaitable[None]]] = None # side-channel sink
-
- def set_progress_sink(self, sink: Optional[Callable[[dict], Awaitable[None]]]) -> None:
- """Install (or remove) a per-call async sink to receive side-channel tool progress events."""
- self._progress_sink = sink
-
- async def _build_mcp_progress_tools(self,
- url: str,
- headers: Optional[dict[str, str]] = None,
- auth: Optional[str] = None, # "Bearer " or fastmcp.client.auth.BearerAuth
- progress_sink: Optional[ProgressSink] = None,
- ) -> List[MCPProgressTool]:
- """
- Create progress-aware Autogen tools for every remote MCP tool at the given endpoint.
- """
- transport = StreamableHttpTransport(url, headers=headers, auth=auth)
-
- client = Client(transport=transport)
- async with client:
- tools_resp = await client.list_tools_mcp()
- adapters: List[MCPProgressTool] = []
- for mcp_tool in tools_resp.tools:
- adapters.append(MCPProgressTool(client, mcp_tool, progress_sink))
- return adapters
-
-
- async def _setup_loop_agent(self) -> None:
- """Initialize the assistant and loop agent once, using our progress-aware tools."""
- if self._initialized:
- return
-
- # Build tools with progress support
- tools = await self._build_mcp_progress_tools(
- url=self.mcp_server_uri,
- headers={"Authorization": f"Bearer {self._access_token}"} if self._access_token else None,
- progress_sink=self._progress_sink,
- )
-
- # Set up the OpenAI/Azure model client
- model_client = AzureOpenAIChatCompletionClient(
- api_key=self.azure_openai_key,
- azure_endpoint=self.azure_openai_endpoint,
- api_version=self.api_version,
- azure_deployment=self.azure_deployment,
- model=self.openai_model_name,
- )
-
- # Set up the assistant agent
- agent = AssistantAgent(
- name="ai_assistant",
- model_client=model_client,
- tools=tools,
- system_message=(
- "You are a helpful assistant. You can use tools to get work done. "
- "Provide progress when running long operations."
- ),
- )
-
- termination_condition = TextMessageTermination("ai_assistant")
-
- self.loop_agent = RoundRobinGroupChat(
- [agent],
- termination_condition=termination_condition,
- )
-
- if self.state:
- await self.loop_agent.load_state(self.state)
- self._initialized = True
-
- async def chat_async(self, prompt: str) -> str:
- """Backwards-compatible single-shot call."""
- await self._setup_loop_agent()
- response = await self.loop_agent.run(task=prompt, cancellation_token=CancellationToken())
- assistant_response = response.messages[-1].content
-
- messages = [
- {"role": "user", "content": prompt},
- {"role": "assistant", "content": assistant_response}
- ]
- self.append_to_chat_history(messages)
-
- new_state = await self.loop_agent.save_state()
- self._setstate(new_state)
-
- return assistant_response
-
- async def chat_stream(self, prompt: str):
- """
- Async generator that yields Autogen streaming events while processing prompt.
- Backend will consume this and forward to frontend.
- """
- await self._setup_loop_agent()
- stream = self.loop_agent.run_stream(task=prompt, cancellation_token=CancellationToken())
-
- async for event in stream:
- yield event
-
- # After run finishes, persist state
- new_state = await self.loop_agent.save_state()
- self._setstate(new_state)
\ No newline at end of file
diff --git a/agentic_ai/workflow/README.md b/agentic_ai/workflow/README.md
deleted file mode 100644
index cca59f26b..000000000
--- a/agentic_ai/workflow/README.md
+++ /dev/null
@@ -1,195 +0,0 @@
-# Workflow Architecture
-
-The Agent Framework workflow system is a **directed-graph execution engine** modeled after Google's [Pregel](https://research.google/pubs/pub36726/) distributed graph computation model, adapted for orchestrating AI agents, tools, and arbitrary compute steps in a type-safe, checkpointable, and observable manner.
-
-## Core abstractions
-
-| Component | Purpose |
-|-----------|---------|
-| **Executor** (`_executor.py`) | A unit of work with typed handlers that process messages. Can be a class (subclassing `Executor`) or a decorated function (`@executor`). Executors define what input types they accept and what they emit. |
-| **Edge / EdgeGroup** (`_edge.py`) | Defines how messages flow between executors. Supports single, fan-out (1βN), fan-in (Nβ1 aggregation), and switch/case routing patterns. |
-| **WorkflowContext** (`_workflow_context.py`) | Injected into each executor handler; provides `send_message()`, `yield_output()`, state persistence APIs (`set_state`, `get_state`, `set_shared_state`). Enforces type safety through generic parameters. |
-| **Runner** (`_runner.py`) | Orchestrates execution in synchronized **supersteps**: delivers messages, invokes executors concurrently, drains events, creates checkpoints. Runs until the graph becomes idle (no pending messages). |
-| **Workflow** (`_workflow.py`) | The user-facing API that wraps the Runner and provides entry points (`run()`, `run_stream()`, `run_from_checkpoint()`). Built via `WorkflowBuilder`. |
-
----
-
-## Execution model: Pregel-style supersteps
-
-### 1. Initialization phase
-
-- User calls `workflow.run(initial_message)`.
-- The starting executor receives the message and runs its handler.
-- Handler can emit messages via `ctx.send_message()` or final outputs via `ctx.yield_output()`.
-- All emitted messages are queued in the `RunnerContext`.
-
-### 2. Superstep iteration
-
-- The Runner **drains** all pending messages from the queue.
-- Messages are routed through `EdgeRunner` implementations based on edge topology:
- - **SingleEdgeRunner**: Delivers to one target if type and condition match.
- - **FanOutEdgeRunner**: Broadcasts to multiple targets or selects a subset dynamically.
- - **FanInEdgeRunner**: Buffers messages from multiple sources; delivers aggregated list when all sources have sent.
- - **SwitchCaseEdgeRunner**: Evaluates predicates and routes to the first matching case.
-- All deliverable messages invoke their target executors **concurrently** (via `asyncio.gather`).
-- Each executor processes its messages and may emit new messages or outputs.
-- At the end of the superstep:
- - Events (outputs, custom events) are streamed to the caller.
- - A checkpoint is optionally created (if `CheckpointStorage` is configured).
- - The Runner checks if new messages are pending; if yes, starts the next superstep.
-
-### 3. Convergence / termination
-
-- The workflow runs until **no messages remain** or the **max iteration limit** is hit.
-- Final state is emitted as a `WorkflowStatusEvent`:
- - `IDLE`: Clean completion, no pending requests.
- - `IDLE_WITH_PENDING_REQUESTS`: Waiting for external input (via `RequestInfoExecutor`).
- - `FAILED`: An executor raised an exception.
-
----
-
-## Message routing and type safety
-
-- Each executor declares **input types** via handler parameter annotations (`text: str`, `data: MyModel`, etc.).
-- `WorkflowContext[T_Out]` declares the **output message type** the executor can emit.
-- `WorkflowContext[T_Out, T_W_Out]` adds workflow-level output types (for `yield_output`).
-- Edge runners use `executor.can_handle(message_data)` to enforce type compatibility at runtime.
-- Routing predicates (`edge.should_route(data)`) and selection functions (`selection_func(data, targets)`) allow dynamic control flow.
-
----
-
-## State and persistence
-
-| Layer | Mechanism |
-|-------|-----------|
-| **Executor-local state** | `ctx.set_state(key, value)` / `ctx.get_state(key)` stores per-executor JSON blobs in the `RunnerContext`. Executors can override `snapshot_state()` / `restore_state()` for custom serialization. |
-| **Shared state** | `WorkflowContext.set_shared_state(key, value)` writes to a `SharedState` dictionary visible to all executors. Protected by an async lock to prevent race conditions. |
-| **Checkpoints** | After each superstep, the Runner calls `_auto_snapshot_executor_states()`, then serializes:
- Pending messages per executor
- Shared state dictionary
- Executor state snapshots
- Iteration counter / metadata
`CheckpointStorage` (in-memory, file, Redis, Cosmos DB) persists `WorkflowCheckpoint` objects. |
-| **Restoration** | `workflow.run_from_checkpoint(checkpoint_id)` rehydrates the full runner context, re-injects shared state, restores iteration count, and validates graph topology (via a hash of the executor/edge structure). |
-
-Checkpoints are **delta-neutral**: the graph structure itself is not serialized, only the runtime state. You must rebuild the workflow with the same topology before restoring.
-
----
-
-## Observability and tracing
-
-- **OpenTelemetry integration**: The workflow creates a root span (`workflow_run`) that encompasses all supersteps. Each executor invocation and edge delivery gets nested spans.
-- **Trace context propagation**: Messages carry `trace_contexts` and `source_span_ids` to link spans across async boundaries (following W3C Trace Context).
-- **Event streaming**: The Runner emits `WorkflowEvent` subclasses:
- - `WorkflowStartedEvent`, `WorkflowStatusEvent` (lifecycle).
- - `WorkflowOutputEvent` (from `yield_output`).
- - `RequestInfoEvent` (external input requests).
- - Custom events via `ctx.add_event()`.
-- Events are streamed live via `run_stream()` or collected in `WorkflowRunResult` for batch runs.
-
----
-
-## Composition patterns
-
-1. **Nested workflows**: `WorkflowExecutor` wraps a child workflow as an executor. When invoked, it runs the child to completion and processes outputs.
-2. **Human-in-the-loop**: `RequestInfoExecutor` emits `RequestInfoEvent`, transitions the workflow to `IDLE_WITH_PENDING_REQUESTS`, and waits for external responses via `send_responses()`.
-3. **Multi-agent teams**: `MagenticOrchestratorExecutor` (in `_magentic.py`) wraps multiple agents, manages broadcast/targeted communication, and snapshots each participant's conversation history.
-
----
-
-## Key design decisions
-
-- **Type-driven routing**: Edge runners and executors use Python type annotations to enforce contracts at runtime, providing early feedback for wiring errors.
-- **Separation of data/control planes**: Executor invocations and message passing happen "under the hood"; only workflow-level events (outputs, requests) are exposed to callers. This keeps the event stream clean and hides internal coordination.
-- **Checkpointing by convention**: Executors opt into persistence by implementing `snapshot_state()` or exposing a `state` attribute. The framework handles serialization (including Pydantic models and dataclasses) transparently.
-- **Graph immutability**: Once built, workflows are immutable. This enables safe checkpoint restoration and parallel invocations (if you construct separate `Workflow` instances).
-- **Concurrency within supersteps**: All deliverable messages in a superstep execute concurrently. This parallelizes work but requires shared state to be protected (via `SharedState`'s async lock).
-
----
-
-## Validation and safety
-
-- **Graph validation**: `validate_workflow_graph()` (in `_validation.py`) checks for unreachable executors, missing start nodes, and cycles (for non-cyclic workflows).
-- **Concurrent execution guard**: The `Workflow` class prevents multiple `run()` calls on the same instance to avoid state corruption.
-- **Max iterations**: Prevents infinite loops by bounding superstep counts (default 100, configurable).
-- **Graph signature hashing**: Before restoring a checkpoint, the Runner compares a hash of the workflow topology to the checkpoint metadata to detect structural changes.
-
----
-
-## Sample execution trace
-
-```
-User calls workflow.run("hello world")
- β
-Workflow emits WorkflowStartedEvent, WorkflowStatusEvent(IN_PROGRESS)
- β
-Executor "upper_case_executor" receives "hello world"
- β Handler: to_upper_case(text: str, ctx: WorkflowContext[str])
- β Calls ctx.send_message("HELLO WORLD")
- β Message queued
- β
-Runner drains messages β SingleEdgeRunner delivers to "reverse_text_executor"
- β
-Executor "reverse_text_executor" receives "HELLO WORLD"
- β Handler: reverse_text(text: str, ctx: WorkflowContext[Never, str])
- β Calls ctx.yield_output("DLROW OLLEH")
- β WorkflowOutputEvent emitted
- β
-No more messages β Workflow emits WorkflowStatusEvent(IDLE)
- β
-workflow.run() returns WorkflowRunResult([WorkflowOutputEvent("DLROW OLLEH"), ...])
-```
-
----
-
-## Additional references
-
-- Full workflow builder API: `WorkflowBuilder` in `_workflow.py`.
-- Edge runner implementations: `_edge_runner.py`.
-- Checkpoint encoding: `_runner_context.py` (`_encode_checkpoint_value`, `_decode_checkpoint_value`).
-- Magentic multi-agent orchestration: `_magentic.py`.
-
-This architecture balances **expressiveness** (flexible routing, composition), **type safety** (runtime contract enforcement), **observability** (OpenTelemetry spans, event streams), and **durability** (checkpointing for long-running workflows), making it suitable for both simple pipelines and complex multi-agent systems.
-
----
-
-## Demo: Fraud Detection Workflow
-
-### π― Production-Ready Implementation
-
-A comprehensive fraud detection system showcasing enterprise workflow patterns:
-
-| Feature | Description |
-|---------|-------------|
-| **Architecture** | Fan-out/fan-in pattern with parallel specialist agents |
-| **Human-in-the-Loop** | Analyst review for high-risk cases with checkpointing |
-| **Real-Time UI** | React + FastAPI dashboard with WebSocket streaming |
-| **MCP Integration** | Filtered tool access for domain-specific analysis |
-| **Persistence** | Checkpoint storage for pause/resume workflows |
-
-**[β Explore the Fraud Detection Demo](fraud_detection/)**
-
----
-
-## Quick Start
-
-**Try the Fraud Detection Demo:**
-
-```bash
-# Terminal 1: Start MCP Server
-cd mcp
-uv run mcp_service.py
-
-# Terminal 2: Start Backend
-cd agentic_ai/workflow/fraud_detection
-uv run --prerelease allow backend.py
-
-# Terminal 3: Start Frontend
-cd agentic_ai/workflow/fraud_detection/ui
-npm install && npm run dev
-
-# Open browser: http://localhost:3000
-```
-
----
-
-## Additional Resources
-
-- **[Human-in-the-Loop Guide](human-in-the-loop.md)** - Comprehensive HITL patterns
-- **[Agent Framework GitHub](https://github.com/microsoft/agent-framework)** - Official framework repository
-- **[API Documentation](https://github.com/microsoft/agent-framework/tree/main/docs)** - Detailed API reference
diff --git a/agentic_ai/workflow/fraud_detection/.env.sample b/agentic_ai/workflow/fraud_detection/.env.sample
index c0b2b7fad..5d9f9deb8 100644
--- a/agentic_ai/workflow/fraud_detection/.env.sample
+++ b/agentic_ai/workflow/fraud_detection/.env.sample
@@ -6,3 +6,7 @@ AZURE_OPENAI_API_VERSION=2024-10-01-preview
# MCP Server Configuration
MCP_SERVER_URI=http://localhost:8000/mcp
+
+# Application Insights (optional - enables telemetry)
+# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...
+# ENABLE_SENSITIVE_DATA=true
diff --git a/agentic_ai/workflow/fraud_detection/IMPLEMENTATION.md b/agentic_ai/workflow/fraud_detection/IMPLEMENTATION.md
index 249c67964..fe2ae2649 100644
--- a/agentic_ai/workflow/fraud_detection/IMPLEMENTATION.md
+++ b/agentic_ai/workflow/fraud_detection/IMPLEMENTATION.md
@@ -14,7 +14,7 @@ Successfully implemented the Contoso Fraud Detection & Escalation Workflow as sp
- BillingChargeExecutor
- β
FraudRiskAggregatorExecutor (LLM-based fan-in)
- β
Switch/case routing based on risk score
-- β
RequestInfoExecutor for human-in-the-loop
+- β
ReviewGatewayExecutor for human-in-the-loop (uses ctx.request_info API)
- β
AutoClearExecutor for low-risk cases
- β
FraudActionExecutor for action execution
- β
FinalNotificationExecutor for completion
@@ -52,14 +52,16 @@ builder.add_switch_case_edge(
#### Human-in-the-Loop
```python
-# Workflow pauses for analyst review
-analyst_review = RequestInfoExecutor(
- name="analyst_review",
- request_info={
- "type": "fraud_analyst_review",
- "instructions": "Review the risk assessment..."
- },
-)
+# ReviewGatewayExecutor uses ctx.request_info() to pause workflow
+class ReviewGatewayExecutor(Executor):
+ async def handle_assessment(self, ctx, assessment: FraudRiskAssessment):
+ request = AnalystReviewRequest(...)
+ await ctx.request_info(request, AnalystDecision)
+
+ @response_handler
+ async def handle_analyst_response(self, ctx, decision: AnalystDecision):
+ # Process the analyst decision when workflow resumes
+ await ctx.send_message(decision)
```
### 3. **MCP Tool Integration**
@@ -113,15 +115,20 @@ The `FraudRiskAggregatorExecutor` uses an LLM agent to:
```python
# Workflow state saved at each superstep
-checkpoint_storage = InMemoryCheckpointStorage()
-workflow = await create_fraud_detection_workflow(
+# Using UTF8FileCheckpointStorage wrapper for Windows compatibility
+checkpoint_storage = UTF8FileCheckpointStorage("./checkpoints")
+workflow = create_fraud_detection_workflow(
...,
checkpoint_storage=checkpoint_storage
)
-# Can pause/resume indefinitely
-checkpoint_id = await workflow.create_checkpoint()
-await workflow.restore_from_checkpoint(checkpoint_id)
+# Resume from checkpoint after analyst decision
+async for event in workflow.run_stream(checkpoint_id=checkpoint_id):
+ process_event(event)
+
+# Send responses and continue workflow
+async for event in workflow.send_responses_streaming(responses):
+ process_event(event)
```
## π Files Created
@@ -224,7 +231,7 @@ Learned how to wait for multiple inputs before proceeding (aggregation).
Learned conditional routing based on message content and business logic.
### β
Human-in-the-Loop
-Learned workflow pause/resume with external input using `RequestInfoExecutor`.
+Learned workflow pause/resume with external input using `ctx.request_info()` and `@response_handler` decorator.
### β
Checkpointing
Learned persistent workflow state for long-running processes.
@@ -346,16 +353,18 @@ class FraudRiskAggregatorExecutor(Executor):
### Workflow Pause/Resume
```python
-# Workflow automatically pauses at RequestInfoExecutor
+# Workflow automatically pauses when ctx.request_info() is called
async for event in workflow.run_stream(alert):
- if hasattr(event, "request_info"):
- # Wait for analyst decision
- decision = await get_analyst_decision()
+ if isinstance(event, RequestInfoEvent):
+ # Workflow is now paused, waiting for analyst decision
+ request_id = event.request_id
+
+ # ... Get analyst decision from UI ...
- # Resume workflow
- await workflow.send_responses({decision})
- async for event in workflow.run_stream(alert):
- # Continues from where it paused
+ # Resume workflow with the response
+ responses = {request_id: analyst_decision}
+ async for event in workflow.send_responses_streaming(responses):
+ # @response_handler processes the decision
process_event(event)
```
diff --git a/agentic_ai/workflow/fraud_detection/QUICKSTART.md b/agentic_ai/workflow/fraud_detection/QUICKSTART.md
index bf998a6bb..213ab2e11 100644
--- a/agentic_ai/workflow/fraud_detection/QUICKSTART.md
+++ b/agentic_ai/workflow/fraud_detection/QUICKSTART.md
@@ -188,16 +188,16 @@ Additional checks:
### Change Analyst Instructions
-**Location**: `create_fraud_detection_workflow()`
+**Location**: `ReviewGatewayExecutor` class in `fraud_detection_workflow.py`
```python
-analyst_review = RequestInfoExecutor(
- name="analyst_review",
- request_info={
- "type": "fraud_analyst_review",
- "instructions": "YOUR CUSTOM INSTRUCTIONS HERE"
- },
-)
+# The AnalystReviewRequest dataclass contains the request details
+@dataclass
+class AnalystReviewRequest:
+ alert_id: str
+ customer_id: int
+ risk_assessment: FraudRiskAssessment
+ instructions: str = "Review the risk assessment and decide on action"
```
### Modify Action Logic
diff --git a/agentic_ai/workflow/fraud_detection/README.md b/agentic_ai/workflow/fraud_detection/README.md
index abadfb4d4..9fa788cb0 100644
--- a/agentic_ai/workflow/fraud_detection/README.md
+++ b/agentic_ai/workflow/fraud_detection/README.md
@@ -37,9 +37,9 @@ This example demonstrates a comprehensive fraud detection system using the Agent
(High Risk β₯0.6) (Low Risk <0.6)
β β
ββββββββββββββββ ββββββββββββββββ
- β Analyst β β Auto Clear β
- β Review β β Executor β
- β (Human Input)β ββββββββ¬ββββββββ
+ β Review β β Auto Clear β
+ β Gateway β β Executor β
+ β(Human Input) β ββββββββ¬ββββββββ
ββββββββ¬ββββββββ β
β β
ββββββββββββββββ β
@@ -80,10 +80,10 @@ This example demonstrates a comprehensive fraud detection system using the Agent
- **Low risk (<0.6)**: Auto-clear
### 5. **Human-in-the-Loop**
-- Uses `RequestInfoExecutor` for analyst review
-- Workflow pauses and creates checkpoint
+- Uses `ctx.request_info()` API for analyst review within `ReviewGatewayExecutor`
+- Workflow pauses and creates checkpoint automatically
- Analyst provides decision (lock account, refund charges, clear, both)
-- Workflow resumes with analyst's decision
+- `@response_handler` decorator processes the response when workflow resumes
### 6. **Checkpointing**
- Workflow state saved at each superstep
@@ -103,8 +103,8 @@ This example demonstrates a comprehensive fraud detection system using the Agent
2. **SuspiciousActivityAlert** β [UsagePattern, Location, Billing] (fan-out)
3. **[UsageAnalysisResult, LocationAnalysisResult, BillingAnalysisResult]** β Aggregator (fan-in)
4. **FraudRiskAssessment** β Switch (risk score check)
-5. **FraudRiskAssessment** β AnalystReview OR AutoClear
-6. **AnalystDecision** β FraudAction (if high risk)
+5. **FraudRiskAssessment** β ReviewGateway OR AutoClear
+6. **AnalystDecision** β FraudAction (if high risk, after human review)
7. **ActionResult** β FinalNotification
8. **FinalNotification** β Workflow output
@@ -260,7 +260,7 @@ instructions=(
1. **Fan-Out Pattern**: One message β multiple executors
2. **Fan-In Pattern**: Multiple messages β one executor (waits for all)
3. **Switch/Case Routing**: Conditional routing based on message content
-4. **Human-in-the-Loop**: Workflow pause/resume with external input
+4. **Human-in-the-Loop**: Workflow pause/resume with `ctx.request_info()` and `@response_handler`
5. **Checkpointing**: Persistent workflow state across restarts
6. **MCP Tool Integration**: Domain-specific tool filtering
7. **LLM Agent Executors**: AI-powered decision making
@@ -390,6 +390,17 @@ http://localhost:3000
- Check WebSocket connection in DevTools
- Verify events are being sent from backend
+**Checkpoint Encoding Error (Windows):**
+
+- If you see `'charmap' codec can't encode character` errors, the backend uses `UTF8FileCheckpointStorage` wrapper to handle Unicode characters in LLM output
+- Ensure you're using the latest backend.py which includes this fix
+
+**Analyst Decision Not Resuming Workflow:**
+
+- Check that the checkpoint was created successfully (no encoding errors in logs)
+- Verify the `request_id` matches between the decision and the pending request
+- Check backend logs for `continue_workflow` messages
+
## License
diff --git a/agentic_ai/workflow/fraud_detection/backend.py b/agentic_ai/workflow/fraud_detection/backend.py
index e15446a07..2f64da70e 100644
--- a/agentic_ai/workflow/fraud_detection/backend.py
+++ b/agentic_ai/workflow/fraud_detection/backend.py
@@ -6,9 +6,33 @@
import asyncio
import logging
+import os
+import sys
from datetime import datetime
+from pathlib import Path
from typing import Any
+from dotenv import load_dotenv
+
+# Load environment first so observability can read connection string
+load_dotenv()
+
+# ------------------------------------------------------------------
+# Observability (must be before any agent imports)
+# ------------------------------------------------------------------
+sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # agentic_ai/
+
+try:
+ from observability import setup_observability
+ _observability_enabled = setup_observability(
+ service_name="contoso-fraud-detection",
+ enable_live_metrics=True,
+ enable_sensitive_data=os.getenv("ENABLE_SENSITIVE_DATA", "false").lower() in ("1", "true", "yes"),
+ )
+except ImportError:
+ _observability_enabled = False
+
+# ------------------------------------------------------------------
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
@@ -28,19 +52,116 @@
WorkflowOutputEvent,
WorkflowStatusEvent,
RequestInfoEvent,
- RequestInfoExecutor,
+ SuperStepCompletedEvent,
+ WorkflowCheckpoint,
+ get_checkpoint_summary,
)
from agent_framework.azure import AzureOpenAIChatClient
from azure.identity import AzureCliCredential
-from dotenv import load_dotenv
-import os
+import json
+from dataclasses import asdict
-# Load environment variables
-load_dotenv()
+
+# ============================================================================
+# UTF-8 Checkpoint Storage Wrapper (fixes Windows encoding issues)
+# ============================================================================
+
+class UTF8FileCheckpointStorage:
+ """
+ Wrapper around FileCheckpointStorage that ensures UTF-8 encoding.
+
+ This fixes the Windows 'charmap' codec error when LLM output contains
+ Unicode characters (like combining diacritical marks) that can't be
+ encoded with the default cp1252 encoding.
+ """
+
+ def __init__(self, storage_path: str | Path):
+ """Initialize the file storage with UTF-8 encoding."""
+ self.storage_path = Path(storage_path)
+ self.storage_path.mkdir(parents=True, exist_ok=True)
+ logger.info(f"Initialized UTF-8 file checkpoint storage at {self.storage_path}")
+
+ async def save_checkpoint(self, checkpoint: WorkflowCheckpoint) -> str:
+ """Save a checkpoint with UTF-8 encoding and return its ID."""
+ file_path = self.storage_path / f"{checkpoint.checkpoint_id}.json"
+ checkpoint_dict = asdict(checkpoint)
+
+ def _write_atomic() -> None:
+ tmp_path = file_path.with_suffix(".json.tmp")
+ with open(tmp_path, "w", encoding="utf-8") as f:
+ json.dump(checkpoint_dict, f, indent=2, ensure_ascii=False)
+ os.replace(tmp_path, file_path)
+
+ await asyncio.to_thread(_write_atomic)
+ logger.info(f"Saved checkpoint {checkpoint.checkpoint_id} to {file_path}")
+ return checkpoint.checkpoint_id
+
+ async def load_checkpoint(self, checkpoint_id: str) -> WorkflowCheckpoint | None:
+ """Load a checkpoint by ID with UTF-8 encoding."""
+ file_path = self.storage_path / f"{checkpoint_id}.json"
+
+ if not file_path.exists():
+ return None
+
+ def _read() -> dict[str, Any]:
+ with open(file_path, encoding="utf-8") as f:
+ return json.load(f)
+
+ checkpoint_dict = await asyncio.to_thread(_read)
+ checkpoint = WorkflowCheckpoint(**checkpoint_dict)
+ logger.info(f"Loaded checkpoint {checkpoint_id} from {file_path}")
+ return checkpoint
+
+ async def list_checkpoint_ids(self, workflow_id: str | None = None) -> list[str]:
+ """List checkpoint IDs with UTF-8 encoding."""
+ def _list_ids() -> list[str]:
+ checkpoint_ids: list[str] = []
+ for file_path in self.storage_path.glob("*.json"):
+ try:
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ if workflow_id is None or data.get("workflow_id") == workflow_id:
+ checkpoint_ids.append(data.get("checkpoint_id", file_path.stem))
+ except Exception as e:
+ logger.warning(f"Failed to read checkpoint file {file_path}: {e}")
+ return checkpoint_ids
+
+ return await asyncio.to_thread(_list_ids)
+
+ async def list_checkpoints(self, workflow_id: str | None = None) -> list[WorkflowCheckpoint]:
+ """List checkpoint objects with UTF-8 encoding."""
+ def _list_checkpoints() -> list[WorkflowCheckpoint]:
+ checkpoints: list[WorkflowCheckpoint] = []
+ for file_path in self.storage_path.glob("*.json"):
+ try:
+ with open(file_path, encoding="utf-8") as f:
+ data = json.load(f)
+ if workflow_id is None or data.get("workflow_id") == workflow_id:
+ checkpoints.append(WorkflowCheckpoint.from_dict(data))
+ except Exception as e:
+ logger.warning(f"Failed to read checkpoint file {file_path}: {e}")
+ return checkpoints
+
+ return await asyncio.to_thread(_list_checkpoints)
+
+ async def delete_checkpoint(self, checkpoint_id: str) -> bool:
+ """Delete a checkpoint by ID."""
+ file_path = self.storage_path / f"{checkpoint_id}.json"
+
+ def _delete() -> bool:
+ if file_path.exists():
+ file_path.unlink()
+ logger.info(f"Deleted checkpoint {checkpoint_id} from {file_path}")
+ return True
+ return False
+
+ return await asyncio.to_thread(_delete)
# Configure logging
logging.basicConfig(level=logging.INFO, force=True)
logger = logging.getLogger(__name__)
+if _observability_enabled:
+ logger.info("β
Application Insights observability enabled for fraud detection workflow")
# Keep agent_framework at INFO level
logging.getLogger("agent_framework").setLevel(logging.INFO)
@@ -130,7 +251,7 @@ class WorkflowStatus(BaseModel):
# Pre-initialized resources (created once on startup)
mcp_tool: MCPStreamableHTTPTool | None = None
chat_client: AzureOpenAIChatClient | None = None
-checkpoint_storage: FileCheckpointStorage | None = None
+checkpoint_storage: UTF8FileCheckpointStorage | None = None
# ============================================================================
@@ -155,14 +276,18 @@ def disconnect(self, websocket: WebSocket):
async def broadcast(self, message: dict):
"""Broadcast message to all connected clients."""
+ msg_type = message.get('type', message.get('event_type', 'unknown'))
+ logger.info(f"[BROADCAST] Sending message type={msg_type} to {len(self.active_connections)} connections")
+
if not self.active_connections:
- logger.warning(f"No active WebSocket connections to broadcast to. Message type: {message.get('type', 'unknown')}")
+ logger.warning(f"No active WebSocket connections to broadcast to. Message type: {msg_type}")
return
disconnected = []
for connection in self.active_connections:
try:
await connection.send_json(message)
+ logger.info(f"[BROADCAST] Successfully sent {msg_type}")
except Exception as e:
logger.error(f"Error sending to WebSocket: {e}")
disconnected.append(connection)
@@ -314,7 +439,9 @@ async def _lookup() -> tuple[str | None, int | None]:
for checkpoint in checkpoints_sorted:
try:
- pending_requests = RequestInfoExecutor.pending_requests_from_checkpoint(checkpoint)
+ # Use the new get_checkpoint_summary API instead of RequestInfoExecutor
+ summary = get_checkpoint_summary(checkpoint)
+ pending_events = summary.pending_request_info_events
except Exception as exc: # pragma: no cover - defensive logging
logger.debug(
"Unable to inspect checkpoint %s for alert %s: %s",
@@ -324,9 +451,9 @@ async def _lookup() -> tuple[str | None, int | None]:
)
continue
- for pending in pending_requests:
+ for pending in pending_events:
if pending.request_id == request_id:
- return checkpoint.checkpoint_id, pending.iteration
+ return checkpoint.checkpoint_id, checkpoint.iteration_count
return None, None
@@ -626,52 +753,78 @@ async def send_progress_updates():
# Small delay to ensure WebSocket connection is fully established
await asyncio.sleep(0.1)
+ # Track if we've seen a RequestInfoEvent (workflow is awaiting decision)
+ seen_request_info = False
+ request_info_event_data = None
+
# Run workflow and stream events
async for event in workflow.run_stream(alert):
+ logger.info(f"[DEBUG] Received event: {type(event).__name__}")
await process_event(alert_id, event)
# Check for human-in-the-loop request
if isinstance(event, RequestInfoEvent):
+ logger.info(f"[DEBUG] RequestInfoEvent detected! request_id={event.request_id}, source={event.source_executor_id}")
request_payload = _serialize_analyst_request(event)
- checkpoint_id, checkpoint_iteration = await _resolve_checkpoint_for_request(
- alert_id,
- event.request_id,
- )
+ logger.info(f"[DEBUG] Serialized request payload keys: {list(request_payload.keys())}")
timestamp = datetime.now().isoformat()
+ # Store the pending decision immediately - checkpoint will be resolved later
pending_decisions[alert_id] = {
**request_payload,
"timestamp": timestamp,
"source_executor_id": event.source_executor_id,
- "checkpoint_id": checkpoint_id,
- "checkpoint_iteration": checkpoint_iteration,
+ "checkpoint_id": None, # Will be resolved after superstep completes
+ "checkpoint_iteration": None,
}
pending_request_events[alert_id] = event
active_workflows[alert_id]["status"] = "awaiting_decision"
- active_workflows[alert_id]["pending_checkpoint_id"] = checkpoint_id
- if checkpoint_id:
- active_workflows[alert_id]["last_checkpoint_id"] = checkpoint_id
- else:
- logger.warning(
- "No checkpoint recorded for alert %s request %s; resume will be unavailable until one is created.",
- alert_id,
- event.request_id,
- )
+ # Broadcast decision_required immediately - don't wait for checkpoint
await manager.broadcast(
{
"type": "decision_required",
"alert_id": alert_id,
"request_id": event.request_id,
"data": request_payload,
- "checkpoint_id": checkpoint_id,
- "checkpoint_iteration": checkpoint_iteration,
+ "checkpoint_id": None, # Will be resolved after superstep completes
+ "checkpoint_iteration": None,
"timestamp": timestamp,
}
)
+ logger.info(f"[DEBUG] Broadcast decision_required for {alert_id}")
logger.info(f"Workflow {alert_id} awaiting analyst decision")
+
+ # Mark that we've seen the request - continue processing to get checkpoint
+ seen_request_info = True
+ request_info_event_data = event
+ # DON'T return here - continue to let the superstep complete and checkpoint be created
+
+ # After a superstep completes following a RequestInfoEvent, resolve the checkpoint
+ elif seen_request_info and isinstance(event, SuperStepCompletedEvent):
+ logger.info(f"[DEBUG] SuperStepCompleted after RequestInfoEvent - resolving checkpoint")
+ checkpoint_id, checkpoint_iteration = await _resolve_checkpoint_for_request(
+ alert_id,
+ request_info_event_data.request_id,
+ )
+ logger.info(f"[DEBUG] Resolved checkpoint: id={checkpoint_id}, iteration={checkpoint_iteration}")
+
+ # Update stored values with resolved checkpoint
+ if checkpoint_id:
+ pending_decisions[alert_id]["checkpoint_id"] = checkpoint_id
+ pending_decisions[alert_id]["checkpoint_iteration"] = checkpoint_iteration
+ active_workflows[alert_id]["pending_checkpoint_id"] = checkpoint_id
+ active_workflows[alert_id]["last_checkpoint_id"] = checkpoint_id
+ else:
+ logger.warning(
+ "No checkpoint recorded for alert %s request %s; resume will be unavailable until one is created.",
+ alert_id,
+ request_info_event_data.request_id,
+ )
+
+ # Now we can exit - workflow is paused awaiting decision
return
# If we exit the loop naturally, mark as completed
@@ -759,44 +912,49 @@ async def continue_workflow(alert_id: str, responses: dict[str, Any], checkpoint
workflow_state.setdefault("workflow_id", workflow.id)
workflow_state["status"] = "running"
- # Debug: Check what's in the checkpoint
+ # Debug: Check what's in the checkpoint using get_checkpoint_summary
checkpoint = await checkpoint_storage.load_checkpoint(effective_checkpoint_id)
if checkpoint:
- logger.info(f"Checkpoint has {len(checkpoint.executor_states)} executor states")
- analyst_state = checkpoint.executor_states.get("analyst_review", {})
- logger.info(f"analyst_review state keys: {list(analyst_state.keys()) if isinstance(analyst_state, dict) else 'not a dict'}")
- if isinstance(analyst_state, dict):
- shared_state_key = RequestInfoExecutor._PENDING_SHARED_STATE_KEY
- pending_requests = checkpoint.shared_state.get(shared_state_key, {})
- logger.info(f"Pending requests in shared state: {list(pending_requests.keys())}")
+ summary = get_checkpoint_summary(checkpoint)
+ logger.info(f"Checkpoint status: {summary.status}")
+ logger.info(f"Pending request events: {len(summary.pending_request_info_events)}")
+ for pending in summary.pending_request_info_events:
+ logger.info(f" - Request ID: {pending.request_id}, Source: {pending.source_executor_id}")
- logger.info(f"Starting run_stream_from_checkpoint with responses keys: {list(responses.keys())}")
+ logger.info(f"Starting workflow resume with responses keys: {list(responses.keys())}")
logger.info(f"Checkpoint ID: {effective_checkpoint_id}")
- # Immediately broadcast analyst_review completion since the framework may not emit it
- # The RequestInfoExecutor was waiting, and when we provide the response, it completes
+ # Immediately broadcast review_gateway completion since the framework may not emit it
+ # The executor using ctx.request_info was waiting, and when we provide the response, it completes
# but this completion event is not always emitted in the event stream during resume
await manager.broadcast({
"alert_id": alert_id,
"type": "ExecutorCompletedEvent",
"event_type": "executor_completed",
- "executor_id": "analyst_review",
+ "executor_id": "review_gateway",
"timestamp": datetime.now().isoformat(),
})
- logger.info("Broadcast analyst_review completion event")
+ logger.info("Broadcast review_gateway completion event")
# Track request info executors that completed during resume
completed_request_executors = set()
- async for event in workflow.run_stream_from_checkpoint(
- effective_checkpoint_id,
+ # First, restore from checkpoint
+ logger.info(f"Restoring workflow from checkpoint {effective_checkpoint_id}")
+ async for event in workflow.run_stream(
+ checkpoint_id=effective_checkpoint_id,
checkpoint_storage=checkpoint_storage,
- responses=responses,
):
+ logger.info(f"Restore event received: {type(event).__name__}")
+ await process_event(alert_id, event)
+
+ # Now send the responses to continue the workflow
+ logger.info(f"Sending responses to continue workflow: {list(responses.keys())}")
+ async for event in workflow.send_responses_streaming(responses):
logger.info(f"Event received: {type(event).__name__}")
- # Track when analyst_review completes so we can re-broadcast at the end
- if isinstance(event, ExecutorCompletedEvent) and event.executor_id == "analyst_review":
+ # Track when review_gateway completes so we can re-broadcast at the end
+ if isinstance(event, ExecutorCompletedEvent) and event.executor_id == "review_gateway":
completed_request_executors.add(event.executor_id)
await process_event(alert_id, event)
@@ -979,11 +1137,11 @@ async def startup_event():
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential(), deployment_name=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT"))
logger.info("β Azure OpenAI client initialized")
- # Initialize checkpoint storage
+ # Initialize checkpoint storage with UTF-8 encoding wrapper
import pathlib
checkpoint_dir = pathlib.Path("./checkpoints")
checkpoint_dir.mkdir(parents=True, exist_ok=True)
- checkpoint_storage = FileCheckpointStorage(str(checkpoint_dir))
+ checkpoint_storage = UTF8FileCheckpointStorage(str(checkpoint_dir))
logger.info(f"β Checkpoint storage initialized at {checkpoint_dir.absolute()}")
logger.info("Backend ready! π")
diff --git a/agentic_ai/workflow/fraud_detection/fraud_detection_workflow.py b/agentic_ai/workflow/fraud_detection/fraud_detection_workflow.py
index 8b6b8b0b1..da36abf1e 100644
--- a/agentic_ai/workflow/fraud_detection/fraud_detection_workflow.py
+++ b/agentic_ai/workflow/fraud_detection/fraud_detection_workflow.py
@@ -16,12 +16,13 @@
3. Fan-In to FraudRiskAggregatorExecutor (LLM-based agent):
- Produces FraudRiskScore and recommended action (lock account, refund charges, ignore)
4. SwitchCaseEdgeRunner:
- - If risk score β₯ threshold β route to RequestInfoExecutor for human fraud analyst review
+ - If risk score β₯ threshold β route to ReviewGatewayExecutor for human fraud analyst review
- Else β route to AutoClearExecutor
-5. RequestInfoExecutor β sends "Fraud Case Review Request" to analyst with full context
-6. Workflow pauses β checkpoint saved
+5. ReviewGatewayExecutor β calls ctx.request_info() to request analyst decision, workflow pauses
+6. Checkpoint saved β workflow awaits analyst response
7. Analyst decides (approve lock/refund or clear)
8. Workflow resumes:
+ - ReviewGatewayExecutor's @response_handler processes analyst decision
- FraudActionExecutor β performs chosen action (e.g., lock account, reverse charges)
9. FinalNotificationExecutor β informs customer and logs audit trail
@@ -30,7 +31,7 @@
- Fan-out pattern to multiple specialist agents with MCP tools
- Fan-in aggregation to produce single risk assessment
- LLM-based risk scoring
-- Human-in-the-loop for high-risk cases
+- Human-in-the-loop via ctx.request_info() and @response_handler
- Checkpointing for workflow pause/resume
"""
@@ -54,14 +55,12 @@
FileCheckpointStorage,
MCPStreamableHTTPTool,
RequestInfoEvent,
- RequestInfoExecutor,
- RequestInfoMessage,
- RequestResponse,
WorkflowBuilder,
WorkflowContext,
WorkflowOutputEvent,
WorkflowStatusEvent,
handler,
+ response_handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from pydantic import BaseModel, Field
@@ -141,8 +140,8 @@ class FraudRiskAssessment:
@dataclass
-class AnalystReviewRequest(RequestInfoMessage):
- """Request for analyst review sent to RequestInfoExecutor."""
+class AnalystReviewRequest:
+ """Request for analyst review - used with ctx.request_info()."""
assessment: FraudRiskAssessment | None = None
prompt: str = ""
@@ -678,44 +677,64 @@ async def handle_analysis_results(
class ReviewGatewayExecutor(Executor):
- """Gateway that routes high-risk assessments to RequestInfoExecutor for human review."""
+ """
+ Gateway that handles high-risk assessments with human-in-the-loop review.
+
+ Uses the new request_info API:
+ 1. Receives high-risk assessment
+ 2. Calls ctx.request_info() to pause workflow and request analyst input
+ 3. @response_handler receives the analyst's decision
+ 4. Forwards decision to FraudActionExecutor
+ """
- def __init__(self, analyst_review_id: str, fraud_action_id: str, id: str = "review_gateway") -> None:
+ def __init__(self, fraud_action_id: str, id: str = "review_gateway") -> None:
super().__init__(id=id)
- self._analyst_review_id = analyst_review_id
self._fraud_action_id = fraud_action_id
+ # Store the assessment for use in the response handler
+ self._pending_assessment: FraudRiskAssessment | None = None
@handler
async def handle_assessment(
- self, assessment: FraudRiskAssessment, ctx: WorkflowContext[AnalystReviewRequest]
+ self, assessment: FraudRiskAssessment, ctx: WorkflowContext[AnalystDecision]
) -> None:
logger.info(f"[ReviewGateway] Routing high-risk assessment {assessment.alert_id} to analyst")
+ # Store assessment for response handler
+ self._pending_assessment = assessment
+
# Create analyst review request
request = AnalystReviewRequest(
assessment=assessment,
prompt=f"Review fraud case for alert {assessment.alert_id}. Risk score: {assessment.overall_risk_score:.2f}. Recommended action: {assessment.recommended_action}",
)
- # Send to RequestInfoExecutor
- await ctx.send_message(request, target_id=self._analyst_review_id)
+ # Request info from external analyst - workflow will pause here
+ # The response will be handled by the @response_handler below
+ await ctx.request_info(request, AnalystDecision)
+ logger.info(f"[ReviewGateway] Waiting for analyst decision on {assessment.alert_id}")
- @handler
+ @response_handler
async def handle_analyst_response(
- self, response: RequestResponse[AnalystReviewRequest, AnalystDecision], ctx: WorkflowContext[AnalystDecision]
+ self,
+ original_request: AnalystReviewRequest,
+ response: AnalystDecision,
+ ctx: WorkflowContext[AnalystDecision],
) -> None:
- logger.info(f"[ReviewGateway] Received analyst decision")
-
- assessment = response.original_request.assessment if response.original_request else None
- decision = response.data
+ """Handle the analyst's decision and forward to fraud action executor."""
+ logger.info(f"[ReviewGateway] Received analyst decision: {response.approved_action}")
- if assessment and getattr(decision, "customer_id", None) in (None, 0):
- # Now using dataclasses, use replace() to update fields
+ # Ensure decision has customer_id from original assessment
+ if original_request.assessment and getattr(response, "customer_id", None) in (None, 0):
from dataclasses import replace
- decision = replace(decision, customer_id=assessment.customer_id)
+ response = replace(
+ response,
+ customer_id=original_request.assessment.customer_id,
+ alert_id=original_request.assessment.alert_id,
+ )
# Forward the analyst decision to fraud action executor
- await ctx.send_message(decision, target_id=self._fraud_action_id)
+ await ctx.send_message(response, target_id=self._fraud_action_id)
+ logger.info(f"[ReviewGateway] Forwarded decision to fraud action executor")
class AutoClearExecutor(Executor):
@@ -822,7 +841,7 @@ async def create_fraud_detection_workflow(
"""
Build the fraud detection workflow.
- Topology:
+ Topology (updated for new request_info API):
AlertRouter β [UsagePattern, Location, Billing] β FraudRiskAggregator
β
(Switch based on risk score)
@@ -830,10 +849,14 @@ async def create_fraud_detection_workflow(
βββββββββββββββββββββββββ΄βββββββββββββββββββββββ
β β
(High Risk) (Low Risk)
- RequestInfoExecutor AutoClearExecutor
- β β
- (Analyst Decision) β
+ ReviewGateway AutoClearExecutor
+ (uses ctx.request_info β
+ for human-in-the-loop) β
+ β β
FraudActionExecutor βββββββββββββββββββββ FinalNotificationExecutor
+
+ Note: ReviewGateway now uses ctx.request_info() and @response_handler
+ instead of the deprecated RequestInfoExecutor.
"""
# Create executors
@@ -846,10 +869,8 @@ async def create_fraud_detection_workflow(
fraud_action = FraudActionExecutor()
final_notification = FinalNotificationExecutor()
- # Create human-in-the-loop executors
- analyst_review = RequestInfoExecutor(id="analyst_review")
+ # Create human-in-the-loop executor (now uses ctx.request_info internally)
review_gateway = ReviewGatewayExecutor(
- analyst_review_id=analyst_review.id,
fraud_action_id=fraud_action.id,
)
@@ -862,23 +883,21 @@ async def create_fraud_detection_workflow(
# Fan-in edge: 3 analysts β Aggregator (waits for all 3)
builder.add_fan_in_edges([usage_executor, location_executor, billing_executor], aggregator)
- # # Switch/case edges: Aggregator β High risk OR Low risk
+ # Switch/case edges: Aggregator β High risk OR Low risk
builder.add_switch_case_edge_group(
aggregator,
[
- # High risk β Review Gateway β Analyst review
+ # High risk β Review Gateway (will request human input via ctx.request_info)
Case(condition=lambda assessment: assessment.overall_risk_score >= 0.6, target=review_gateway),
# Low risk β Auto clear
Default(target=auto_clear),
],
)
- # # Review gateway routes to analyst review and back, then to fraud action
- builder.add_edge(review_gateway, analyst_review)
- builder.add_edge(analyst_review, review_gateway)
+ # Review gateway β Fraud action (after analyst decision via @response_handler)
builder.add_edge(review_gateway, fraud_action)
- # # Both paths β Final notification
+ # Both paths β Final notification
builder.add_edge(auto_clear, final_notification)
builder.add_edge(fraud_action, final_notification)
diff --git a/agentic_ai/workflow/fraud_detection/pyproject.toml b/agentic_ai/workflow/fraud_detection/pyproject.toml
index 5c297b7c7..c91139aa2 100644
--- a/agentic_ai/workflow/fraud_detection/pyproject.toml
+++ b/agentic_ai/workflow/fraud_detection/pyproject.toml
@@ -6,7 +6,8 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi==0.115.12",
- "agent-framework==1.0.0b251007",
+ "agent-framework==1.0.0b260130",
+ "azure-monitor-opentelemetry>=1.8.5",
"fastmcp==2.7.1",
"flasgger==0.9.7.1",
"flask==3.0.3",
@@ -15,7 +16,6 @@ dependencies = [
"openai>=1.90.0,<1.110.0",
"pydantic==2.11.4",
"requests==2.32.4",
- "streamlit==1.45.0",
"tenacity==8.5.0",
"uvicorn>=0.25.0",
"websockets>=15.0.1",
diff --git a/agentic_ai/workflow/fraud_detection/scenario.md b/agentic_ai/workflow/fraud_detection/scenario.md
index c7182eb79..70abc4f6b 100644
--- a/agentic_ai/workflow/fraud_detection/scenario.md
+++ b/agentic_ai/workflow/fraud_detection/scenario.md
@@ -13,10 +13,10 @@ Contosoβs automated systems flag suspicious account activity, but certain case
3. **Fan-In** to `FraudRiskAggregatorExecutor`:
- Produces `FraudRiskScore` and recommended action (lock account, refund charges, ignore).
4. **SwitchCaseEdgeRunner**:
- - If risk score β₯ threshold β route to `RequestInfoExecutor` for human fraud analyst review.
+ - If risk score β₯ threshold β route to `ReviewGatewayExecutor` for human fraud analyst review.
- Else β route to `AutoClearExecutor`.
-5. **RequestInfoExecutor** β sends βFraud Case Review Requestβ to analyst with full context.
-6. **Workflow pauses** β checkpoint saved.
+5. **ReviewGatewayExecutor** β uses `ctx.request_info()` to request analyst review with full context.
+6. **Workflow pauses** β checkpoint saved automatically.
7. **Analyst decides** (approve lock/refund or clear).
8. Workflow resumes:
- `FraudActionExecutor` β performs chosen action (e.g., lock account, reverse charges).
diff --git a/agentic_ai/workflow/fraud_detection/ui/src/App.jsx b/agentic_ai/workflow/fraud_detection/ui/src/App.jsx
index 9cf0a9b66..67f692064 100644
--- a/agentic_ai/workflow/fraud_detection/ui/src/App.jsx
+++ b/agentic_ai/workflow/fraud_detection/ui/src/App.jsx
@@ -1,11 +1,13 @@
-import { useState, useCallback, useEffect } from 'react';
+import React, { useState, useCallback, useEffect } from 'react';
import {
Box,
ThemeProvider,
+ createTheme,
CssBaseline,
AppBar,
Toolbar,
Typography,
+ Container,
Paper,
Grid,
} from '@mui/material';
@@ -15,18 +17,32 @@ import ControlPanel from './components/ControlPanel';
import AnalystDecisionPanel from './components/AnalystDecisionPanel';
import EventLog from './components/EventLog';
import { useWebSocket } from './hooks/useWebSocket';
-import { fetchAlerts, startWorkflow, submitDecision } from './utils/api';
-import { isDuplicateEvent } from './utils/helpers';
-import { EVENT_TYPES } from './constants/workflow';
-import { API_CONFIG, APP_CONFIG } from './constants/config';
-import theme from './theme';
-
-/**
- * Main application component for the Fraud Detection Workflow Visualizer
- * Manages the state and orchestration of workflow visualization, controls, and event logging
- */
+
+const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ success: {
+ main: '#4caf50',
+ },
+ warning: {
+ main: '#ff9800',
+ },
+ error: {
+ main: '#f44336',
+ },
+ },
+ typography: {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ },
+});
+
function App() {
- // State management
const [alerts, setAlerts] = useState([]);
const [selectedAlert, setSelectedAlert] = useState(null);
const [workflowRunning, setWorkflowRunning] = useState(false);
@@ -34,57 +50,50 @@ function App() {
const [pendingDecision, setPendingDecision] = useState(null);
const [executorStates, setExecutorStates] = useState({});
- // WebSocket connection for real-time updates
- const { lastMessage, sendMessage } = useWebSocket(API_CONFIG.WS_URL);
+ // WebSocket hook for real-time updates
+ const { lastMessage, sendMessage } = useWebSocket('ws://localhost:8001/ws');
- /**
- * Load sample alerts on component mount
- */
+ // Load sample alerts on mount
useEffect(() => {
- const loadAlerts = async () => {
- try {
- const alertsData = await fetchAlerts();
- setAlerts(alertsData);
- } catch (error) {
- console.error('Failed to load alerts:', error);
- }
- };
-
- loadAlerts();
+ fetch('/api/alerts')
+ .then((res) => res.json())
+ .then((data) => setAlerts(data.alerts))
+ .catch((err) => console.error('Error loading alerts:', err));
}, []);
- /**
- * Handle incoming WebSocket messages
- * Process different event types and update application state accordingly
- */
+ // Handle WebSocket messages
useEffect(() => {
if (!lastMessage) return;
try {
const event = lastMessage;
- // Add to event log - prevent duplicates
+ // Add to event log - prevent duplicates by checking timestamp + type + executor_id
setEvents((prev) => {
- return isDuplicateEvent(event, prev) ? prev : [...prev, event];
+ const eventKey = `${event.timestamp}-${event.type || event.event_type}-${event.executor_id || ''}`;
+ const isDuplicate = prev.some(
+ (e) => `${e.timestamp}-${e.type || e.event_type}-${e.executor_id || ''}` === eventKey
+ );
+ return isDuplicate ? prev : [...prev, event];
});
// Handle workflow initialization
- if (event.type === EVENT_TYPES.WORKFLOW_INITIALIZING) {
+ if (event.type === 'workflow_initializing') {
// Keep workflow running flag true, just show initialization message
}
// Handle workflow started
- if (event.type === EVENT_TYPES.WORKFLOW_STARTED) {
+ if (event.type === 'workflow_started') {
// Workflow is now running
}
// Update executor states based on event type
- if (event.event_type === EVENT_TYPES.EXECUTOR_INVOKED) {
+ if (event.event_type === 'executor_invoked') {
setExecutorStates((prev) => ({
...prev,
[event.executor_id]: 'running',
}));
- } else if (event.event_type === EVENT_TYPES.EXECUTOR_COMPLETED) {
+ } else if (event.event_type === 'executor_completed') {
setExecutorStates((prev) => ({
...prev,
[event.executor_id]: 'completed',
@@ -92,13 +101,13 @@ function App() {
}
// Handle decision required
- if (event.type === EVENT_TYPES.DECISION_REQUIRED) {
+ if (event.type === 'decision_required') {
setPendingDecision(event);
setWorkflowRunning(false);
}
// Handle workflow completion
- if (event.type === EVENT_TYPES.WORKFLOW_COMPLETED || event.type === EVENT_TYPES.WORKFLOW_ERROR) {
+ if (event.type === 'workflow_completed' || event.type === 'workflow_error') {
setWorkflowRunning(false);
// Keep all executor states as-is (they should already be 'completed')
}
@@ -107,10 +116,6 @@ function App() {
}
}, [lastMessage]);
- /**
- * Start a workflow for the selected alert
- * @param {Object} alert - The alert object to process
- */
const handleStartWorkflow = useCallback(async (alert) => {
console.log('Starting workflow for alert:', alert);
setSelectedAlert(alert);
@@ -120,7 +125,15 @@ function App() {
setPendingDecision(null);
try {
- const data = await startWorkflow(alert);
+ const response = await fetch('/api/workflow/start', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(alert),
+ });
+
+ const data = await response.json();
console.log('Workflow started:', data);
} catch (error) {
console.error('Error starting workflow:', error);
@@ -128,15 +141,19 @@ function App() {
}
}, []);
- /**
- * Submit analyst decision and resume workflow
- * @param {Object} decision - The decision object containing analyst's input
- */
const handleSubmitDecision = useCallback(async (decision) => {
console.log('Submitting decision:', decision);
try {
- const data = await submitDecision(decision);
+ const response = await fetch('/api/workflow/decision', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(decision),
+ });
+
+ const data = await response.json();
console.log('Decision submitted:', data);
setPendingDecision(null);
@@ -149,13 +166,13 @@ function App() {
return (
-
+
{/* App Bar */}
- {APP_CONFIG.TITLE}
+ Fraud Detection Workflow Visualizer
Real-time Multi-Agent Workflow Monitoring
@@ -164,29 +181,27 @@ function App() {
{/* Main Content */}
-
+
{/* Left Column - Controls and Decision Panel */}
-
-
-
+
+
+ {pendingDecision && (
+
-
- {pendingDecision && (
-
- )}
-
+ )}
{/* Center Column - Workflow Visualization */}
-
+
Workflow Graph
@@ -203,14 +218,14 @@ function App() {
{/* Right Column - Event Log */}
-
+
-
+
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/agentic_ai/workflow/fraud_detection/ui/src/components/AnalystDecisionPanel.jsx b/agentic_ai/workflow/fraud_detection/ui/src/components/AnalystDecisionPanel.jsx
index c633fd731..d2cea18f6 100644
--- a/agentic_ai/workflow/fraud_detection/ui/src/components/AnalystDecisionPanel.jsx
+++ b/agentic_ai/workflow/fraud_detection/ui/src/components/AnalystDecisionPanel.jsx
@@ -45,13 +45,15 @@ function AnalystDecisionPanel({ decision, onSubmit }) {
AI Analysis
-
+
- {decision.data.reasoning}
+ {decision.data.reasoning.length > 500
+ ? decision.data.reasoning.substring(0, 500) + '...'
+ : decision.data.reasoning}
diff --git a/agentic_ai/workflow/fraud_detection/ui/src/components/WorkflowVisualizer.jsx b/agentic_ai/workflow/fraud_detection/ui/src/components/WorkflowVisualizer.jsx
index c7b361301..1852c058f 100644
--- a/agentic_ai/workflow/fraud_detection/ui/src/components/WorkflowVisualizer.jsx
+++ b/agentic_ai/workflow/fraud_detection/ui/src/components/WorkflowVisualizer.jsx
@@ -51,7 +51,7 @@ const initialNodes = [
id: 'review_gateway',
type: 'custom',
position: { x: 550, y: 500 },
- data: { label: 'Review Gateway', status: 'idle', description: 'Routes to analyst review' },
+ data: { label: 'Review Gateway', status: 'idle', description: 'Human analyst review (pauses workflow)' },
},
{
id: 'auto_clear_executor',
@@ -59,22 +59,16 @@ const initialNodes = [
position: { x: 250, y: 500 },
data: { label: 'Auto Clear', status: 'idle', description: 'Auto-clears low risk' },
},
- {
- id: 'analyst_review',
- type: 'custom',
- position: { x: 550, y: 650 },
- data: { label: 'Analyst Review', status: 'idle', description: 'Human review required' },
- },
{
id: 'fraud_action_executor',
type: 'custom',
- position: { x: 550, y: 800 },
+ position: { x: 550, y: 650 },
data: { label: 'Fraud Action', status: 'idle', description: 'Execute fraud action' },
},
{
id: 'final_notification_executor',
type: 'custom',
- position: { x: 400, y: 950 },
+ position: { x: 400, y: 800 },
data: { label: 'Final Notification', status: 'idle', description: 'Send notifications' },
},
];
@@ -94,10 +88,8 @@ const initialEdges = [
{ id: 'e3-1', source: 'fraud_risk_aggregator', target: 'review_gateway', label: 'High Risk', style: { stroke: '#f44336' } },
{ id: 'e3-2', source: 'fraud_risk_aggregator', target: 'auto_clear_executor', label: 'Low Risk', style: { stroke: '#4caf50' } },
- // Review loop
- { id: 'e4-1', source: 'review_gateway', target: 'analyst_review' },
- { id: 'e4-2', source: 'analyst_review', target: 'review_gateway', animated: true, style: { stroke: '#ff9800' } },
- { id: 'e4-3', source: 'review_gateway', target: 'fraud_action_executor' },
+ // Review gateway to fraud action (human review happens via request_info, then proceeds)
+ { id: 'e4-1', source: 'review_gateway', target: 'fraud_action_executor', animated: true, style: { stroke: '#ff9800' } },
// Final paths
{ id: 'e5-1', source: 'auto_clear_executor', target: 'final_notification_executor' },
diff --git a/agentic_ai/workflow/fraud_detection/ui/src/hooks/useWebSocket.js b/agentic_ai/workflow/fraud_detection/ui/src/hooks/useWebSocket.js
index 531588e32..dd9a4f13d 100644
--- a/agentic_ai/workflow/fraud_detection/ui/src/hooks/useWebSocket.js
+++ b/agentic_ai/workflow/fraud_detection/ui/src/hooks/useWebSocket.js
@@ -3,6 +3,7 @@ import { WS_CONFIG } from '../constants/config';
/**
* Custom hook for managing WebSocket connections with automatic reconnection
+ * Uses a message queue to ensure no messages are lost when they arrive quickly
* @param {string} url - WebSocket URL to connect to
* @returns {Object} Object containing lastMessage, readyState, and sendMessage function
*/
@@ -12,6 +13,29 @@ export function useWebSocket(url) {
const ws = useRef(null);
const reconnectTimeout = useRef(null);
const reconnectAttempts = useRef(0);
+
+ // Message queue to prevent lost messages during rapid updates
+ const messageQueue = useRef([]);
+ const processingQueue = useRef(false);
+
+ // Process messages from queue one at a time
+ const processQueue = useCallback(() => {
+ if (processingQueue.current || messageQueue.current.length === 0) {
+ return;
+ }
+
+ processingQueue.current = true;
+ const message = messageQueue.current.shift();
+
+ // Use setTimeout to ensure React processes each state update
+ setLastMessage(message);
+
+ // Schedule next message processing
+ setTimeout(() => {
+ processingQueue.current = false;
+ processQueue();
+ }, 0);
+ }, []);
const connect = useCallback(() => {
try {
@@ -26,7 +50,9 @@ export function useWebSocket(url) {
ws.current.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
- setLastMessage(data);
+ // Add to queue instead of directly setting state
+ messageQueue.current.push(data);
+ processQueue();
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
@@ -56,7 +82,7 @@ export function useWebSocket(url) {
} catch (error) {
console.error('Error creating WebSocket:', error);
}
- }, [url]);
+ }, [url, processQueue]);
useEffect(() => {
connect();
diff --git a/agentic_ai/workflow/fraud_detection/uv.lock b/agentic_ai/workflow/fraud_detection/uv.lock
index 306cb8415..0576fe255 100644
--- a/agentic_ai/workflow/fraud_detection/uv.lock
+++ b/agentic_ai/workflow/fraud_detection/uv.lock
@@ -25,22 +25,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/23/bf86f1d3a04a6d967176e20939a98207f8a8ed49480622ad6cf5ce232083/a2a_sdk-0.3.8-py3-none-any.whl", hash = "sha256:21254dd47d89a958b9d15576a69fe3d44aaef558858d148e187d1f1e26b320e7", size = 138098 },
]
+[[package]]
+name = "ag-ui-protocol"
+version = "0.1.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 },
+]
+
[[package]]
name = "agent-framework"
-version = "1.0.0b251007"
+version = "1.0.0b260130"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "agent-framework-a2a" },
- { name = "agent-framework-azure-ai" },
- { name = "agent-framework-copilotstudio" },
- { name = "agent-framework-core" },
- { name = "agent-framework-devui" },
- { name = "agent-framework-mem0" },
- { name = "agent-framework-redis" },
+ { name = "agent-framework-core", extra = ["all"] },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6b/a6/d21b9666b738b7398ab1b58c31fcd17f0b4da04e822f261b097058451ee5/agent_framework-1.0.0b251007.tar.gz", hash = "sha256:c17e1286471d22f60304a36a03e38f87bf7a720dee6fce5c35bc738eb5227abc", size = 1689664 }
+sdist = { url = "https://files.pythonhosted.org/packages/93/10/ba51bf04ea2900897a221664e4e673dcc7a7a58a6658eeb85115e920d9b4/agent_framework-1.0.0b260130.tar.gz", hash = "sha256:50e13b74366b8092cb81769f07b3b42d6ddc8888a51244933c3214df591b7108", size = 3506765 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/aa/7c4b12e59040d2cb7c4d19d34ceb0de2a126c9850d954d08cc02acb760d4/agent_framework-1.0.0b251007-py3-none-any.whl", hash = "sha256:c582b2f7d1659cc5c543c6a6e90cfd05c87547b0dadbce38cd46aa75f883474f", size = 5551 },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/2a8efa9085c7fec503a64038f986faf0cdf7f5de853c4ae30724e2e2bda6/agent_framework-1.0.0b260130-py3-none-any.whl", hash = "sha256:b9ba1487f91ab22031e01b5c09e5649181fd717f807d94f22ec43a409c43cde1", size = 5552 },
]
[[package]]
@@ -56,6 +62,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/af/87/09806e5b4a3c95ba7aca5be9bda853b518a174754530343f909f53beca76/agent_framework_a2a-1.0.0b251007-py3-none-any.whl", hash = "sha256:b26b9a43056783dd8b11926d4dd005a008e3a52ed885d9e9303f862542f019d1", size = 6766 },
]
+[[package]]
+name = "agent-framework-ag-ui"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ag-ui-protocol" },
+ { name = "agent-framework-core" },
+ { name = "fastapi" },
+ { name = "uvicorn" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/2f/ae316dec3d27b484d5e11dd6469d5dee660416b38d51179a8712d987617c/agent_framework_ag_ui-1.0.0b260130.tar.gz", hash = "sha256:0ebf489fe43050b6e63f3188be13449389735e3e82905c3479c9fba8e73568a9", size = 93211 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/5e/ccfe94370e26928bea78e5fd89efbe4254e2263e1718b49f38a24108abf9/agent_framework_ag_ui-1.0.0b260130-py3-none-any.whl", hash = "sha256:68cab476436a6bf7d3b1ac6341b3debdf45cd516e311522b8b84ebfafa0f4be5", size = 67897 },
+]
+
+[[package]]
+name = "agent-framework-anthropic"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "anthropic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/50/523d45d86768301cfd853f0c54d5dcbc1df81b50d9e6a89a8acfa2f533f7/agent_framework_anthropic-1.0.0b260130.tar.gz", hash = "sha256:d8ac99cdc9e82f91e8a8f749965523b49e2341c51ce8dc1dd9a1c7c2df567a4e", size = 12252 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/4a/da59aa7ca5b3441ab74807ed3861ec4f6ae9f301421c50944859e111a169/agent_framework_anthropic-1.0.0b260130-py3-none-any.whl", hash = "sha256:7272dd56a09c6d3e33652c5031b01224d5333fee176f2129382fcc6729714261", size = 12316 },
+]
+
[[package]]
name = "agent-framework-azure-ai"
version = "1.0.0b251007"
@@ -71,6 +105,47 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/11/2ea07e06c695371cdc2fdc8f4bf9117f1b4e19f3a128d4da07e522afc8ed/agent_framework_azure_ai-1.0.0b251007-py3-none-any.whl", hash = "sha256:ce15f01ad04d534e42f6a678472d44a3814406e24d96b2770f857170019e0cc4", size = 12810 },
]
+[[package]]
+name = "agent-framework-azure-ai-search"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "azure-search-documents" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/63/81c7853aa526f3c3667871cea14667af73323c6c53d31c34be34926a9de4/agent_framework_azure_ai_search-1.0.0b260130.tar.gz", hash = "sha256:0a622fdddd7dc0287de693f2aa6f770ec52ea8d1eaca817c4276daa08001c10b", size = 13312 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/ec/ac8143dbb1af2ec510f7772d712803193a6a0ad5f36b06e7ec7121df5c80/agent_framework_azure_ai_search-1.0.0b260130-py3-none-any.whl", hash = "sha256:0278c948696d7a00193a0271074c6057b57589ff98eda5544f2eafeac051d6e9", size = 13449 },
+]
+
+[[package]]
+name = "agent-framework-azurefunctions"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "agent-framework-durabletask" },
+ { name = "azure-functions" },
+ { name = "azure-functions-durable" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/0e/59c4c45c380b4d0dcfb71be45ec60a8d52b271979b5cf9e5be1f9e974653/agent_framework_azurefunctions-1.0.0b260130.tar.gz", hash = "sha256:b6a971036c7088a61e5079549f11e0c7972b955452bdb6d576769ed8da27b920", size = 16340 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/fa/200b40db670f79f561ff1e69e9626729ceb6486af970e3489f6c3a295d76/agent_framework_azurefunctions-1.0.0b260130-py3-none-any.whl", hash = "sha256:7d529a0bad67caa38d8823462c439e97de5e1cf364c0e9a0895df5fb44996f64", size = 17788 },
+]
+
+[[package]]
+name = "agent-framework-chatkit"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "openai-chatkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/be/9e/3f2d6440ad2a16308c26d894995895d131225c5284328190b1c5ae7f769a/agent_framework_chatkit-1.0.0b260130.tar.gz", hash = "sha256:e5953337a5d8dd7930c2692ba1b23cf771002a9797b5c2306e3f3b256db200cc", size = 12415 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/f1/68496e52aa36e66cf2962b8a8c6937053e2e57ad5f135b6983d705172554/agent_framework_chatkit-1.0.0b260130-py3-none-any.whl", hash = "sha256:a7814a5b222de7a0ac57fb89f4a6e534521c7e58bdc86a6465885fb9d57e63f1", size = 11712 },
+]
+
[[package]]
name = "agent-framework-copilotstudio"
version = "1.0.0b251007"
@@ -86,26 +161,58 @@ wheels = [
[[package]]
name = "agent-framework-core"
-version = "1.0.0b251007"
+version = "1.0.0b260130"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "aiofiles" },
{ name = "azure-identity" },
- { name = "azure-monitor-opentelemetry" },
- { name = "azure-monitor-opentelemetry-exporter" },
{ name = "mcp", extra = ["ws"] },
{ name = "openai" },
{ name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-sdk" },
{ name = "opentelemetry-semantic-conventions-ai" },
+ { name = "packaging" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0f/ba/c4d706aa716a37f1f3400e7489ad464c3be86f94cd062d130d8ae3671121/agent_framework_core-1.0.0b251007.tar.gz", hash = "sha256:56ac1705b43e0ebe49ab7ec890625db3c619af63f455b87e41c631449e8b5de3", size = 384807 }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/39/e508e778219bd6d20e023a6f48235861a639e3cf888776f9e873bbad3c6b/agent_framework_core-1.0.0b260130.tar.gz", hash = "sha256:030a5b2ced796eec6839c2dabad90b4bd1ea33d1026f3ed1813050a56ccfa4ec", size = 301823 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/36/68/afe66c72951a279e0fe048fd5af1e775528cde40dbdab8ec03b42c545df4/agent_framework_core-1.0.0b260130-py3-none-any.whl", hash = "sha256:75b4dd0ca2ae52574d406cf5c9ed7adf63e187379f72fce891743254d83dfd56", size = 348724 },
+]
+
+[package.optional-dependencies]
+all = [
+ { name = "agent-framework-a2a" },
+ { name = "agent-framework-ag-ui" },
+ { name = "agent-framework-anthropic" },
+ { name = "agent-framework-azure-ai" },
+ { name = "agent-framework-azure-ai-search" },
+ { name = "agent-framework-azurefunctions" },
+ { name = "agent-framework-chatkit" },
+ { name = "agent-framework-copilotstudio" },
+ { name = "agent-framework-declarative" },
+ { name = "agent-framework-devui" },
+ { name = "agent-framework-durabletask" },
+ { name = "agent-framework-github-copilot" },
+ { name = "agent-framework-lab" },
+ { name = "agent-framework-mem0" },
+ { name = "agent-framework-ollama" },
+ { name = "agent-framework-purview" },
+ { name = "agent-framework-redis" },
+]
+
+[[package]]
+name = "agent-framework-declarative"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "powerfx", marker = "python_full_version < '3.14'" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/a4/7821524773b40366db789ba620e23e086b3d205cf0e21f2a94b19026b4a3/agent_framework_declarative-1.0.0b260130.tar.gz", hash = "sha256:30171a7cdd4f140cc66f17084b2fa5296a45d6b5ce59a2deed95b008a672c98d", size = 78227 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/34/fc/e122b9f7e63b0e3ec425edc22aaff4649ef7dec5214fc93309a1331b7d37/agent_framework_core-1.0.0b251007-py3-none-any.whl", hash = "sha256:5042c37fa53370089fb7db07d7be9f1dfa4ea3491754878b04504e07cc9993c3", size = 261210 },
+ { url = "https://files.pythonhosted.org/packages/da/1c/e85fb11e3e1922e6442073e1ac7a0042a04d6f645393227c2b498575d187/agent_framework_declarative-1.0.0b260130-py3-none-any.whl", hash = "sha256:9ccfa1ed846c2e414ace1f9320e6e7fbbddf3ea9dafdeed138e2bfcb481c2bef", size = 89331 },
]
[[package]]
@@ -123,6 +230,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/b6/ab40dc4f768104bfaa43523e003c88aded8f79428f6c73a22f9547ddc752/agent_framework_devui-1.0.0b251007-py3-none-any.whl", hash = "sha256:350952d2c6442702b0281dfb4736cf9d94e1a7ea30de7f1c4419ad9408b42b2e", size = 268535 },
]
+[[package]]
+name = "agent-framework-durabletask"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "durabletask" },
+ { name = "durabletask-azuremanaged" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/95/9d5ee7fd1fdcd52c10aa1b2902964701d1d62b9d35cc7d05115b90db6329/agent_framework_durabletask-1.0.0b260130.tar.gz", hash = "sha256:63a2c8e0968a51d8e132892e9d385d2b82ccb95263d2c0316dc46b0eaa4dd7a4", size = 30285 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/22/122ed515935926137cc3c6ca795ef01b30feb82160cfc0f29a34f9d603de/agent_framework_durabletask-1.0.0b260130-py3-none-any.whl", hash = "sha256:a46e292800d10a62ce0923efe753594ddbf0bd6d1bb6e1258380f0dbf7d0302f", size = 36357 },
+]
+
+[[package]]
+name = "agent-framework-github-copilot"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "github-copilot-sdk" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/00/f69d731db02e256b8d18d6d8cd20d3d0684245df876f22b836743403a9c1/agent_framework_github_copilot-1.0.0b260130.tar.gz", hash = "sha256:3f5f231785bc8e663da2d1db65a5e4ee49a0f6266e31cccbf3ef05a79ab6c90d", size = 7929 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/b8/0a09396682e915dc25dc39c69fc06cc199b9901ccb0fdbb5e9e2886d2cb0/agent_framework_github_copilot-1.0.0b260130-py3-none-any.whl", hash = "sha256:b8844bacbf666ff1ea7f27d34a42c11be4ade1c4d57e7545341bb74462d82703", size = 8752 },
+]
+
+[[package]]
+name = "agent-framework-lab"
+version = "1.0.0b251024"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/c5/be86273cb3545651d0c8112ff9f38ae8fe13b740ce9b65b9be83ff2d70ee/agent_framework_lab-1.0.0b251024.tar.gz", hash = "sha256:4261cb595b6edfd4f30db613c1885c71b3dcfa2088cf29224d4f17b3ff956b2a", size = 23397 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/0f/3974b2b1f6bf523ee3ced0886b6afd5ca8bbebd24aa5278ef77db0d3d765/agent_framework_lab-1.0.0b251024-py3-none-any.whl", hash = "sha256:1596408991a92fcacef4bb939305d2b59159517b707f48114105fc0dd46bfee7", size = 26589 },
+]
+
[[package]]
name = "agent-framework-mem0"
version = "1.0.0b251007"
@@ -136,6 +283,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/34/93/11905c94b4980b61f4910c7ce26988ff9bfd3a0fa864e2b83c066108363d/agent_framework_mem0-1.0.0b251007-py3-none-any.whl", hash = "sha256:eb9e2e3ae63284f30e4874afd7383e8ad0258a1e1acbc5bdf5e352911d8bb9b3", size = 5297 },
]
+[[package]]
+name = "agent-framework-ollama"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "ollama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/a3/2c18ad3f7878415148a118a6695eb2bf02d2ba99a4138992bad3ad7a194f/agent_framework_ollama-1.0.0b260130.tar.gz", hash = "sha256:312b5d7eaf6894307c57844bf7cd7172f0592cf28f7c253d0a6460992dd87392", size = 8096 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/27/23e23a1919592dcf2aaf25aa9950a7dbda77c4ba03cba8843491b9f12024/agent_framework_ollama-1.0.0b260130-py3-none-any.whl", hash = "sha256:55e4e17f226ad61e8a9dcbbcc24ab006a3480043ecb4d32c12d2444f628054d6", size = 9167 },
+]
+
+[[package]]
+name = "agent-framework-purview"
+version = "1.0.0b260130"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "agent-framework-core" },
+ { name = "azure-core" },
+ { name = "httpx" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/60/09/02ecaddb6c647f1f0b6f399b902bbb99282e1547c4ef169a44f40684696d/agent_framework_purview-1.0.0b260130.tar.gz", hash = "sha256:80c9641f7ab33a8c366dc74b5cf55f91a6bc3095a4d6e5f7cf08a430d4357795", size = 26785 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/8f/c1a53f11fa80fb5dddf550104f9b81f321f23cff224606a19ecc92b4b483/agent_framework_purview-1.0.0b260130-py3-none-any.whl", hash = "sha256:4bd1d0ed320ab04358b662df945b1d59797a4dab497bb3a12cda33136466b8fc", size = 26139 },
+]
+
[[package]]
name = "agent-framework-redis"
version = "1.0.0b251007"
@@ -151,15 +325,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/1b/4aec0bb3976f47396fdc59c2ebdba759078f49f41b4e52a0a2c289d04a1e/agent_framework_redis-1.0.0b251007-py3-none-any.whl", hash = "sha256:4633f2e94d68ea9b4374f564bd622a1766913010b151fc04e01a751343f04804", size = 15606 },
]
-[[package]]
-name = "aiofiles"
-version = "24.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 },
-]
-
[[package]]
name = "aiohappyeyeballs"
version = "2.6.1"
@@ -268,28 +433,31 @@ wheels = [
]
[[package]]
-name = "altair"
-version = "5.5.0"
+name = "annotated-types"
+version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jinja2" },
- { name = "jsonschema" },
- { name = "narwhals" },
- { name = "packaging" },
- { name = "typing-extensions", marker = "python_full_version < '3.14'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/16/b1/f2969c7bdb8ad8bbdda031687defdce2c19afba2aa2c8e1d2a17f78376d8/altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d", size = 705305 }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c", size = 731200 },
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
-name = "annotated-types"
-version = "0.7.0"
+name = "anthropic"
+version = "0.77.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "docstring-parser" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/88/61/50aef0587acd9dd8bf1b8b7fd7fbb25ba4c6ec5387a6ffc195a697951fcc/anthropic-0.77.1.tar.gz", hash = "sha256:a19d78ff6fff9e05d211e3a936051cd5b9462f0eac043d2d45b2372f455d11cd", size = 504691 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+ { url = "https://files.pythonhosted.org/packages/2b/54/e83babf9833547c5548b4e25230ef3d62492e45925b0d104a43e501918a0/anthropic-0.77.1-py3-none-any.whl", hash = "sha256:76fd6f2ab36033a5294d58182a5f712dab9573c3a54413a275ecdf29e727c1e0", size = 397856 },
]
[[package]]
@@ -321,7 +489,6 @@ dependencies = [
{ name = "openai" },
{ name = "pydantic" },
{ name = "requests" },
- { name = "streamlit" },
{ name = "tenacity" },
{ name = "uvicorn" },
{ name = "websockets" },
@@ -329,7 +496,7 @@ dependencies = [
[package.metadata]
requires-dist = [
- { name = "agent-framework", specifier = "==1.0.0b251007" },
+ { name = "agent-framework", specifier = "==1.0.0b260130" },
{ name = "fastapi", specifier = "==0.115.12" },
{ name = "fastmcp", specifier = "==2.7.1" },
{ name = "flasgger", specifier = "==0.9.7.1" },
@@ -339,19 +506,18 @@ requires-dist = [
{ name = "openai", specifier = ">=1.90.0,<1.110.0" },
{ name = "pydantic", specifier = "==2.11.4" },
{ name = "requests", specifier = "==2.32.4" },
- { name = "streamlit", specifier = "==1.45.0" },
{ name = "tenacity", specifier = "==8.5.0" },
{ name = "uvicorn", specifier = ">=0.25.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
[[package]]
-name = "asgiref"
-version = "3.10.0"
+name = "asyncio"
+version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/46/08/4dfec9b90758a59acc6be32ac82e98d1fbfc321cb5cfa410436dbacf821c/asgiref-3.10.0.tar.gz", hash = "sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e", size = 37483 }
+sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/17/9c/fc2331f538fbf7eedba64b2052e99ccf9ba9d6888e2f41441ee28847004b/asgiref-3.10.0-py3-none-any.whl", hash = "sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734", size = 24050 },
+ { url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555 },
]
[[package]]
@@ -405,6 +571,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/10/8b7bd070e3cc804343dab124ce66a3b7999a72d5be0e49232cbcd1d36e18/azure_ai_projects-1.1.0b4-py3-none-any.whl", hash = "sha256:d8aab84fd7cd7c5937e78141e37ca4473dc5ed6cce2c0490c634418abe14afea", size = 126670 },
]
+[[package]]
+name = "azure-common"
+version = "1.1.28"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 },
+]
+
[[package]]
name = "azure-core"
version = "1.35.1"
@@ -420,16 +595,33 @@ wheels = [
]
[[package]]
-name = "azure-core-tracing-opentelemetry"
-version = "1.0.0b12"
+name = "azure-functions"
+version = "1.25.0b3.dev3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "azure-core" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/09/a402b424bf7f489661fdf59126ebc8b9644626033a5f88b9dfe7c5fe1658/azure_functions-1.25.0b3.dev3.tar.gz", hash = "sha256:cee70ab55a87051da5c5ecca4ba747705e64f2c7f76f0f59f9072001059abf32", size = 141925 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/af/c67c849ce87416eae78ddf424e8222f5b0980404e60616a87fb041170344/azure_functions-1.25.0b3.dev3-py3-none-any.whl", hash = "sha256:64ef99a7baf053242394a20fc820458fbf0c207ee5635adfcbb3bb8f0d297716", size = 114054 },
+]
+
+[[package]]
+name = "azure-functions-durable"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiohttp" },
+ { name = "azure-functions" },
+ { name = "furl" },
{ name = "opentelemetry-api" },
+ { name = "opentelemetry-sdk" },
+ { name = "python-dateutil" },
+ { name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/7f/5de13a331a5f2919417819cc37dcf7c897018f02f83aa82b733e6629a6a6/azure_core_tracing_opentelemetry-1.0.0b12.tar.gz", hash = "sha256:bb454142440bae11fd9d68c7c1d67ae38a1756ce808c5e4d736730a7b4b04144", size = 26010 }
+sdist = { url = "https://files.pythonhosted.org/packages/51/3a/f168b434fa69eaaf5d14b54d88239b851eceb7e10f666b55289dd0933ccb/azure-functions-durable-1.4.0.tar.gz", hash = "sha256:945488ef28917dae4295a4dd6e6f6601ffabe32e3fbb94ceb261c9b65b6e6c0f", size = 176584 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/5e/97a471f66935e7f89f521d0e11ae49c7f0871ca38f5c319dccae2155c8d8/azure_core_tracing_opentelemetry-1.0.0b12-py3-none-any.whl", hash = "sha256:38fd42709f1cc4bbc4f2797008b1c30a6a01617e49910c05daa3a0d0c65053ac", size = 11962 },
+ { url = "https://files.pythonhosted.org/packages/74/01/7f03229fa5c05a5cc7e41172aef80c5242d28aeea0825f592f93141a4b91/azure_functions_durable-1.4.0-py3-none-any.whl", hash = "sha256:0efe919cdda96924791feabe192a37c7d872414b4c6ce348417a02ee53d8cc31", size = 143159 },
]
[[package]]
@@ -449,44 +641,18 @@ wheels = [
]
[[package]]
-name = "azure-monitor-opentelemetry"
-version = "1.8.1"
+name = "azure-search-documents"
+version = "11.7.0b2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "azure-common" },
{ name = "azure-core" },
- { name = "azure-core-tracing-opentelemetry" },
- { name = "azure-monitor-opentelemetry-exporter" },
- { name = "opentelemetry-instrumentation-django" },
- { name = "opentelemetry-instrumentation-fastapi" },
- { name = "opentelemetry-instrumentation-flask" },
- { name = "opentelemetry-instrumentation-psycopg2" },
- { name = "opentelemetry-instrumentation-requests" },
- { name = "opentelemetry-instrumentation-urllib" },
- { name = "opentelemetry-instrumentation-urllib3" },
- { name = "opentelemetry-resource-detector-azure" },
- { name = "opentelemetry-sdk" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/55/ae/eae89705498c975b1cfcc2ce0e5bfbe784a47ffd54cef6fbebe31fdb2295/azure_monitor_opentelemetry-1.8.1.tar.gz", hash = "sha256:9b93b62868775d74db60d9e997cfccc5898260c5de23278d7e99cce3764e9fda", size = 53471 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/85/ab/d063f5d0debbb01ef716789f5b4b315d58f657dd5dbf15e47ca6648a557b/azure_monitor_opentelemetry-1.8.1-py3-none-any.whl", hash = "sha256:bebca6af9d81ddc52df59b281a5acc84182bbf1cbccd6f843a2074f6e283947e", size = 27169 },
-]
-
-[[package]]
-name = "azure-monitor-opentelemetry-exporter"
-version = "1.0.0b42"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "azure-core" },
- { name = "azure-identity" },
- { name = "fixedint" },
- { name = "msrest" },
- { name = "opentelemetry-api" },
- { name = "opentelemetry-sdk" },
- { name = "psutil" },
+ { name = "isodate" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d6/d9/ed8c3a4ad8ae2ab8745d940305ca8ec41208dfdefa6a7a232ab62ffe40a3/azure_monitor_opentelemetry_exporter-1.0.0b42.tar.gz", hash = "sha256:d35b60e0404446932e31e3a0b1c2202e47e9b78f91bb24110bd164aa62c5bb87", size = 245626 }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/bde0f03e0a742ba3bbcc929f91ed2f3b1420c2bb84c9a7f878f3b87ebfce/azure_search_documents-11.7.0b2.tar.gz", hash = "sha256:b6e039f8038ff2210d2057e704e867c6e29bb46bfcd400da4383e45e4b8bb189", size = 423956 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/86/c9/d6050a53565f3653205fad79d53982411530b80a72ee1352110c0c0cd0fe/azure_monitor_opentelemetry_exporter-1.0.0b42-py2.py3-none-any.whl", hash = "sha256:649d8a634c119ae942d2dd20ff3006dda88050d3c7044b09bd5111e66439833e", size = 183324 },
+ { url = "https://files.pythonhosted.org/packages/e5/26/ed4498374f9088818278ac225f2bea688b4ec979d81bf83a5355c8c366af/azure_search_documents-11.7.0b2-py3-none-any.whl", hash = "sha256:f82117b321344a84474269ed26df194c24cca619adc024d981b1b86aee3c6f05", size = 432037 },
]
[[package]]
@@ -651,6 +817,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295 },
]
+[[package]]
+name = "clr-loader"
+version = "0.2.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483 },
+]
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -704,6 +882,43 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
]
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 },
+]
+
+[[package]]
+name = "durabletask"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asyncio" },
+ { name = "grpcio" },
+ { name = "packaging" },
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/27/3d021e6b36fc1aab6099fafc56dfc8059b4e8968615a26c1a0418601e50a/durabletask-1.3.0.tar.gz", hash = "sha256:11e38dda6df4737fadca0c71fc0a0f769955877c8a8bdb25ccbf90cf45afbf63", size = 57830 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/87/31ea460dbfaf50d9877f143e2ce9829cac2fb106747d9900cc353356ea77/durabletask-1.3.0-py3-none-any.whl", hash = "sha256:411f23e13391b8845edca010873dd7a87ee7cfc1fe05753ab28a7cd7c3c1bd77", size = 64112 },
+]
+
+[[package]]
+name = "durabletask-azuremanaged"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "azure-identity" },
+ { name = "durabletask" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/29/6bb0b5fe51aa92e117adcdc93efe97cf5476d86c1496e5c5ab35d99a8d07/durabletask_azuremanaged-1.3.0.tar.gz", hash = "sha256:55172588e075afa80d46dcc2e5ddbd84be0a20cc78c74f687040c3720677d34c", size = 4343 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/11/4d34fec302c4813e626080f1532d189767eb31d6d80e8f3698c230512f14/durabletask_azuremanaged-1.3.0-py3-none-any.whl", hash = "sha256:9da914f569da1597c858d494a95eda37e4372726c0ee65f30080dcafab262d60", size = 6366 },
+]
+
[[package]]
name = "exceptiongroup"
version = "1.3.0"
@@ -749,15 +964,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/b8/af0bb06d1388b680c64ec7b9767d3718e51e65d91e425c1296446f10a9fc/fastmcp-2.7.1-py3-none-any.whl", hash = "sha256:e75b4c7088338f2532d79f37a2ae654f47bfd7d3d15340233fda25bc168231b6", size = 127618 },
]
-[[package]]
-name = "fixedint"
-version = "0.1.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/32/c6/b1b9b3f69915d51909ef6ebe6352e286ec3d6f2077278af83ec6e3cc569c/fixedint-0.1.6.tar.gz", hash = "sha256:703005d090499d41ce7ce2ee7eae8f7a5589a81acdc6b79f1728a56495f2c799", size = 12750 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c8/6d/8f5307d26ce700a89e5a67d1e1ad15eff977211f9ed3ae90d7b0d67f4e66/fixedint-0.1.6-py3-none-any.whl", hash = "sha256:b8cf9f913735d2904deadda7a6daa9f57100599da1de57a7448ea1be75ae8c9c", size = 12702 },
-]
-
[[package]]
name = "flasgger"
version = "0.9.7.1"
@@ -878,27 +1084,30 @@ wheels = [
]
[[package]]
-name = "gitdb"
-version = "4.0.12"
+name = "furl"
+version = "2.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "smmap" },
+ { name = "orderedmultidict" },
+ { name = "six" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 }
+sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 },
+ { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550 },
]
[[package]]
-name = "gitpython"
-version = "3.1.45"
+name = "github-copilot-sdk"
+version = "0.1.20"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "gitdb" },
+ { name = "pydantic" },
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076 }
+sdist = { url = "https://files.pythonhosted.org/packages/02/7d/afde0ec85815a558612130dc5ff79536299f411e672410c3edc0c1edeb2a/github_copilot_sdk-0.1.20.tar.gz", hash = "sha256:9e89cd46577fd18dd808d7113b7e20e021c4f944121a0a4891945460fb26c53c", size = 92207 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168 },
+ { url = "https://files.pythonhosted.org/packages/55/91/f8cfa809184988a273af58824b312d31a532ee3ee70875100b5061540178/github_copilot_sdk-0.1.20-py3-none-any.whl", hash = "sha256:e7fa1bb843e2494930126551b80f3a035f36c47a05f9173ad0cdfb4151ad9346", size = 40306 },
]
[[package]]
@@ -957,6 +1166,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586 },
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281 },
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142 },
+ { url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846 },
+ { url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814 },
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899 },
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814 },
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073 },
@@ -966,6 +1177,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497 },
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662 },
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210 },
+ { url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759 },
+ { url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288 },
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685 },
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586 },
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346 },
@@ -973,9 +1186,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659 },
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355 },
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512 },
+ { url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508 },
+ { url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760 },
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425 },
]
+[[package]]
+name = "griffe"
+version = "1.15.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705 },
+]
+
[[package]]
name = "grpcio"
version = "1.76.0rc1"
@@ -1336,7 +1563,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.16.0"
+version = "1.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -1345,15 +1572,18 @@ dependencies = [
{ name = "jsonschema" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
+ { name = "pyjwt", extra = ["crypto"] },
{ name = "python-multipart" },
{ name = "pywin32", marker = "sys_platform == 'win32'" },
{ name = "sse-starlette" },
{ name = "starlette" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918 }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266 },
+ { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615 },
]
[package.optional-dependencies]
@@ -1495,22 +1725,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 },
]
-[[package]]
-name = "msrest"
-version = "0.7.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "azure-core" },
- { name = "certifi" },
- { name = "isodate" },
- { name = "requests" },
- { name = "requests-oauthlib" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 },
-]
-
[[package]]
name = "multidict"
version = "6.7.0"
@@ -1610,15 +1824,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 },
]
-[[package]]
-name = "narwhals"
-version = "2.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/87/76/9ca8f4d03f02b8289807d0c91eeb01fa6b7fdd6273769d5bd1f94773b40b/narwhals-2.7.0.tar.gz", hash = "sha256:e3fff7f1610fd3318ede78c969bc5954ce710d585eefdb689586fb69da3da43c", size = 569315 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/74/0d/bc630dfd34ad2150d40f9392e94d3803980e71a47e10a709ce9bfcd40ffe/narwhals-2.7.0-py3-none-any.whl", hash = "sha256:010791aa0cee86d90bf2b658264aaec3eeea34fb4ddf2e83746ea4940bcffae3", size = 412767 },
-]
-
[[package]]
name = "numpy"
version = "2.3.3"
@@ -1683,12 +1888,16 @@ wheels = [
]
[[package]]
-name = "oauthlib"
-version = "3.3.1"
+name = "ollama"
+version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
+dependencies = [
+ { name = "httpx" },
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
+ { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354 },
]
[[package]]
@@ -1711,279 +1920,89 @@ wheels = [
]
[[package]]
-name = "openapi-pydantic"
-version = "0.5.1"
+name = "openai-agents"
+version = "0.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
+ { name = "griffe" },
+ { name = "mcp" },
+ { name = "openai" },
{ name = "pydantic" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
-]
-
-[[package]]
-name = "opentelemetry-api"
-version = "1.37.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "importlib-metadata" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/63/04/05040d7ce33a907a2a02257e601992f0cdf11c73b33f13c4492bf6c3d6d5/opentelemetry_api-1.37.0.tar.gz", hash = "sha256:540735b120355bd5112738ea53621f8d5edb35ebcd6fe21ada3ab1c61d1cd9a7", size = 64923 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/91/48/28ed9e55dcf2f453128df738210a980e09f4e468a456fa3c763dbc8be70a/opentelemetry_api-1.37.0-py3-none-any.whl", hash = "sha256:accf2024d3e89faec14302213bc39550ec0f4095d1cf5ca688e1bfb1c8612f47", size = 65732 },
-]
-
-[[package]]
-name = "opentelemetry-exporter-otlp-proto-common"
-version = "1.37.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-proto" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/dc/6c/10018cbcc1e6fff23aac67d7fd977c3d692dbe5f9ef9bb4db5c1268726cc/opentelemetry_exporter_otlp_proto_common-1.37.0.tar.gz", hash = "sha256:c87a1bdd9f41fdc408d9cc9367bb53f8d2602829659f2b90be9f9d79d0bfe62c", size = 20430 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/08/13/b4ef09837409a777f3c0af2a5b4ba9b7af34872bc43609dda0c209e4060d/opentelemetry_exporter_otlp_proto_common-1.37.0-py3-none-any.whl", hash = "sha256:53038428449c559b0c564b8d718df3314da387109c4d36bd1b94c9a641b0292e", size = 18359 },
-]
-
-[[package]]
-name = "opentelemetry-exporter-otlp-proto-grpc"
-version = "1.37.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "googleapis-common-protos" },
- { name = "grpcio" },
- { name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-otlp-proto-common" },
- { name = "opentelemetry-proto" },
- { name = "opentelemetry-sdk" },
+ { name = "requests" },
+ { name = "types-requests" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/11/4ad0979d0bb13ae5a845214e97c8d42da43980034c30d6f72d8e0ebe580e/opentelemetry_exporter_otlp_proto_grpc-1.37.0.tar.gz", hash = "sha256:f55bcb9fc848ce05ad3dd954058bc7b126624d22c4d9e958da24d8537763bec5", size = 24465 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/39/17/46630b74751031a658706bef23ac99cdc2953cd3b2d28ec90590a0766b3e/opentelemetry_exporter_otlp_proto_grpc-1.37.0-py3-none-any.whl", hash = "sha256:aee5104835bf7993b7ddaaf380b6467472abaedb1f1dbfcc54a52a7d781a3890", size = 19305 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "packaging" },
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f6/36/7c307d9be8ce4ee7beb86d7f1d31027f2a6a89228240405a858d6e4d64f9/opentelemetry_instrumentation-0.58b0.tar.gz", hash = "sha256:df640f3ac715a3e05af145c18f527f4422c6ab6c467e40bd24d2ad75a00cb705", size = 31549 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-asgi"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "asgiref" },
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7b/e2/03ff707d881d590c7adaed5e9d1979aed7e5e53fc1ed89035e5ed9f304af/opentelemetry_instrumentation_asgi-0.58b0.tar.gz", hash = "sha256:3ccc0c9c1c8c71e8d9da5945c6dcd9c0c8d147839f208536b7042c6dd98e65c9", size = 25116 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/71/a00884c6655387c70070138acbf79a6616ad5d4489680f40708d75b598a7/opentelemetry_instrumentation_asgi-0.58b0-py3-none-any.whl", hash = "sha256:508a6d79e333d648d2afee0e140b6e80eb5d443be183be58e81d9ff88373168a", size = 16798 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-dbapi"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e5/8b/bf6c72d54f77eb0e4445e3b0415e69b3ea5fa40b9372c586db91ffc59b17/opentelemetry_instrumentation_dbapi-0.58b0.tar.gz", hash = "sha256:34ca7e7bf942d5ebf1ea3838e34154b3900bd00d17115a99b83c4ee280e658ac", size = 14223 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/18/bf51a3913da9659d17aa1f44860bcc5d721ddcdcdbfbca80b596139d8811/opentelemetry_instrumentation_dbapi-0.58b0-py3-none-any.whl", hash = "sha256:49283687dfc47f05484d4b186fecca8a96b70e18fd406e34c13eb5f09eabb67c", size = 12522 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-django"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-instrumentation-wsgi" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/69/e8/6055cb9129a5a80b8814b770b79ce559977ff3b3b11dfd4067566ff25c1d/opentelemetry_instrumentation_django-0.58b0.tar.gz", hash = "sha256:24f45706a9dc3c47b9214ed5422fd0d35a850f3f40b04112a91fc10561cfd3f5", size = 25009 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/1a/396e941a7aae4e81793931a60669378c64a147a6dca75344b4c26cc8811b/opentelemetry_instrumentation_django-0.58b0-py3-none-any.whl", hash = "sha256:6e3ada766ce965e9486d193e10cb32749bd51fe1adb5ec6b9310e33fe89fad6d", size = 19596 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-fastapi"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-instrumentation-asgi" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/64/09/4f8fcab834af6b403e5e2d94bdfb2d0835ba8cd1049bcc156995f47b65fb/opentelemetry_instrumentation_fastapi-0.58b0.tar.gz", hash = "sha256:03da470d694116a0a40f4e76319e42f3ff9efc49abf804b2acc2c07f96661497", size = 24598 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/45/fb/82de06eba54e5cb979274f073065ebc374794853502d342b5155073d1194/opentelemetry_instrumentation_fastapi-0.58b0-py3-none-any.whl", hash = "sha256:d89bfec69c9ffc5d9f3fe58655d6660a66b2bca863b9132712c06edcde68b6fa", size = 13460 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-flask"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-instrumentation-wsgi" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
- { name = "packaging" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/df/68aa2dea7e04401f9ce669e6a7a46cc25eb4bb7a14004bf7d535bb27c122/opentelemetry_instrumentation_flask-0.58b0.tar.gz", hash = "sha256:ea2e06f448cef263c21f86401984906f68a5c766c7359000afb5621ae528d9c5", size = 19420 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/07/7f/88079bc3e4aa188d78692328453f906dca35fa9f286623af13df0b0a1ead/opentelemetry_instrumentation_flask-0.58b0-py3-none-any.whl", hash = "sha256:b0d57ad4db7bd0177ddf8c7ae3adf8bd90e2ebfa2dd30884c6a97c97197e4ac5", size = 14685 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-psycopg2"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-instrumentation-dbapi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/36/3e/eee2fafdd5c7f141cffc588859659e3e0808872c7f8f28d321921d6cc08d/opentelemetry_instrumentation_psycopg2-0.58b0.tar.gz", hash = "sha256:e08e2336926a920bc01788d7ff08315c7d995bd62bc9588c316ebb46f05ae95c", size = 10735 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/20/d8/5e92e7e1be63076a23309523c827437ab7e4f77c20fa2077c49aa055cf56/opentelemetry_instrumentation_psycopg2-0.58b0-py3-none-any.whl", hash = "sha256:6afa483f4d1f6d94702082c96e5a0e14bad32195e0d3c10ab5cda92a8f523e36", size = 10732 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-requests"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/36/42/83ee32de763b919779aaa595b60c5a7b9c0a4b33952bbe432c5f6a783085/opentelemetry_instrumentation_requests-0.58b0.tar.gz", hash = "sha256:ae9495e6ff64e27bdb839fce91dbb4be56e325139828e8005f875baf41951a2e", size = 15188 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/4d/f3476b28ea167d1762134352d01ae9693940a42c78994d9f1b32a4477816/opentelemetry_instrumentation_requests-0.58b0-py3-none-any.whl", hash = "sha256:672a0be0bb5b52bea0c11820b35e27edcf4cd22d34abe4afc59a92a80519f8a8", size = 12966 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-urllib"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/84/94/48171907cb9ced5bdc5be18f8cc8a8234bb2ec695f20c69f1330b336f2fb/opentelemetry_instrumentation_urllib-0.58b0.tar.gz", hash = "sha256:071e5a28a1c4198cfa33937484f4b0b1068aab26d75e71e55f598a717f268d0a", size = 13932 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/57/66/37edce21a14b6a983577d7aea95fee3581e80f9b4a272f514726e5041104/opentelemetry_instrumentation_urllib-0.58b0-py3-none-any.whl", hash = "sha256:63ad8a304a299bcb39224ecedc718a391404c8f2d4cc5755edfb5e49904e7b27", size = 12674 },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation-urllib3"
-version = "0.58b0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/9f/e7/affaeadd974587c6eaab1c8af2dfba776dcc083493c97cb193173570d335/opentelemetry_instrumentation_urllib3-0.58b0.tar.gz", hash = "sha256:978b8e3daa076437b1f7ed7509d8156108aee0679556fd355e532c4065dd7635", size = 15791 }
+sdist = { url = "https://files.pythonhosted.org/packages/a4/37/2b4f828840d3ff32d82b813c3371ec9ee26b3b8dc6b4acbb7a4a579f617a/openai_agents-0.3.3.tar.gz", hash = "sha256:b016381a6890e1cb6879eb23c53c35f8c2312be1117f1cd4e4b5e2463150839f", size = 1816230 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1a/5f/d960be00ec15a722f2bfff9d226b320cc6c4191b737c98b937541fdecf83/opentelemetry_instrumentation_urllib3-0.58b0-py3-none-any.whl", hash = "sha256:9e698785afe311edfab772152cb4851f1aaffde5110bb83e4e45d8c4e97277ee", size = 13188 },
+ { url = "https://files.pythonhosted.org/packages/65/59/fd49fd2c3184c0d5fedb8c9c456ae9852154828bca7ee69dce004ea83188/openai_agents-0.3.3-py3-none-any.whl", hash = "sha256:aa2c74e010b923c09f166e63a51fae8c850c62df8581b84bafcbe5bd208d1505", size = 210893 },
]
[[package]]
-name = "opentelemetry-instrumentation-wsgi"
-version = "0.58b0"
+name = "openai-chatkit"
+version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-instrumentation" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "opentelemetry-util-http" },
+ { name = "jinja2" },
+ { name = "openai" },
+ { name = "openai-agents" },
+ { name = "pydantic" },
+ { name = "uvicorn" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3e/76/c33bb3f219dc2266f98b8f927e913e93b31e94af7aa6430a9a9f167f9ab2/opentelemetry_instrumentation_wsgi-0.58b0.tar.gz", hash = "sha256:0ea27d44c83b48e6b182a904c801ca62b2999642647f32ef33c8a9c8bbf6a245", size = 18377 }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/8d/80d05af592b4c9484014de5cb5fd095916ac32f077232f1e62b85452cf07/openai_chatkit-1.6.0.tar.gz", hash = "sha256:01d029f4ddbb2035a84a484cecb254e6848601ae76a466bc8f8ce8b61c62efa6", size = 60890 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/81/05/a168ba97831823e170937b4f1a0f95657c8bd9cc573c81629f464c3cc32b/opentelemetry_instrumentation_wsgi-0.58b0-py3-none-any.whl", hash = "sha256:cef5bdf1cb7a5162fdb1c1a2f95e4a08e02b1ca67ce828a1efdf81e9f23273b7", size = 14449 },
+ { url = "https://files.pythonhosted.org/packages/1a/9d/6830850971dcd89f0461801be0cab7affce8d584799fc1397077bd082c3f/openai_chatkit-1.6.0-py3-none-any.whl", hash = "sha256:241887f65dd129d0af7cc6e30c46c99c4a477317c1862d8620d3a579b0511dcd", size = 42271 },
]
[[package]]
-name = "opentelemetry-proto"
-version = "1.37.0"
+name = "openapi-pydantic"
+version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "protobuf" },
+ { name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/dd/ea/a75f36b463a36f3c5a10c0b5292c58b31dbdde74f6f905d3d0ab2313987b/opentelemetry_proto-1.37.0.tar.gz", hash = "sha256:30f5c494faf66f77faeaefa35ed4443c5edb3b0aa46dad073ed7210e1a789538", size = 46151 }
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c4/25/f89ea66c59bd7687e218361826c969443c4fa15dfe89733f3bf1e2a9e971/opentelemetry_proto-1.37.0-py3-none-any.whl", hash = "sha256:8ed8c066ae8828bbf0c39229979bdf583a126981142378a9cbe9d6fd5701c6e2", size = 72534 },
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381 },
]
[[package]]
-name = "opentelemetry-resource-detector-azure"
-version = "0.1.5"
+name = "opentelemetry-api"
+version = "1.39.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "opentelemetry-sdk" },
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/67/e4/0d359d48d03d447225b30c3dd889d5d454e3b413763ff721f9b0e4ac2e59/opentelemetry_resource_detector_azure-0.1.5.tar.gz", hash = "sha256:e0ba658a87c69eebc806e75398cd0e9f68a8898ea62de99bc1b7083136403710", size = 11503 }
+sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c3/ae/c26d8da88ba2e438e9653a408b0c2ad6f17267801250a8f3cc6405a93a72/opentelemetry_resource_detector_azure-0.1.5-py3-none-any.whl", hash = "sha256:4dcc5d54ab5c3b11226af39509bc98979a8b9e0f8a24c1b888783755d3bf00eb", size = 14252 },
+ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 },
]
[[package]]
name = "opentelemetry-sdk"
-version = "1.37.0"
+version = "1.39.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f4/62/2e0ca80d7fe94f0b193135375da92c640d15fe81f636658d2acf373086bc/opentelemetry_sdk-1.37.0.tar.gz", hash = "sha256:cc8e089c10953ded765b5ab5669b198bbe0af1b3f89f1007d19acd32dc46dda5", size = 170404 }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/62/9f4ad6a54126fb00f7ed4bb5034964c6e4f00fcd5a905e115bd22707e20d/opentelemetry_sdk-1.37.0-py3-none-any.whl", hash = "sha256:8f3c3c22063e52475c5dbced7209495c2c16723d016d39287dfc215d1771257c", size = 131941 },
+ { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 },
]
[[package]]
name = "opentelemetry-semantic-conventions"
-version = "0.58b0"
+version = "0.60b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/aa/1b/90701d91e6300d9f2fb352153fb1721ed99ed1f6ea14fa992c756016e63a/opentelemetry_semantic_conventions-0.58b0.tar.gz", hash = "sha256:6bd46f51264279c433755767bb44ad00f1c9e2367e1b42af563372c5a6fa0c25", size = 129867 }
+sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954 },
+ { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 },
]
[[package]]
@@ -1996,12 +2015,15 @@ wheels = [
]
[[package]]
-name = "opentelemetry-util-http"
-version = "0.58b0"
+name = "orderedmultidict"
+version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411 }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652 },
+ { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897 },
]
[[package]]
@@ -2013,119 +2035,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
-[[package]]
-name = "pandas"
-version = "2.3.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
- { name = "python-dateutil" },
- { name = "pytz" },
- { name = "tzdata" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 },
- { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 },
- { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 },
- { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 },
- { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 },
- { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 },
- { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 },
- { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 },
- { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 },
- { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 },
- { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 },
- { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 },
- { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 },
- { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 },
- { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 },
- { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 },
- { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 },
- { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 },
- { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 },
- { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 },
- { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 },
- { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 },
- { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 },
- { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 },
- { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 },
- { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 },
- { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 },
- { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 },
- { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 },
- { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 },
- { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 },
- { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 },
- { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 },
-]
-
-[[package]]
-name = "pillow"
-version = "11.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 },
- { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 },
- { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 },
- { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 },
- { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 },
- { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 },
- { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 },
- { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 },
- { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 },
- { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 },
- { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 },
- { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 },
- { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 },
- { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 },
- { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 },
- { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 },
- { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 },
- { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 },
- { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 },
- { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 },
- { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 },
- { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 },
- { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 },
- { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 },
- { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 },
- { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 },
- { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 },
- { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 },
- { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 },
- { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 },
- { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 },
- { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 },
- { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 },
- { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 },
- { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 },
- { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 },
- { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 },
- { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 },
- { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 },
- { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 },
- { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 },
- { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 },
- { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 },
- { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 },
- { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 },
- { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 },
- { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 },
- { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 },
- { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 },
- { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 },
- { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 },
- { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 },
- { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 },
- { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 },
- { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 },
- { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 },
- { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 },
-]
-
[[package]]
name = "ply"
version = "3.11"
@@ -2164,6 +2073,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/de/84/586422d8861b5391c8414360b10f603c0b7859bb09ad688e64430ed0df7b/posthog-6.7.6-py3-none-any.whl", hash = "sha256:b09a7e65a042ec416c28874b397d3accae412a80a8b0ef3fa686fbffc99e4d4b", size = 137348 },
]
+[[package]]
+name = "powerfx"
+version = "0.0.34"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+ { name = "pythonnet" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/96/0f8a1f86485b3ec0315e3e8403326884a0334b3dcd699df2482669cca4be/powerfx-0.0.34-py3-none-any.whl", hash = "sha256:f2dc1c42ba8bfa4c72a7fcff2a00755b95394547388ca0b3e36579c49ee7ed75", size = 3483089 },
+]
+
[[package]]
name = "propcache"
version = "0.4.1"
@@ -2274,51 +2196,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 },
]
-[[package]]
-name = "psutil"
-version = "7.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242 },
- { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682 },
- { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994 },
- { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163 },
- { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625 },
- { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812 },
- { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965 },
- { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971 },
-]
-
-[[package]]
-name = "pyarrow"
-version = "21.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305 },
- { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264 },
- { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099 },
- { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529 },
- { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883 },
- { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802 },
- { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175 },
- { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306 },
- { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622 },
- { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094 },
- { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576 },
- { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342 },
- { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218 },
- { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551 },
- { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064 },
- { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837 },
- { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158 },
- { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885 },
- { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625 },
- { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890 },
- { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006 },
-]
-
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -2420,19 +2297,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608 },
]
-[[package]]
-name = "pydeck"
-version = "0.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jinja2" },
- { name = "numpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/40e14e196864a0f61a92abb14d09b3d3da98f94ccb03b49cf51688140dab/pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605", size = 3832240 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 },
-]
-
[[package]]
name = "pygments"
version = "2.19.2"
@@ -2495,6 +2359,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577 },
]
+[[package]]
+name = "pythonnet"
+version = "3.0.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "clr-loader" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 },
+]
+
[[package]]
name = "pytz"
version = "2025.2"
@@ -2641,19 +2517,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
]
-[[package]]
-name = "requests-oauthlib"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "oauthlib" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 },
-]
-
[[package]]
name = "rich"
version = "14.1.0"
@@ -2778,15 +2641,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
-[[package]]
-name = "smmap"
-version = "5.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 },
-]
-
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -2849,35 +2703,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
]
-[[package]]
-name = "streamlit"
-version = "1.45.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "altair" },
- { name = "blinker" },
- { name = "cachetools" },
- { name = "click" },
- { name = "gitpython" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "pandas" },
- { name = "pillow" },
- { name = "protobuf" },
- { name = "pyarrow" },
- { name = "pydeck" },
- { name = "requests" },
- { name = "tenacity" },
- { name = "toml" },
- { name = "tornado" },
- { name = "typing-extensions" },
- { name = "watchdog", marker = "sys_platform != 'darwin'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c0/52/83d46b4c5044477e9c6dee779704a9d4041610439533af9c0a5084921098/streamlit-1.45.0.tar.gz", hash = "sha256:4e99014e113a11a7163b9da5ac079efb1ae5f8575a09c5a6a9c43cd6877a2a88", size = 9462166 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/43/ff/f41cfaf1bb58223fe77ff87213a689f6c9c82f7363f9d7c879d294dbe985/streamlit-1.45.0-py3-none-any.whl", hash = "sha256:b7d03ec68a23de0f1922ec9a28fbe3fe37d9fb31ad31d6c429d262c3631c2943", size = 9856265 },
-]
-
[[package]]
name = "tenacity"
version = "8.5.0"
@@ -2887,34 +2712,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165 },
]
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 },
-]
-
-[[package]]
-name = "tornado"
-version = "6.5.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563 },
- { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729 },
- { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295 },
- { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644 },
- { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878 },
- { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549 },
- { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973 },
- { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954 },
- { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023 },
- { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427 },
- { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456 },
-]
-
[[package]]
name = "tqdm"
version = "4.67.1"
@@ -2942,6 +2739,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748 },
]
+[[package]]
+name = "types-requests"
+version = "2.32.4.20260107"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676 },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -2963,15 +2772,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
]
-[[package]]
-name = "tzdata"
-version = "2025.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
-]
-
[[package]]
name = "urllib3"
version = "2.5.0"
@@ -3025,24 +2825,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
]
-[[package]]
-name = "watchdog"
-version = "6.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
- { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
- { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
- { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
- { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
- { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
- { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
- { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
- { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
- { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
-]
-
[[package]]
name = "watchfiles"
version = "1.1.0"
@@ -3153,55 +2935,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 },
]
-[[package]]
-name = "wrapt"
-version = "1.17.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 },
- { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 },
- { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 },
- { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 },
- { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 },
- { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 },
- { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 },
- { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 },
- { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 },
- { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 },
- { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 },
- { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 },
- { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 },
- { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 },
- { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 },
- { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 },
- { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 },
- { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 },
- { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 },
- { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 },
- { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 },
- { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 },
- { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 },
- { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 },
- { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 },
- { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 },
- { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 },
- { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 },
- { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 },
- { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 },
- { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 },
- { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 },
- { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 },
- { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 },
- { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 },
- { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 },
- { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 },
- { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 },
- { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 },
- { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 },
- { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 },
-]
-
[[package]]
name = "yarl"
version = "1.22.0"
diff --git a/agentic_ai/workflow/fraud_detection_durable/.env.sample b/agentic_ai/workflow/fraud_detection_durable/.env.sample
new file mode 100644
index 000000000..f237bfe30
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/.env.sample
@@ -0,0 +1,26 @@
+# Azure OpenAI Configuration
+AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/
+AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4o
+AZURE_OPENAI_API_VERSION=2024-10-01-preview
+# Optional: Use API key instead of Azure CLI auth
+# AZURE_OPENAI_API_KEY=your-api-key
+
+# MCP Server
+MCP_SERVER_URI=http://localhost:8000/mcp
+
+# Durable Task Scheduler
+# Local emulator (default)
+DTS_ENDPOINT=http://localhost:8080
+DTS_TASKHUB=fraud-detection
+
+# Azure-hosted (production)
+# DTS_ENDPOINT=https://your-dts-endpoint.azure.com
+# DTS_TASKHUB=fraud-detection-prod
+
+# Human-in-the-loop Configuration
+ANALYST_APPROVAL_TIMEOUT_HOURS=72
+MAX_REVIEW_ATTEMPTS=3
+
+# Application Insights (optional - enables telemetry)
+# APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=...
+# ENABLE_SENSITIVE_DATA=true
diff --git a/agentic_ai/workflow/fraud_detection_durable/Dockerfile b/agentic_ai/workflow/fraud_detection_durable/Dockerfile
new file mode 100644
index 000000000..06d1c76d8
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/Dockerfile
@@ -0,0 +1,72 @@
+# Fraud Detection Durable Workflow
+# Multi-stage build: React UI + Python Worker/Backend
+# DTS runs as sidecar container in Container Apps
+# Build context: agentic_ai/ (parent dir, for shared observability module)
+
+# ============================================================================
+# Stage 1: Build React Frontend
+# ============================================================================
+FROM node:20-alpine AS frontend-builder
+
+WORKDIR /app/frontend
+
+# Copy frontend package files
+COPY workflow/fraud_detection_durable/ui/package*.json ./
+
+# Install dependencies
+RUN npm ci
+
+# Copy frontend source
+COPY workflow/fraud_detection_durable/ui/ ./
+
+# Build React app (Vite outputs to 'dist/')
+RUN npm run build
+
+# ============================================================================
+# Stage 2: Python Backend with Frontend Assets
+# ============================================================================
+FROM python:3.12-slim
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install uv for fast Python package management
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
+
+WORKDIR /app
+
+# Copy dependency files first for caching
+COPY workflow/fraud_detection_durable/pyproject.toml ./
+
+# Install Python dependencies
+RUN uv pip install --system -e .
+
+# Copy application code
+COPY workflow/fraud_detection_durable/*.py ./
+COPY workflow/fraud_detection_durable/start.sh ./
+RUN chmod +x /app/start.sh
+
+# Copy shared observability module
+COPY observability /app/observability
+
+# Copy built frontend from previous stage (Vite outputs to 'dist')
+COPY --from=frontend-builder /app/frontend/dist ./static
+
+# Expose ports
+# 8002 - FastAPI Backend (serves both API and UI)
+EXPOSE 8002
+
+# Environment variables (can be overridden)
+# DTS_ENDPOINT should point to the sidecar container
+ENV DTS_ENDPOINT="http://localhost:8080"
+ENV DTS_TASKHUB="default"
+ENV PYTHONUNBUFFERED=1
+ENV BACKEND_PORT=8002
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
+ CMD curl -f http://localhost:8002/health || exit 1
+
+CMD ["/app/start.sh"]
diff --git a/agentic_ai/workflow/fraud_detection_durable/README.md b/agentic_ai/workflow/fraud_detection_durable/README.md
new file mode 100644
index 000000000..5aa5c7753
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/README.md
@@ -0,0 +1,360 @@
+# Durable Fraud Detection Workflow
+
+A hybrid architecture combining **Workflow** (complex topology) and **Durable Task** (durability, HITL) for enterprise-grade fraud detection.
+
+## ποΈ Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β DURABLE TASK ORCHESTRATION (Outer Layer) β
+β Handles: Durability, Long Waits, Crash Recovery β
+β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β 1. Receive Alert β β
+β β 2. Call "run_fraud_analysis" Activity βββββββββββββββββββββββ β β
+β β β β β
+β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
+β β β WORKFLOW (Inner Layer - Activity) β β β β
+β β β Handles: Complex Topology, Fast Execution β β β β
+β β β β β β β
+β β β AlertRouter β β β β
+β β β β β β β β
+β β β βββββ΄ββββ¬ββββββββ (fan-out) β β β β
+β β β β β β β β β β
+β β β Usage Location Billing β β β β
+β β β βββββββββΌββββββββ (fan-in) β β β β
+β β β β β β β β
+β β β Aggregator (LLM) β β β β
+β β β β β β β β
+β β β Returns: FraudRiskAssessment β β β β
+β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β β
+β β βββββββ β β
+β β 3. Check Risk Score (simple if/else) β β
+β β β β
+β β IF risk >= 0.6: β β
+β β β notify_analyst Activity β β
+β β β wait_for_external_event("AnalystDecision") βΈοΈ β β
+β β β execute_fraud_action Activity β β
+β β β β
+β β ELSE: β β
+β β β auto_clear Activity β β
+β β β β
+β β 4. send_notification Activity β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+## π― Why Hybrid Architecture?
+
+| Feature | Workflow Only | Durable Task Only | **Hybrid (This)** |
+|---------|--------------|-------------------|-------------------|
+| Complex topology (fan-out/fan-in) | β
Easy | β Manual | β
Easy |
+| Crash recovery | β Lost state | β
Automatic | β
Automatic |
+| Human-in-the-loop | β οΈ Manual checkpoints | β
Built-in events | β
Built-in events |
+| Timeout handling | β Not built-in | β
Native timers | β
Native timers |
+| Long waits (hours/days) | β Memory-bound | β
Persistent | β
Persistent |
+| Visibility/Dashboard | β Custom logging | β
DTS Dashboard | β
DTS Dashboard |
+
+## π Project Structure
+
+```
+fraud_detection_durable/
+βββ pyproject.toml # Dependencies
+βββ .env.sample # Environment template
+βββ fraud_analysis_workflow.py # Inner workflow (fan-out β aggregate)
+βββ worker.py # DTS Worker with orchestration
+βββ client.py # CLI client for testing
+βββ backend.py # FastAPI backend for UI
+βββ README.md # This file
+βββ ui/ # React UI
+ βββ src/
+ β βββ App.jsx # Main app with WebSocket connection
+ β βββ components/
+ β βββ WorkflowVisualizer.jsx # Interactive workflow diagram
+ βββ package.json
+```
+
+## π Quick Start
+
+### Prerequisites
+
+1. **Docker** - For DTS emulator
+2. **Python 3.12+**
+3. **Azure OpenAI** - With a deployed model
+4. **MCP Server** - Running on port 8000
+
+### Step 1: Start Durable Task Scheduler
+
+```bash
+docker run -d --name dts-emulator \
+ -p 8080:8080 -p 8082:8082 \
+ mcr.microsoft.com/dts/dts-emulator:latest
+```
+
+Dashboard: http://localhost:8082
+
+### Step 2: Start MCP Server
+
+```bash
+cd mcp
+uv run mcp_service.py
+```
+
+### Step 3: Configure Environment
+
+```bash
+cd agentic_ai/workflow/fraud_detection_durable
+cp .env.sample .env
+# Edit .env with your Azure OpenAI credentials
+```
+
+### Step 4: Install Dependencies
+
+```bash
+uv sync
+```
+
+### Step 5: Start Worker
+
+```bash
+uv run worker.py
+```
+
+### Step 6: Run Tests
+
+**Option A: CLI Client**
+```bash
+uv run client.py
+```
+
+**Option B: FastAPI Backend + React UI**
+```bash
+# Terminal 1: Backend
+uv run backend.py
+
+# Terminal 2: React UI
+cd ui
+npm install
+npm run dev
+```
+
+Open http://localhost:5173 to view the interactive workflow UI.
+
+## π§ͺ Test Scenarios
+
+### 1. High-Risk Alert with Analyst Approval
+
+```
+Alert: ALERT-001 (multi_country_login, high severity)
+ β
+Workflow: Fan-out to 3 specialists
+ β
+Risk Score: 0.75 (HIGH RISK)
+ β
+βΈοΈ Waiting for analyst decision...
+ β
+Analyst: "lock_account"
+ β
+β
Account locked, notification sent
+```
+
+### 2. Low-Risk Alert with Auto-Clear
+
+```
+Alert: ALERT-002 (data_spike, low severity)
+ β
+Workflow: Fan-out to 3 specialists
+ β
+Risk Score: 0.35 (LOW RISK)
+ β
+β
Auto-cleared, notification sent
+```
+
+### 3. Timeout Escalation
+
+```
+Alert: ALERT-003 (unusual_charges, high severity)
+ β
+Workflow: Fan-out to 3 specialists
+ β
+Risk Score: 0.80 (CRITICAL)
+ β
+βΈοΈ Waiting for analyst decision...
+ β
+β° Timeout (72 hours)
+ β
+β οΈ Escalated to manager
+```
+
+## οΏ½οΈ React UI Features
+
+The interactive React UI provides real-time visualization of the fraud detection workflow:
+
+### Interactive Workflow Diagram
+
+- **Real-time Status Updates**: Nodes change color based on execution state
+ - Gray: Pending
+ - Blue: Running (with pulse animation)
+ - Green: Completed
+ - Red: Failed
+
+- **Clickable Nodes**: Click any workflow step to see detailed execution info:
+ - **Tool Calls**: Actual MCP tool calls made (e.g., `get_billing_summary`, `get_data_usage`)
+ - **Arguments**: Parameters passed to each tool
+ - **Results**: Output returned from each tool call
+ - **Step Output**: Final output from the agent step
+
+### Human-in-the-Loop Panel
+
+When the workflow reaches the Review Gateway (risk β₯ 0.6):
+- Shows analyst decision options: `lock_account`, `flag_review`, `dismiss`
+- Displays risk assessment details
+- Allows analyst to submit decision via UI
+
+### Example: Viewing Step Details
+
+```
+1. Click on "Usage Analyst" node
+2. Popover shows:
+ - Tool Calls (Real):
+ β’ get_data_usage(subscription_id=5, start_date="2025-12-01", ...)
+ β Result: {"total_gb": 45.2, "daily_avg": 1.5, ...}
+ β’ get_billing_summary(customer_id=3)
+ β Result: {"current_balance": 150.00, ...}
+ - Output: "Usage analysis indicates 300% spike in data..."
+```
+
+## οΏ½π DTS Dashboard
+
+Open http://localhost:8082 to see:
+
+- All orchestration instances
+- Pending external events (analyst decisions)
+- Activity execution logs
+- Orchestration timeline and status
+
+
+
+## π§ Configuration
+
+### Environment Variables
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI endpoint | Required |
+| `AZURE_OPENAI_CHAT_DEPLOYMENT` | Deployment name | `gpt-4o` |
+| `MCP_SERVER_URI` | MCP server URL | `http://localhost:8000/mcp` |
+| `DTS_ENDPOINT` | DTS scheduler URL | `http://localhost:8080` |
+| `DTS_TASKHUB` | DTS task hub name | `fraud-detection` |
+| `ANALYST_APPROVAL_TIMEOUT_HOURS` | Timeout for analyst review | `72` |
+
+### Risk Threshold
+
+Edit `worker.py` to change the risk threshold:
+
+```python
+# Current: 0.6 (60%)
+if risk_score >= 0.6:
+ # High risk path
+else:
+ # Low risk path
+```
+
+## ποΈ Key Components
+
+### 1. Inner Workflow (`fraud_analysis_workflow.py`)
+
+The workflow handles complex multi-agent topology:
+
+```python
+# Fan-out: Alert β 3 Specialists
+builder.add_edge(alert_router, usage_executor)
+builder.add_edge(alert_router, location_executor)
+builder.add_edge(alert_router, billing_executor)
+
+# Fan-in: 3 Specialists β Aggregator
+builder.add_fan_in_edge(
+ [usage_executor, location_executor, billing_executor],
+ aggregator
+)
+```
+
+### 2. DTS Orchestration (`worker.py`)
+
+The orchestration handles durability and HITL:
+
+```python
+def fraud_detection_orchestration(context, payload):
+ # Run inner workflow as activity
+ assessment = yield context.call_activity("run_fraud_analysis", alert)
+
+ if assessment["risk_score"] >= 0.6:
+ # Wait for analyst with timeout
+ approval_task = context.wait_for_external_event("AnalystDecision")
+ timeout_task = context.create_timer(timedelta(hours=72))
+
+ winner = yield when_any([approval_task, timeout_task])
+
+ if winner == approval_task:
+ yield context.call_activity("execute_fraud_action", decision)
+ else:
+ yield context.call_activity("escalate_timeout", assessment)
+ else:
+ yield context.call_activity("auto_clear_alert", assessment)
+```
+
+### 3. Activities
+
+| Activity | Purpose |
+|----------|---------|
+| `run_fraud_analysis` | Runs inner workflow, returns assessment |
+| `notify_analyst` | Sends notification for review |
+| `execute_fraud_action` | Executes approved action |
+| `auto_clear_alert` | Auto-clears low-risk alerts |
+| `escalate_timeout` | Escalates on timeout |
+| `send_notification` | Sends final notification |
+
+## π Comparison with Original Implementation
+
+| Aspect | Original (`fraud_detection/`) | Durable (`fraud_detection_durable/`) |
+|--------|-------------------------------|-------------------------------------|
+| HITL Pattern | `ctx.request_info()` + `@response_handler` | `wait_for_external_event()` |
+| Checkpointing | `FileCheckpointStorage` (manual) | DTS (automatic) |
+| Timeout | Not built-in | Native `create_timer()` |
+| Recovery | Load checkpoint manually | Automatic replay |
+| Dashboard | Custom logging | DTS Dashboard |
+| Topology | Full workflow | Workflow as activity |
+
+## π Troubleshooting
+
+### "Cannot connect to DTS"
+
+```bash
+# Check if DTS is running
+docker ps | grep dts
+
+# Restart if needed
+docker restart dts-emulator
+```
+
+### "Worker not processing"
+
+1. Check worker is running: `uv run worker.py`
+2. Check logs for errors
+3. Verify DTS endpoint in `.env`
+
+### "Analyst decision not received"
+
+1. Check instance ID matches
+2. Verify event name is `AnalystDecision`
+3. Check DTS dashboard for pending events
+
+## π Related Documentation
+
+- [Agent Framework Workflow](../human-in-the-loop.md)
+- [Durable Task Samples](https://github.com/microsoft/agent-framework/tree/main/python/samples/getting_started/durabletask)
+- [Original Fraud Detection](../fraud_detection/README.md)
+
+## π License
+
+Copyright (c) Microsoft. All rights reserved.
diff --git a/agentic_ai/workflow/fraud_detection_durable/backend.py b/agentic_ai/workflow/fraud_detection_durable/backend.py
new file mode 100644
index 000000000..df653c200
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/backend.py
@@ -0,0 +1,571 @@
+"""
+FastAPI Backend for Durable Fraud Detection.
+
+This backend provides:
+1. REST API to start orchestrations and submit decisions
+2. WebSocket for real-time status updates
+3. Integration with DTS client
+
+The UI connects here instead of managing workflow directly.
+"""
+
+import asyncio
+import json
+import logging
+import os
+import sys
+import time
+from datetime import datetime
+from typing import Any
+
+from pathlib import Path
+
+from dotenv import load_dotenv
+
+# Load environment first so observability can read connection string
+load_dotenv()
+
+# ------------------------------------------------------------------
+# Observability (must be before any agent imports)
+# ------------------------------------------------------------------
+# Add parent directories to path for the shared observability module
+sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # agentic_ai/
+
+try:
+ from observability import setup_observability
+ _observability_enabled = setup_observability(
+ service_name="contoso-fraud-workflow",
+ enable_live_metrics=True,
+ enable_sensitive_data=os.getenv("ENABLE_SENSITIVE_DATA", "false").lower() in ("1", "true", "yes"),
+ )
+except ImportError:
+ _observability_enabled = False
+
+# ------------------------------------------------------------------
+from azure.identity import DefaultAzureCredential
+from durabletask.azuremanaged.client import DurableTaskSchedulerClient
+from durabletask.client import OrchestrationState
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import FileResponse
+from pydantic import BaseModel
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+if _observability_enabled:
+ logger.info("β
Application Insights observability enabled for fraud workflow backend")
+
+# FastAPI app
+app = FastAPI(
+ title="Durable Fraud Detection API",
+ description="Hybrid Workflow + Durable Task architecture for fraud detection",
+ version="1.0.0",
+)
+
+# CORS - allow localhost for dev and Azure Container Apps for prod
+CORS_ORIGINS = [
+ "http://localhost:3000",
+ "http://localhost:5173",
+ "http://localhost:8002",
+]
+# Add Azure Container Apps URL if set
+if os.getenv("CONTAINER_APP_URL"):
+ CORS_ORIGINS.append(os.getenv("CONTAINER_APP_URL"))
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=CORS_ORIGINS,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# ============================================================================
+# Static Files (React UI)
+# ============================================================================
+
+# Serve static files from React build (production mode)
+# Vite outputs to 'dist/' with assets in 'dist/assets/'
+STATIC_DIR = Path(__file__).parent / "static"
+STATIC_ASSET_DIR = STATIC_DIR / "assets" # Vite structure
+
+if STATIC_ASSET_DIR.exists():
+ app.mount("/assets", StaticFiles(directory=str(STATIC_ASSET_DIR)), name="assets")
+ logger.info(f"Serving static assets from {STATIC_ASSET_DIR}")
+elif STATIC_DIR.exists():
+ # Fallback: mount entire static dir
+ logger.info(f"Static assets dir not found, mounting {STATIC_DIR}")
+
+# Constants
+ANALYST_APPROVAL_EVENT = "AnalystDecision"
+ORCHESTRATION_NAME = "fraud_detection_orchestration"
+
+
+# ============================================================================
+# Request/Response Models
+# ============================================================================
+
+
+class StartWorkflowRequest(BaseModel):
+ """Request to start a fraud detection workflow."""
+ alert_id: str
+ customer_id: int
+ alert_type: str
+ description: str = ""
+ severity: str = "medium"
+ approval_timeout_hours: float = 72.0
+
+
+class StartWorkflowResponse(BaseModel):
+ """Response after starting a workflow."""
+ instance_id: str
+ alert_id: str
+ status: str
+
+
+class AnalystDecisionRequest(BaseModel):
+ """Analyst decision submitted from UI."""
+ instance_id: str
+ alert_id: str
+ approved_action: str
+ analyst_notes: str = ""
+ analyst_id: str = "analyst_ui"
+
+
+class WorkflowStatusResponse(BaseModel):
+ """Workflow status response."""
+ instance_id: str
+ status: str
+ custom_status: str | None
+ result: dict | None
+
+
+class AlertInfo(BaseModel):
+ """Sample alert info."""
+ alert_id: str
+ customer_id: int
+ alert_type: str
+ description: str
+ severity: str
+
+
+# ============================================================================
+# Sample Alerts
+# ============================================================================
+
+SAMPLE_ALERTS = [
+ AlertInfo(
+ alert_id="ALERT-001",
+ customer_id=1,
+ alert_type="multi_country_login",
+ description="Login attempts from USA and Russia within 2 hours",
+ severity="high",
+ ),
+ AlertInfo(
+ alert_id="ALERT-002",
+ customer_id=2,
+ alert_type="data_spike",
+ description="Data usage increased by 500% in last 24 hours",
+ severity="medium",
+ ),
+ AlertInfo(
+ alert_id="ALERT-003",
+ customer_id=3,
+ alert_type="unusual_charges",
+ description="Three large purchases totaling $5,000 in 10 minutes",
+ severity="high",
+ ),
+]
+
+
+# ============================================================================
+# DTS Client
+# ============================================================================
+
+_dts_client: DurableTaskSchedulerClient | None = None
+
+
+def get_dts_client() -> DurableTaskSchedulerClient:
+ """Get or create DTS client."""
+ global _dts_client
+
+ if _dts_client is None:
+ taskhub = os.getenv("DTS_TASKHUB", "default")
+ endpoint = os.getenv("DTS_ENDPOINT", "http://localhost:8080")
+
+ credential = None if endpoint.startswith("http://localhost") else DefaultAzureCredential()
+
+ _dts_client = DurableTaskSchedulerClient(
+ host_address=endpoint,
+ secure_channel=not endpoint.startswith("http://localhost"),
+ taskhub=taskhub,
+ token_credential=credential,
+ )
+ logger.info(f"DTS client initialized: {endpoint}/{taskhub}")
+
+ return _dts_client
+
+
+# ============================================================================
+# WebSocket Manager
+# ============================================================================
+
+
+class ConnectionManager:
+ """Manages WebSocket connections for real-time updates."""
+
+ def __init__(self):
+ self.active_connections: dict[str, list[WebSocket]] = {} # instance_id -> connections
+
+ async def connect(self, websocket: WebSocket, instance_id: str):
+ await websocket.accept()
+ if instance_id not in self.active_connections:
+ self.active_connections[instance_id] = []
+ self.active_connections[instance_id].append(websocket)
+ logger.info(f"WebSocket connected for instance {instance_id}")
+
+ def disconnect(self, websocket: WebSocket, instance_id: str):
+ if instance_id in self.active_connections:
+ self.active_connections[instance_id].remove(websocket)
+ if not self.active_connections[instance_id]:
+ del self.active_connections[instance_id]
+ logger.info(f"WebSocket disconnected for instance {instance_id}")
+
+ async def broadcast(self, instance_id: str, message: dict):
+ """Broadcast message to all connections watching this instance."""
+ if instance_id in self.active_connections:
+ disconnected = []
+ for connection in self.active_connections[instance_id]:
+ try:
+ await connection.send_json(message)
+ except Exception:
+ disconnected.append(connection)
+
+ for conn in disconnected:
+ self.disconnect(conn, instance_id)
+
+
+manager = ConnectionManager()
+
+
+# ============================================================================
+# Background Task: Poll DTS for Status Updates
+# ============================================================================
+
+_polling_tasks: dict[str, asyncio.Task] = {}
+
+
+async def poll_orchestration_status(instance_id: str):
+ """Background task to poll DTS and broadcast status updates."""
+ client = get_dts_client()
+ last_status = None
+ last_custom_status = None
+
+ while instance_id in manager.active_connections:
+ try:
+ state = client.get_orchestration_state(instance_id)
+
+ if state:
+ status = state.runtime_status.name
+ custom_status_raw = state.serialized_custom_status
+
+ # Debug: log raw custom_status
+ # logger.info(f"[Backend] Raw custom_status: {custom_status_raw[:200] if custom_status_raw else 'None'}")
+
+ # Parse custom_status JSON if present
+ custom_status = custom_status_raw
+ step_details = None
+ status_message = custom_status_raw
+
+ if custom_status_raw:
+ try:
+ parsed = json.loads(custom_status_raw)
+ # Handle double-encoding: if parsed is still a string, parse again
+ if isinstance(parsed, str):
+ try:
+ parsed = json.loads(parsed)
+ except json.JSONDecodeError:
+ pass # Keep as string
+
+ if isinstance(parsed, dict):
+ status_message = parsed.get("message", custom_status_raw)
+ step_details = parsed.get("step_details")
+ custom_status = status_message
+ except json.JSONDecodeError:
+ pass # Keep original string
+
+ # Only broadcast if status changed
+ if status != last_status or custom_status_raw != last_custom_status:
+ message = {
+ "type": "status_update",
+ "instance_id": instance_id,
+ "status": status,
+ "custom_status": custom_status,
+ "step_details": step_details,
+ "timestamp": datetime.now().isoformat(),
+ }
+
+ # Check if waiting for analyst
+ if custom_status and "Awaiting analyst" in custom_status:
+ message["decision_required"] = True
+
+ # Check if completed
+ if status in ("COMPLETED", "FAILED", "TERMINATED"):
+ if state.serialized_output:
+ try:
+ message["result"] = json.loads(state.serialized_output)
+ except json.JSONDecodeError:
+ message["result"] = {"raw": state.serialized_output}
+
+ await manager.broadcast(instance_id, message)
+
+ last_status = status
+ last_custom_status = custom_status_raw
+
+ # Stop polling if completed
+ if status in ("COMPLETED", "FAILED", "TERMINATED"):
+ logger.info(f"Orchestration {instance_id} completed, stopping poll")
+ break
+
+ except Exception as e:
+ logger.error(f"Error polling status for {instance_id}: {e}")
+
+ await asyncio.sleep(0.1) # Poll every 100ms for responsive UI
+
+ # Cleanup
+ if instance_id in _polling_tasks:
+ del _polling_tasks[instance_id]
+
+
+def start_status_polling(instance_id: str):
+ """Start background polling for an instance."""
+ if instance_id not in _polling_tasks:
+ task = asyncio.create_task(poll_orchestration_status(instance_id))
+ _polling_tasks[instance_id] = task
+
+
+# ============================================================================
+# REST API Endpoints
+# ============================================================================
+
+
+@app.get("/")
+async def read_root():
+ """Serve the React frontend index.html."""
+ index_path = STATIC_DIR / "index.html"
+ if index_path.exists():
+ return FileResponse(str(index_path))
+ return {
+ "message": "Durable Fraud Detection API",
+ "version": "1.0.0",
+ "docs": "/docs",
+ }
+
+
+@app.get("/health")
+async def health():
+ """Health check endpoint."""
+ return {"status": "healthy", "timestamp": datetime.now().isoformat()}
+
+
+@app.get("/api/alerts", response_model=list[AlertInfo])
+async def get_alerts():
+ """Get sample alerts."""
+ return SAMPLE_ALERTS
+
+
+@app.post("/api/workflow/start", response_model=StartWorkflowResponse)
+async def start_workflow(request: StartWorkflowRequest):
+ """Start a new fraud detection orchestration."""
+ client = get_dts_client()
+
+ alert = {
+ "alert_id": request.alert_id,
+ "customer_id": request.customer_id,
+ "alert_type": request.alert_type,
+ "description": request.description,
+ "timestamp": datetime.now().isoformat(),
+ "severity": request.severity,
+ "approval_timeout_hours": request.approval_timeout_hours,
+ }
+
+ instance_id = client.schedule_new_orchestration(
+ ORCHESTRATION_NAME,
+ input=alert,
+ instance_id=f"fraud-{request.alert_id}-{int(time.time())}",
+ )
+
+ logger.info(f"Started orchestration {instance_id} for alert {request.alert_id}")
+
+ return StartWorkflowResponse(
+ instance_id=instance_id,
+ alert_id=request.alert_id,
+ status="started",
+ )
+
+
+@app.get("/api/workflow/status/{instance_id}", response_model=WorkflowStatusResponse)
+async def get_workflow_status(instance_id: str):
+ """Get current status of an orchestration."""
+ client = get_dts_client()
+
+ state = client.get_orchestration_state(instance_id)
+
+ if not state:
+ raise HTTPException(status_code=404, detail="Orchestration not found")
+
+ result = None
+ if state.serialized_output:
+ try:
+ result = json.loads(state.serialized_output)
+ except json.JSONDecodeError:
+ result = {"raw": state.serialized_output}
+
+ return WorkflowStatusResponse(
+ instance_id=instance_id,
+ status=state.runtime_status.name,
+ custom_status=state.serialized_custom_status,
+ result=result,
+ )
+
+
+@app.post("/api/workflow/decision")
+async def submit_decision(request: AnalystDecisionRequest):
+ """Submit analyst decision for a pending orchestration."""
+ client = get_dts_client()
+
+ decision = {
+ "alert_id": request.alert_id,
+ "approved_action": request.approved_action,
+ "analyst_notes": request.analyst_notes,
+ "analyst_id": request.analyst_id,
+ }
+
+ client.raise_orchestration_event(
+ instance_id=request.instance_id,
+ event_name=ANALYST_APPROVAL_EVENT,
+ data=decision,
+ )
+
+ logger.info(f"Submitted decision for {request.instance_id}: {request.approved_action}")
+
+ # Broadcast decision event
+ await manager.broadcast(request.instance_id, {
+ "type": "decision_submitted",
+ "instance_id": request.instance_id,
+ "action": request.approved_action,
+ "timestamp": datetime.now().isoformat(),
+ })
+
+ return {"status": "submitted", "instance_id": request.instance_id}
+
+
+# ============================================================================
+# WebSocket Endpoint
+# ============================================================================
+
+
+@app.websocket("/ws/{instance_id}")
+async def websocket_endpoint(websocket: WebSocket, instance_id: str):
+ """
+ WebSocket endpoint for real-time status updates.
+
+ Connect to /ws/{instance_id} to receive updates for a specific orchestration.
+ """
+ await manager.connect(websocket, instance_id)
+
+ # Start polling for this instance
+ start_status_polling(instance_id)
+
+ try:
+ # Send initial status
+ client = get_dts_client()
+ state = client.get_orchestration_state(instance_id)
+
+ if state:
+ await websocket.send_json({
+ "type": "initial_status",
+ "instance_id": instance_id,
+ "status": state.runtime_status.name,
+ "custom_status": state.serialized_custom_status,
+ })
+
+ # Keep connection alive
+ while True:
+ try:
+ # Wait for client messages (ping/pong, commands, etc.)
+ data = await asyncio.wait_for(websocket.receive_text(), timeout=30)
+
+ # Handle client commands
+ try:
+ message = json.loads(data)
+ if message.get("type") == "ping":
+ await websocket.send_json({"type": "pong"})
+ except json.JSONDecodeError:
+ pass
+
+ except asyncio.TimeoutError:
+ # Send ping to keep connection alive
+ try:
+ await websocket.send_json({"type": "ping"})
+ except Exception:
+ break
+
+ except WebSocketDisconnect:
+ logger.info(f"WebSocket disconnected for {instance_id}")
+ except Exception as e:
+ logger.error(f"WebSocket error for {instance_id}: {e}")
+ finally:
+ manager.disconnect(websocket, instance_id)
+
+
+# ============================================================================
+# Startup/Shutdown
+# ============================================================================
+
+
+@app.on_event("startup")
+async def startup():
+ """Initialize on startup."""
+ logger.info("="*60)
+ logger.info("Starting Durable Fraud Detection Backend")
+ logger.info("="*60)
+
+ # Pre-initialize DTS client
+ try:
+ get_dts_client()
+ logger.info("β DTS client initialized")
+ except Exception as e:
+ logger.error(f"Failed to initialize DTS client: {e}")
+ logger.error("Make sure DTS emulator is running")
+
+ logger.info("Backend ready! π")
+
+
+@app.on_event("shutdown")
+async def shutdown():
+ """Cleanup on shutdown."""
+ logger.info("Shutting down backend...")
+
+ # Cancel polling tasks
+ for task in _polling_tasks.values():
+ task.cancel()
+
+
+# ============================================================================
+# Main
+# ============================================================================
+
+
+if __name__ == "__main__":
+ import uvicorn
+
+ port = int(os.environ.get("BACKEND_PORT", "8002"))
+ uvicorn.run(
+ app,
+ host="0.0.0.0",
+ port=port,
+ log_level="info",
+ )
diff --git a/agentic_ai/workflow/fraud_detection_durable/client.py b/agentic_ai/workflow/fraud_detection_durable/client.py
new file mode 100644
index 000000000..5f4993949
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/client.py
@@ -0,0 +1,398 @@
+"""
+Durable Task Client for Fraud Detection Testing.
+
+This client demonstrates:
+1. Starting a fraud detection orchestration
+2. Monitoring orchestration status
+3. Sending analyst decisions (external events)
+4. Viewing results
+
+Prerequisites:
+- Worker must be running (python worker.py)
+- DTS emulator running on port 8080
+"""
+
+import asyncio
+import json
+import logging
+import os
+import time
+from datetime import datetime
+
+from azure.identity import DefaultAzureCredential
+from dotenv import load_dotenv
+from durabletask.azuremanaged.client import DurableTaskSchedulerClient
+from durabletask.client import OrchestrationState
+
+# Load environment
+load_dotenv()
+
+# Configure logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+# Constants
+ANALYST_APPROVAL_EVENT = "AnalystDecision"
+ORCHESTRATION_NAME = "fraud_detection_orchestration"
+
+
+# ============================================================================
+# Sample Alerts
+# ============================================================================
+
+SAMPLE_ALERTS = {
+ "ALERT-001": {
+ "alert_id": "ALERT-001",
+ "customer_id": 1,
+ "alert_type": "multi_country_login",
+ "description": "Login attempts from USA and Russia within 2 hours",
+ "timestamp": datetime.now().isoformat(),
+ "severity": "high",
+ "approval_timeout_hours": 0.05, # 3 minutes for demo
+ },
+ "ALERT-002": {
+ "alert_id": "ALERT-002",
+ "customer_id": 2,
+ "alert_type": "data_spike",
+ "description": "Data usage increased by 500% in last 24 hours",
+ "timestamp": datetime.now().isoformat(),
+ "severity": "medium",
+ "approval_timeout_hours": 0.05,
+ },
+ "ALERT-003": {
+ "alert_id": "ALERT-003",
+ "customer_id": 3,
+ "alert_type": "unusual_charges",
+ "description": "Three large purchases totaling $5,000 in 10 minutes",
+ "timestamp": datetime.now().isoformat(),
+ "severity": "high",
+ "approval_timeout_hours": 0.05,
+ },
+}
+
+
+# ============================================================================
+# Client Helpers
+# ============================================================================
+
+
+def get_client() -> DurableTaskSchedulerClient:
+ """Create a configured DurableTaskSchedulerClient."""
+ taskhub_name = os.getenv("DTS_TASKHUB", "default")
+ endpoint_url = os.getenv("DTS_ENDPOINT", "http://localhost:8080")
+
+ logger.debug(f"Using DTS endpoint: {endpoint_url}")
+ logger.debug(f"Using taskhub: {taskhub_name}")
+
+ credential = None if endpoint_url.startswith("http://localhost") else DefaultAzureCredential()
+
+ return DurableTaskSchedulerClient(
+ host_address=endpoint_url,
+ secure_channel=not endpoint_url.startswith("http://localhost"),
+ taskhub=taskhub_name,
+ token_credential=credential,
+ )
+
+
+def start_orchestration(client: DurableTaskSchedulerClient, alert: dict) -> str:
+ """Start a fraud detection orchestration."""
+ instance_id = client.schedule_new_orchestration(
+ ORCHESTRATION_NAME,
+ input=alert,
+ instance_id=f"fraud-{alert['alert_id']}-{int(time.time())}",
+ )
+ logger.info(f"Started orchestration with instance ID: {instance_id}")
+ return instance_id
+
+
+def get_status(client: DurableTaskSchedulerClient, instance_id: str) -> OrchestrationState | None:
+ """Get orchestration status."""
+ return client.get_orchestration_state(instance_id)
+
+
+def wait_for_status(
+ client: DurableTaskSchedulerClient,
+ instance_id: str,
+ target_status: str = "RUNNING",
+ timeout: int = 60,
+ poll_interval: float = 1.0,
+) -> OrchestrationState | None:
+ """Wait for orchestration to reach a specific status."""
+ start = time.time()
+ while time.time() - start < timeout:
+ state = get_status(client, instance_id)
+ if state:
+ current_status = state.runtime_status.name
+ custom_status = state.custom_status or ""
+ logger.debug(f"Status: {current_status}, Custom: {custom_status}")
+
+ if current_status == target_status:
+ return state
+
+ if current_status in ("COMPLETED", "FAILED", "TERMINATED"):
+ return state
+
+ time.sleep(poll_interval)
+
+ return None
+
+
+def send_analyst_decision(
+ client: DurableTaskSchedulerClient,
+ instance_id: str,
+ alert_id: str,
+ action: str,
+ notes: str = "",
+ analyst_id: str = "analyst_cli",
+) -> None:
+ """Send analyst decision as external event."""
+ decision = {
+ "alert_id": alert_id,
+ "approved_action": action,
+ "analyst_notes": notes,
+ "analyst_id": analyst_id,
+ }
+
+ logger.info(f"Sending analyst decision: {action}")
+ client.raise_orchestration_event(
+ instance_id=instance_id,
+ event_name=ANALYST_APPROVAL_EVENT,
+ data=decision,
+ )
+ logger.info("Decision sent successfully")
+
+
+def print_result(state: OrchestrationState | None) -> None:
+ """Print orchestration result."""
+ if not state:
+ logger.error("No state available")
+ return
+
+ print(f"\n{'='*60}")
+ print("ORCHESTRATION RESULT")
+ print(f"{'='*60}")
+ print(f"Status: {state.runtime_status.name}")
+ print(f"Custom Status: {state.serialized_custom_status or 'N/A'}")
+
+ if state.serialized_output:
+ try:
+ result = json.loads(state.serialized_output)
+ print(f"Alert ID: {result.get('alert_id')}")
+ print(f"Risk Score: {result.get('risk_score', 'N/A')}")
+ print(f"Action Taken: {result.get('action_taken', 'N/A')}")
+ print(f"Success: {result.get('success', 'N/A')}")
+ except json.JSONDecodeError:
+ print(f"Output: {state.serialized_output}")
+
+ print(f"{'='*60}\n")
+
+
+# ============================================================================
+# Test Scenarios
+# ============================================================================
+
+
+def test_high_risk_with_approval(client: DurableTaskSchedulerClient) -> None:
+ """
+ Test: High-risk alert β Analyst approves β Action executed
+ """
+ print("\n" + "="*70)
+ print("TEST: High-Risk Alert with Analyst Approval")
+ print("="*70)
+
+ alert = SAMPLE_ALERTS["ALERT-001"]
+ print(f"\nAlert: {alert['alert_id']} - {alert['alert_type']}")
+ print(f"Description: {alert['description']}")
+
+ # Start orchestration
+ instance_id = start_orchestration(client, alert)
+
+ # Wait for orchestration to request analyst review
+ print("\nβ³ Waiting for analysis to complete and request analyst review...")
+
+ for _ in range(120): # Wait up to 2 minutes
+ state = get_status(client, instance_id)
+ if state:
+ custom_status = state.serialized_custom_status or ""
+ if "Awaiting analyst review" in custom_status:
+ print(f"β Orchestration is waiting for analyst: {custom_status}")
+ break
+ if state.runtime_status.name in ("COMPLETED", "FAILED"):
+ print(f"Orchestration ended: {state.runtime_status.name}")
+ print_result(state)
+ return
+ time.sleep(1)
+ else:
+ print("β οΈ Timeout waiting for analyst review")
+ return
+
+ # Simulate analyst decision
+ print("\nπ€ Simulating analyst decision: lock_account")
+ time.sleep(2) # Simulate analyst thinking
+
+ send_analyst_decision(
+ client=client,
+ instance_id=instance_id,
+ alert_id=alert["alert_id"],
+ action="lock_account",
+ notes="Confirmed fraudulent activity - locking account",
+ analyst_id="analyst_001",
+ )
+
+ # Wait for completion
+ print("\nβ³ Waiting for orchestration to complete...")
+ state = client.wait_for_orchestration_completion(instance_id, timeout=60)
+ print_result(state)
+
+
+def test_low_risk_auto_clear(client: DurableTaskSchedulerClient) -> None:
+ """
+ Test: Low-risk alert β Auto-cleared (no human review)
+ """
+ print("\n" + "="*70)
+ print("TEST: Low-Risk Alert with Auto-Clear")
+ print("="*70)
+
+ # Modify alert to be low severity
+ alert = {
+ **SAMPLE_ALERTS["ALERT-002"],
+ "severity": "low",
+ "description": "Minor data usage increase - likely legitimate",
+ }
+ print(f"\nAlert: {alert['alert_id']} - {alert['alert_type']}")
+ print(f"Description: {alert['description']}")
+
+ # Start orchestration
+ instance_id = start_orchestration(client, alert)
+
+ # Wait for completion (should auto-clear)
+ print("\nβ³ Waiting for orchestration to complete (should auto-clear)...")
+ state = client.wait_for_orchestration_completion(instance_id, timeout=120)
+ print_result(state)
+
+
+def test_timeout_escalation(client: DurableTaskSchedulerClient) -> None:
+ """
+ Test: High-risk alert β Analyst doesn't respond β Timeout escalation
+ """
+ print("\n" + "="*70)
+ print("TEST: Analyst Timeout Escalation")
+ print("="*70)
+
+ # Use very short timeout for demo
+ alert = {
+ **SAMPLE_ALERTS["ALERT-003"],
+ "approval_timeout_hours": 0.001, # ~3.6 seconds
+ }
+ print(f"\nAlert: {alert['alert_id']} - {alert['alert_type']}")
+ print(f"Description: {alert['description']}")
+ print(f"Timeout: {alert['approval_timeout_hours']*3600:.1f} seconds")
+
+ # Start orchestration
+ instance_id = start_orchestration(client, alert)
+
+ # Wait for completion (will timeout and escalate)
+ print("\nβ³ Waiting for timeout and escalation...")
+ state = client.wait_for_orchestration_completion(instance_id, timeout=180)
+ print_result(state)
+
+
+def interactive_mode(client: DurableTaskSchedulerClient) -> None:
+ """
+ Interactive mode for manual testing.
+ """
+ print("\n" + "="*70)
+ print("INTERACTIVE MODE")
+ print("="*70)
+
+ while True:
+ print("\nOptions:")
+ print("1. Start new orchestration (ALERT-001)")
+ print("2. Start new orchestration (ALERT-002)")
+ print("3. Start new orchestration (ALERT-003)")
+ print("4. Check status of instance")
+ print("5. Send analyst decision")
+ print("6. Exit")
+
+ choice = input("\nChoice: ").strip()
+
+ if choice == "1":
+ instance_id = start_orchestration(client, SAMPLE_ALERTS["ALERT-001"])
+ print(f"Instance ID: {instance_id}")
+
+ elif choice == "2":
+ instance_id = start_orchestration(client, SAMPLE_ALERTS["ALERT-002"])
+ print(f"Instance ID: {instance_id}")
+
+ elif choice == "3":
+ instance_id = start_orchestration(client, SAMPLE_ALERTS["ALERT-003"])
+ print(f"Instance ID: {instance_id}")
+
+ elif choice == "4":
+ instance_id = input("Instance ID: ").strip()
+ state = get_status(client, instance_id)
+ if state:
+ print(f"Status: {state.runtime_status.name}")
+ print(f"Custom: {state.custom_status}")
+ if state.serialized_output:
+ print(f"Output: {state.serialized_output}")
+ else:
+ print("Not found")
+
+ elif choice == "5":
+ instance_id = input("Instance ID: ").strip()
+ alert_id = input("Alert ID: ").strip()
+ action = input("Action (lock_account/refund_charges/clear/both): ").strip()
+ send_analyst_decision(client, instance_id, alert_id, action)
+
+ elif choice == "6":
+ break
+
+
+# ============================================================================
+# Main
+# ============================================================================
+
+
+def main():
+ """Main entry point."""
+ print("\n" + "="*70)
+ print("Durable Fraud Detection Client")
+ print("="*70)
+ print("\nMake sure:")
+ print("1. DTS emulator is running (docker)")
+ print("2. Worker is running (python worker.py)")
+ print("3. MCP server is running")
+ print("")
+
+ client = get_client()
+
+ print("Select test mode:")
+ print("1. Run automated tests")
+ print("2. Interactive mode")
+
+ choice = input("\nChoice: ").strip()
+
+ if choice == "1":
+ print("\nRunning automated tests...")
+
+ # Test 1: High risk with approval
+ test_high_risk_with_approval(client)
+
+ # Test 2: Low risk auto-clear
+ # test_low_risk_auto_clear(client)
+
+ # Test 3: Timeout escalation
+ # test_timeout_escalation(client)
+
+ print("\nβ
All tests completed!")
+
+ elif choice == "2":
+ interactive_mode(client)
+
+ else:
+ print("Invalid choice")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/agentic_ai/workflow/fraud_detection_durable/fraud_analysis_workflow.py b/agentic_ai/workflow/fraud_detection_durable/fraud_analysis_workflow.py
new file mode 100644
index 000000000..7557707da
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/fraud_analysis_workflow.py
@@ -0,0 +1,667 @@
+"""
+Fraud Analysis Workflow - Inner Workflow for Fan-Out β Aggregate Pattern.
+
+This workflow handles the complex multi-agent topology:
+1. AlertRouter fans out to 3 specialist agents
+2. UsagePatternExecutor, LocationAnalysisExecutor, BillingChargeExecutor run in parallel
+3. FraudRiskAggregator collects all results and produces FraudRiskAssessment
+
+This is called as an ACTIVITY from the Durable Task orchestration.
+No human-in-the-loop here - that's handled by the outer Durable Task layer.
+"""
+
+import logging
+from dataclasses import dataclass, field
+from datetime import datetime
+from typing import Any
+
+from pydantic import BaseModel
+
+from agent_framework import (
+ ChatAgent,
+ Executor,
+ handler,
+ WorkflowBuilder,
+ WorkflowContext,
+ MCPStreamableHTTPTool,
+)
+from agent_framework.azure import AzureOpenAIChatClient
+
+logger = logging.getLogger(__name__)
+
+
+# ============================================================================
+# Helper Functions for Tool Call Extraction
+# ============================================================================
+
+
+def extract_tool_calls(response) -> list[dict]:
+ """Extract tool calls from an AgentResponse."""
+ tool_calls = []
+ call_id_to_index = {} # Map call_id to tool_calls index
+
+ for msg in response.messages:
+ for content in msg.contents:
+ if content.type == "function_call":
+ # Parse arguments if they're a string
+ args = getattr(content, "arguments", {})
+ if isinstance(args, str):
+ try:
+ import json
+ args = json.loads(args)
+ except:
+ args = {"raw": args}
+
+ call_id = getattr(content, "call_id", None)
+ idx = len(tool_calls)
+ tool_calls.append({
+ "name": getattr(content, "name", "unknown"),
+ "arguments": args,
+ "result": "",
+ "call_id": call_id,
+ })
+ if call_id:
+ call_id_to_index[call_id] = idx
+
+ elif content.type == "function_result":
+ # Match result to existing call by call_id
+ call_id = getattr(content, "call_id", None)
+ result = getattr(content, "result", None)
+ if call_id and call_id in call_id_to_index:
+ idx = call_id_to_index[call_id]
+ result_str = str(result)[:500] if result else ""
+ tool_calls[idx]["result"] = result_str
+
+ elif content.type == "mcp_server_tool_call":
+ call_id = getattr(content, "call_id", None)
+ idx = len(tool_calls)
+ tool_calls.append({
+ "name": getattr(content, "tool_name", "unknown"),
+ "arguments": getattr(content, "arguments", {}),
+ "result": "",
+ "call_id": call_id,
+ })
+ if call_id:
+ call_id_to_index[call_id] = idx
+
+ elif content.type == "mcp_server_tool_result":
+ call_id = getattr(content, "call_id", None)
+ output = getattr(content, "output", None)
+ if call_id and call_id in call_id_to_index:
+ idx = call_id_to_index[call_id]
+ result_str = str(output)[:500] if output else ""
+ tool_calls[idx]["result"] = result_str
+
+ # Remove call_id from final output (not needed for UI)
+ for tc in tool_calls:
+ tc.pop("call_id", None)
+
+ return tool_calls
+
+
+# ============================================================================
+# Message Types (Pydantic models for type-safe messaging)
+# ============================================================================
+
+
+class ToolCallInfo(BaseModel):
+ """Information about a tool call made during analysis."""
+ name: str
+ arguments: dict = {}
+ result: str = ""
+
+
+class SuspiciousActivityAlert(BaseModel):
+ """Initial alert from monitoring system."""
+ alert_id: str
+ customer_id: int
+ alert_type: str
+ description: str = ""
+ timestamp: str = ""
+ severity: str = "medium"
+
+
+class UsageAnalysisResult(BaseModel):
+ """Result from UsagePatternExecutor."""
+ alert_id: str
+ risk_score: float
+ findings: str
+ data_points: list[str] = []
+ tool_calls: list[ToolCallInfo] = []
+
+
+class LocationAnalysisResult(BaseModel):
+ """Result from LocationAnalysisExecutor."""
+ alert_id: str
+ risk_score: float
+ findings: str
+ locations: list[str] = []
+ tool_calls: list[ToolCallInfo] = []
+
+
+class BillingAnalysisResult(BaseModel):
+ """Result from BillingChargeExecutor."""
+ alert_id: str
+ risk_score: float
+ findings: str
+ charges: list[str] = []
+ tool_calls: list[ToolCallInfo] = []
+
+
+class FraudRiskAssessment(BaseModel):
+ """Final aggregated assessment from FraudRiskAggregator."""
+ alert_id: str
+ customer_id: int
+ overall_risk_score: float
+ risk_level: str # "low", "medium", "high", "critical"
+ recommended_action: str # "clear", "lock_account", "refund_charges", "both"
+ reasoning: str
+ usage_findings: str
+ location_findings: str
+ billing_findings: str
+ # Step details for UI
+ step_details: dict = {}
+
+
+# ============================================================================
+# Executors
+# ============================================================================
+
+
+class AlertRouterExecutor(Executor):
+ """Routes incoming alerts to all specialist executors (fan-out)."""
+
+ def __init__(self):
+ super().__init__(id="alert_router")
+
+ @handler
+ async def handle_alert(
+ self, alert: SuspiciousActivityAlert, ctx: WorkflowContext[SuspiciousActivityAlert]
+ ) -> None:
+ logger.info(f"[AlertRouter] Routing alert {alert.alert_id} to 3 specialist executors")
+
+ # Fan-out: send to all 3 specialist executors
+ # The workflow edges will handle delivery
+ await ctx.send_message(alert, target_id="usage_pattern_executor")
+ await ctx.send_message(alert, target_id="location_analysis_executor")
+ await ctx.send_message(alert, target_id="billing_charge_executor")
+
+
+class UsagePatternExecutor(Executor):
+ """Analyzes data usage patterns using MCP tools."""
+
+ def __init__(self, mcp_tool: MCPStreamableHTTPTool, chat_client: AzureOpenAIChatClient):
+ super().__init__(id="usage_pattern_executor")
+
+ # Filter MCP tools for usage analysis
+ allowed_tools = ["get_customer_detail", "get_subscription_detail", "get_data_usage", "search_knowledge_base"]
+ filtered_functions = [func for func in mcp_tool.functions if func.name in allowed_tools]
+
+ self._agent = ChatAgent(
+ chat_client=chat_client,
+ name="UsagePatternAnalyst",
+ instructions=(
+ "You are a specialist in analyzing customer data usage patterns. "
+ "Look for anomalies like sudden spikes, unusual hours, or patterns inconsistent with history. "
+ "Use the available tools to gather data and provide a risk score (0.0-1.0) and findings."
+ ),
+ tools=filtered_functions,
+ )
+
+ @handler
+ async def handle_alert(
+ self, alert: SuspiciousActivityAlert, ctx: WorkflowContext[UsageAnalysisResult]
+ ) -> None:
+ logger.info(f"[UsagePatternExecutor] Analyzing alert {alert.alert_id}")
+
+ prompt = f"""
+Analyze the usage patterns for customer {alert.customer_id}.
+Alert type: {alert.alert_type}
+Description: {alert.description}
+Severity: {alert.severity}
+
+Use tools to gather usage data, then assess the risk.
+
+Respond in this format:
+FINDINGS: [Your detailed findings]
+RISK_SCORE: [0.0-1.0]
+"""
+
+ response = await self._agent.run(prompt)
+ response_text = response.text if response.text else ""
+
+ # Extract tool calls from response
+ tool_calls_raw = extract_tool_calls(response)
+ tool_calls = [ToolCallInfo(name=tc["name"], arguments=tc.get("arguments", {}), result=tc.get("result", "")) for tc in tool_calls_raw]
+
+ # Parse risk score from LLM response (fallback to severity-based)
+ risk_score = 0.5
+ if "RISK_SCORE:" in response_text:
+ try:
+ score_line = [line for line in response_text.split("\n") if "RISK_SCORE:" in line][0]
+ risk_score = float(score_line.split("RISK_SCORE:")[1].strip())
+ except (IndexError, ValueError):
+ # Fallback based on severity
+ severity_scores = {"low": 0.3, "medium": 0.5, "high": 0.7, "critical": 0.9}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.5)
+ else:
+ severity_scores = {"low": 0.3, "medium": 0.5, "high": 0.7, "critical": 0.9}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.5)
+
+ result = UsageAnalysisResult(
+ alert_id=alert.alert_id,
+ risk_score=risk_score,
+ findings=response_text or "Analysis completed",
+ data_points=[],
+ tool_calls=tool_calls,
+ )
+
+ logger.info(f"[UsagePatternExecutor] Completed analysis, risk_score={result.risk_score}, tools={[tc.name for tc in tool_calls]}")
+ await ctx.send_message(result)
+
+
+class LocationAnalysisExecutor(Executor):
+ """Analyzes geolocation data for anomalies."""
+
+ def __init__(self, mcp_tool: MCPStreamableHTTPTool, chat_client: AzureOpenAIChatClient):
+ super().__init__(id="location_analysis_executor")
+
+ # Filter MCP tools for location analysis
+ allowed_tools = ["get_customer_detail", "get_security_logs", "search_knowledge_base"]
+ filtered_functions = [func for func in mcp_tool.functions if func.name in allowed_tools]
+
+ self._agent = ChatAgent(
+ chat_client=chat_client,
+ name="LocationAnalysisAgent",
+ instructions=(
+ "You are a specialist in analyzing geolocation and security patterns. "
+ "Look for impossible travel, VPN usage, or login anomalies. "
+ "Use the available tools to gather data and provide a risk score (0.0-1.0) and findings."
+ ),
+ tools=filtered_functions,
+ )
+
+ @handler
+ async def handle_alert(
+ self, alert: SuspiciousActivityAlert, ctx: WorkflowContext[LocationAnalysisResult]
+ ) -> None:
+ logger.info(f"[LocationAnalysisExecutor] Analyzing alert {alert.alert_id}")
+
+ prompt = f"""
+Analyze the location and security patterns for customer {alert.customer_id}.
+Alert type: {alert.alert_type}
+Description: {alert.description}
+Severity: {alert.severity}
+
+Use tools to gather security logs and location data, then assess the risk.
+
+Respond in this format:
+FINDINGS: [Your detailed findings]
+RISK_SCORE: [0.0-1.0]
+"""
+
+ response = await self._agent.run(prompt)
+ response_text = response.text if response.text else ""
+
+ # Extract tool calls from response
+ tool_calls_raw = extract_tool_calls(response)
+ tool_calls = [ToolCallInfo(name=tc["name"], arguments=tc.get("arguments", {}), result=tc.get("result", "")) for tc in tool_calls_raw]
+
+ # Parse risk score from LLM response (fallback to severity-based)
+ risk_score = 0.5
+ if "RISK_SCORE:" in response_text:
+ try:
+ score_line = [line for line in response_text.split("\n") if "RISK_SCORE:" in line][0]
+ risk_score = float(score_line.split("RISK_SCORE:")[1].strip())
+ except (IndexError, ValueError):
+ severity_scores = {"low": 0.3, "medium": 0.5, "high": 0.8, "critical": 0.95}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.6)
+ else:
+ severity_scores = {"low": 0.3, "medium": 0.5, "high": 0.8, "critical": 0.95}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.6)
+
+ result = LocationAnalysisResult(
+ alert_id=alert.alert_id,
+ risk_score=risk_score,
+ findings=response_text or "Analysis completed",
+ locations=[],
+ tool_calls=tool_calls,
+ )
+
+ logger.info(f"[LocationAnalysisExecutor] Completed analysis, risk_score={result.risk_score}, tools={[tc.name for tc in tool_calls]}")
+ await ctx.send_message(result)
+
+
+class BillingChargeExecutor(Executor):
+ """Analyzes billing and charge patterns."""
+
+ def __init__(self, mcp_tool: MCPStreamableHTTPTool, chat_client: AzureOpenAIChatClient):
+ super().__init__(id="billing_charge_executor")
+
+ # Filter MCP tools for billing analysis
+ allowed_tools = ["get_customer_detail", "get_billing_summary", "get_subscription_detail",
+ "get_customer_orders", "search_knowledge_base"]
+ filtered_functions = [func for func in mcp_tool.functions if func.name in allowed_tools]
+
+ self._agent = ChatAgent(
+ chat_client=chat_client,
+ name="BillingChargeAnalyst",
+ instructions=(
+ "You are a specialist in analyzing billing and charge patterns. "
+ "Look for unusual purchases, subscription changes, or payment anomalies. "
+ "Use the available tools to gather data and provide a risk score (0.0-1.0) and findings."
+ ),
+ tools=filtered_functions,
+ )
+
+ @handler
+ async def handle_alert(
+ self, alert: SuspiciousActivityAlert, ctx: WorkflowContext[BillingAnalysisResult]
+ ) -> None:
+ logger.info(f"[BillingChargeExecutor] Analyzing alert {alert.alert_id}")
+
+ prompt = f"""
+Analyze the billing and charge patterns for customer {alert.customer_id}.
+Alert type: {alert.alert_type}
+Description: {alert.description}
+Severity: {alert.severity}
+
+Use tools to gather billing data and orders, then assess the risk.
+
+Respond in this format:
+FINDINGS: [Your detailed findings]
+RISK_SCORE: [0.0-1.0]
+"""
+
+ response = await self._agent.run(prompt)
+ response_text = response.text if response.text else ""
+
+ # Extract tool calls from response
+ tool_calls_raw = extract_tool_calls(response)
+ tool_calls = [ToolCallInfo(name=tc["name"], arguments=tc.get("arguments", {}), result=tc.get("result", "")) for tc in tool_calls_raw]
+
+ # Parse risk score from LLM response (fallback to severity-based)
+ risk_score = 0.4
+ if "RISK_SCORE:" in response_text:
+ try:
+ score_line = [line for line in response_text.split("\n") if "RISK_SCORE:" in line][0]
+ risk_score = float(score_line.split("RISK_SCORE:")[1].strip())
+ except (IndexError, ValueError):
+ severity_scores = {"low": 0.2, "medium": 0.4, "high": 0.6, "critical": 0.85}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.4)
+ else:
+ severity_scores = {"low": 0.2, "medium": 0.4, "high": 0.6, "critical": 0.85}
+ risk_score = severity_scores.get(alert.severity.lower(), 0.4)
+
+ result = BillingAnalysisResult(
+ alert_id=alert.alert_id,
+ risk_score=risk_score,
+ findings=response_text or "Analysis completed",
+ charges=[],
+ tool_calls=tool_calls,
+ )
+
+ logger.info(f"[BillingChargeExecutor] Completed analysis, risk_score={result.risk_score}, tools={[tc.name for tc in tool_calls]}")
+ await ctx.send_message(result)
+
+
+# Type alias for fan-in results
+AnalysisResult = UsageAnalysisResult | LocationAnalysisResult | BillingAnalysisResult
+
+
+class FraudRiskAggregatorExecutor(Executor):
+ """Aggregates all analysis results and produces final risk assessment.
+
+ Receives a list of results from fan-in edges (all 3 specialists at once).
+ """
+
+ def __init__(self, chat_client: AzureOpenAIChatClient):
+ super().__init__(id="fraud_risk_aggregator")
+ self._chat_client = chat_client
+
+ @handler
+ async def handle_results(
+ self, results: list[AnalysisResult], ctx: WorkflowContext[None, FraudRiskAssessment]
+ ) -> None:
+ """Handle aggregated results from fan-in (receives list of all 3 results)."""
+ logger.info(f"[Aggregator] Received {len(results)} results for aggregation")
+
+ # Separate results by type
+ usage: UsageAnalysisResult | None = None
+ location: LocationAnalysisResult | None = None
+ billing: BillingAnalysisResult | None = None
+
+ for result in results:
+ if isinstance(result, UsageAnalysisResult):
+ usage = result
+ elif isinstance(result, LocationAnalysisResult):
+ location = result
+ elif isinstance(result, BillingAnalysisResult):
+ billing = result
+
+ if not all([usage, location, billing]):
+ raise ValueError(f"Expected 3 different result types, got: {[type(r).__name__ for r in results]}")
+
+ alert_id = usage.alert_id
+ logger.info(f"[Aggregator] Aggregating results for {alert_id}")
+
+ # Use LLM to synthesize findings
+ agent = ChatAgent(
+ chat_client=self._chat_client,
+ name="FraudRiskAggregator",
+ instructions=(
+ "You are a senior fraud analyst synthesizing findings from specialist agents. "
+ "Weigh the evidence, calculate an overall risk score, and recommend an action. "
+ "Be thorough but decisive. Return a JSON response with the assessment."
+ ),
+ )
+
+ prompt = f"""
+Synthesize these fraud analysis findings for alert {alert_id}:
+
+USAGE ANALYSIS (risk: {usage.risk_score}):
+{usage.findings}
+
+LOCATION ANALYSIS (risk: {location.risk_score}):
+{location.findings}
+
+BILLING ANALYSIS (risk: {billing.risk_score}):
+{billing.findings}
+
+Provide:
+1. Overall risk score (0.0-1.0)
+2. Risk level (low/medium/high/critical)
+3. Recommended action (clear/lock_account/refund_charges/both)
+4. Reasoning (1-2 sentences)
+"""
+
+ response = await agent.run(prompt)
+
+ # Calculate weighted score
+ overall_score = (usage.risk_score * 0.3 + location.risk_score * 0.4 + billing.risk_score * 0.3)
+
+ # Determine risk level
+ if overall_score >= 0.8:
+ risk_level = "critical"
+ elif overall_score >= 0.6:
+ risk_level = "high"
+ elif overall_score >= 0.4:
+ risk_level = "medium"
+ else:
+ risk_level = "low"
+
+ # Determine recommended action
+ if overall_score >= 0.6:
+ recommended_action = "lock_account"
+ else:
+ recommended_action = "clear"
+
+ # Helper to safely extract tool calls (handles both ToolCallInfo and dict)
+ def safe_tool_calls(tool_calls_list):
+ result = []
+ for tc in tool_calls_list:
+ if hasattr(tc, 'model_dump'):
+ result.append(tc.model_dump())
+ elif isinstance(tc, dict):
+ result.append(tc)
+ else:
+ result.append({"name": str(tc), "arguments": {}, "result": ""})
+ return result
+
+ # Build step details for UI
+ step_details = {
+ "usage_pattern_executor": {
+ "status": "completed",
+ "risk_score": usage.risk_score,
+ "tool_calls": safe_tool_calls(usage.tool_calls),
+ "output": usage.findings[:300] if len(usage.findings) > 300 else usage.findings,
+ },
+ "location_analysis_executor": {
+ "status": "completed",
+ "risk_score": location.risk_score,
+ "tool_calls": safe_tool_calls(location.tool_calls),
+ "output": location.findings[:300] if len(location.findings) > 300 else location.findings,
+ },
+ "billing_charge_executor": {
+ "status": "completed",
+ "risk_score": billing.risk_score,
+ "tool_calls": safe_tool_calls(billing.tool_calls),
+ "output": billing.findings[:300] if len(billing.findings) > 300 else billing.findings,
+ },
+ "fraud_risk_aggregator": {
+ "status": "completed",
+ "risk_score": overall_score,
+ "tool_calls": [],
+ "output": f"Aggregated risk: {overall_score:.2f} ({risk_level}). Recommended: {recommended_action}",
+ },
+ }
+
+ assessment = FraudRiskAssessment(
+ alert_id=alert_id,
+ customer_id=0, # Would be extracted from original alert
+ overall_risk_score=overall_score,
+ risk_level=risk_level,
+ recommended_action=recommended_action,
+ reasoning=response.text if response.text else "Based on aggregated analysis",
+ usage_findings=usage.findings,
+ location_findings=location.findings,
+ billing_findings=billing.findings,
+ step_details=step_details,
+ )
+
+ logger.info(f"[Aggregator] Assessment complete: risk={overall_score:.2f}, action={recommended_action}")
+ await ctx.yield_output(assessment)
+
+
+# ============================================================================
+# Workflow Builder
+# ============================================================================
+
+
+def create_fraud_analysis_workflow(
+ mcp_tool: MCPStreamableHTTPTool,
+ chat_client: AzureOpenAIChatClient,
+) -> Any:
+ """
+ Create the inner fraud analysis workflow.
+
+ This workflow handles the fan-out β aggregate pattern:
+ - AlertRouter fans out to 3 specialist agents
+ - Specialists run in parallel with MCP tools
+ - Aggregator waits for all 3 and produces FraudRiskAssessment
+
+ Returns:
+ Workflow: The built workflow ready to run
+ """
+ logger.info("[Workflow] Building fraud analysis workflow...")
+
+ # Create executors
+ alert_router = AlertRouterExecutor()
+ usage_executor = UsagePatternExecutor(mcp_tool, chat_client)
+ location_executor = LocationAnalysisExecutor(mcp_tool, chat_client)
+ billing_executor = BillingChargeExecutor(mcp_tool, chat_client)
+ aggregator = FraudRiskAggregatorExecutor(chat_client)
+
+ # Build workflow topology
+ builder = WorkflowBuilder()
+
+ # Set entry point
+ builder.set_start_executor(alert_router)
+
+ # Fan-out: Alert Router β 3 Specialists
+ builder.add_edge(alert_router, usage_executor)
+ builder.add_edge(alert_router, location_executor)
+ builder.add_edge(alert_router, billing_executor)
+
+ # Fan-in: 3 Specialists β Aggregator
+ builder.add_fan_in_edges(
+ [usage_executor, location_executor, billing_executor],
+ aggregator
+ )
+
+ workflow = builder.build()
+ logger.info("[Workflow] Fraud analysis workflow built successfully")
+
+ return workflow
+
+
+# ============================================================================
+# Standalone Test
+# ============================================================================
+
+
+async def main():
+ """Test the workflow standalone (without Durable Task)."""
+ import asyncio
+ import os
+ from dotenv import load_dotenv
+ from azure.identity import AzureCliCredential
+
+ load_dotenv()
+
+ # Initialize MCP tool
+ mcp_uri = os.getenv("MCP_SERVER_URI", "http://localhost:8000/mcp")
+ mcp_tool = MCPStreamableHTTPTool(name="contoso_mcp", url=mcp_uri, timeout=30)
+
+ async with mcp_tool:
+ # Initialize chat client
+ chat_client = AzureOpenAIChatClient(
+ credential=AzureCliCredential(),
+ deployment_name=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o"),
+ )
+
+ # Create workflow
+ workflow = create_fraud_analysis_workflow(mcp_tool, chat_client)
+
+ # Test alert
+ alert = SuspiciousActivityAlert(
+ alert_id="TEST-001",
+ customer_id=1,
+ alert_type="multi_country_login",
+ description="Login attempts from USA and Russia within 2 hours",
+ timestamp=datetime.now().isoformat(),
+ severity="high",
+ )
+
+ print(f"\n{'='*60}")
+ print(f"Running Fraud Analysis Workflow for Alert: {alert.alert_id}")
+ print(f"{'='*60}\n")
+
+ # Run workflow
+ async for event in workflow.run_stream(alert):
+ print(f"Event: {type(event).__name__}")
+ if hasattr(event, 'data') and isinstance(event.data, FraudRiskAssessment):
+ assessment = event.data
+ print(f"\n{'='*60}")
+ print("FRAUD RISK ASSESSMENT")
+ print(f"{'='*60}")
+ print(f"Alert ID: {assessment.alert_id}")
+ print(f"Risk Score: {assessment.overall_risk_score:.2f}")
+ print(f"Risk Level: {assessment.risk_level}")
+ print(f"Recommended Action: {assessment.recommended_action}")
+ print(f"Reasoning: {assessment.reasoning}")
+
+
+if __name__ == "__main__":
+ import asyncio
+ asyncio.run(main())
diff --git a/agentic_ai/workflow/fraud_detection_durable/pyproject.toml b/agentic_ai/workflow/fraud_detection_durable/pyproject.toml
new file mode 100644
index 000000000..17b502ae0
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/pyproject.toml
@@ -0,0 +1,34 @@
+[project]
+name = "fraud-detection-durable"
+version = "0.1.0"
+description = "Durable Fraud Detection Workflow - Hybrid Workflow + Durable Task Architecture"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ # Agent Framework (includes azure sub-package)
+ "agent-framework==1.0.0b260130",
+
+ # Observability
+ "azure-monitor-opentelemetry>=1.8.5",
+
+ # Durable Task Scheduler
+ "durabletask-azuremanaged>=1.0.0a1",
+
+ # FastAPI Backend
+ "fastapi==0.115.12",
+ "uvicorn>=0.25.0",
+ "websockets>=15.0.1",
+
+ # Azure & Auth
+ "azure-identity>=1.15.0",
+
+ # HTTP & Tools
+ "httpx==0.28.1",
+ "pydantic==2.11.4",
+
+ # Environment
+ "python-dotenv>=1.0.0",
+]
+
+[tool.uv]
+prerelease = "allow"
diff --git a/agentic_ai/workflow/fraud_detection_durable/start.sh b/agentic_ai/workflow/fraud_detection_durable/start.sh
new file mode 100644
index 000000000..e4f4472b7
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/start.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# Start both worker and backend processes
+
+echo "Starting Fraud Detection Durable Workflow..."
+echo "DTS Endpoint: $DTS_ENDPOINT"
+
+# Start worker in background
+echo "Starting worker..."
+python worker.py &
+WORKER_PID=$!
+
+# Wait a bit for worker to connect to DTS
+sleep 5
+
+# Start backend in foreground
+echo "Starting backend on port ${BACKEND_PORT:-8002}..."
+python backend.py
+
+# If backend exits, kill worker
+kill $WORKER_PID 2>/dev/null
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.dockerignore b/agentic_ai/workflow/fraud_detection_durable/ui/.dockerignore
new file mode 100644
index 000000000..778a2ee8b
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.dockerignore
@@ -0,0 +1,30 @@
+# Development dependencies and build artifacts
+node_modules/
+dist/
+build/
+
+# Logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# Environment files
+.env
+.env.local
+.env.*.local
+
+# IDE
+.vscode/
+.idea/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Test coverage
+coverage/
+
+# Temporary files
+*.tmp
+*.temp
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.env.example b/agentic_ai/workflow/fraud_detection_durable/ui/.env.example
new file mode 100644
index 000000000..02c3939e0
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.env.example
@@ -0,0 +1,6 @@
+# API Configuration
+VITE_API_BASE_URL=http://localhost:8001
+VITE_WS_URL=ws://localhost:8001/ws
+
+# App Configuration
+VITE_APP_TITLE=Fraud Detection Workflow Visualizer
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.eslintrc.cjs b/agentic_ai/workflow/fraud_detection_durable/ui/.eslintrc.cjs
new file mode 100644
index 000000000..7617f77ca
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.eslintrc.cjs
@@ -0,0 +1,22 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:react/recommended',
+ 'plugin:react/jsx-runtime',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+ settings: { react: { version: '19.0' } },
+ plugins: ['react-refresh'],
+ rules: {
+ 'react/jsx-no-target-blank': 'off',
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ 'react/prop-types': 'warn',
+ },
+}
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.gitignore b/agentic_ai/workflow/fraud_detection_durable/ui/.gitignore
new file mode 100644
index 000000000..aa674a799
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.gitignore
@@ -0,0 +1,31 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Environment variables
+.env
+.env.local
+.env.production.local
+.env.development.local
+.env.test.local
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.prettierignore b/agentic_ai/workflow/fraud_detection_durable/ui/.prettierignore
new file mode 100644
index 000000000..8c444c5a1
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.prettierignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+build
+.env
+.env.local
+*.log
+coverage
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.prettierrc.cjs b/agentic_ai/workflow/fraud_detection_durable/ui/.prettierrc.cjs
new file mode 100644
index 000000000..9f6715a36
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.prettierrc.cjs
@@ -0,0 +1,14 @@
+// Prettier Configuration
+module.exports = {
+ semi: true,
+ trailingComma: 'es5',
+ singleQuote: true,
+ printWidth: 100,
+ tabWidth: 2,
+ useTabs: false,
+ arrowParens: 'always',
+ endOfLine: 'lf',
+ jsxSingleQuote: false,
+ bracketSpacing: true,
+ jsxBracketSameLine: false,
+};
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/.vscode/extensions.json b/agentic_ai/workflow/fraud_detection_durable/ui/.vscode/extensions.json
new file mode 100644
index 000000000..82a944c17
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode",
+ "ms-vscode.vscode-typescript-next"
+ ]
+}
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile b/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile
new file mode 100644
index 000000000..70e074f86
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/Dockerfile
@@ -0,0 +1,65 @@
+# Build stage
+FROM node:22-alpine AS builder
+
+# Set working directory
+WORKDIR /app
+
+# Add metadata
+LABEL maintainer="OpenAI Workshop Team"
+LABEL description="Fraud Detection UI - React + Vite Application"
+
+# Copy package files
+COPY package*.json ./
+
+# Install dependencies with clean install for reproducible builds
+RUN npm ci --only=production=false && \
+ npm cache clean --force
+
+# Copy application source
+COPY . .
+
+# Build the application
+RUN npm run build
+
+# Production stage
+FROM node:22-alpine AS production
+
+# Install dumb-init for proper signal handling
+RUN apk add --no-cache dumb-init
+
+# Set working directory
+WORKDIR /app
+
+# Create a non-root user
+RUN addgroup -g 1001 -S nodejs && \
+ adduser -S nodejs -u 1001
+
+# Install serve globally
+RUN npm install -g serve
+
+# Copy built assets from builder stage
+COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
+
+# Copy startup script that injects runtime config
+COPY --chown=nodejs:nodejs docker-entrypoint.sh /app/docker-entrypoint.sh
+RUN chmod +x /app/docker-entrypoint.sh
+
+# Switch to non-root user
+USER nodejs
+
+# Environment variables for runtime configuration
+ENV API_BASE_URL=""
+ENV WS_URL=""
+
+# Expose port
+EXPOSE 3000
+
+# Health check
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1
+
+# Use dumb-init to handle signals properly
+ENTRYPOINT ["dumb-init", "--"]
+
+# Run startup script that injects config, then serves the app
+CMD ["/app/docker-entrypoint.sh"]
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/README.md b/agentic_ai/workflow/fraud_detection_durable/ui/README.md
new file mode 100644
index 000000000..9a785b969
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/README.md
@@ -0,0 +1,247 @@
+# Fraud Detection Workflow Visualizer
+
+A modern React application for visualizing and managing fraud detection workflows in real-time. Built with Vite, React, Material-UI, and React Flow.
+
+## Features
+
+- π Real-time workflow visualization
+- π― Interactive workflow controls
+- π Live event logging
+- π€ Human-in-the-loop decision making
+- π WebSocket-based real-time updates
+- π¨ Material-UI based responsive design
+- π React Flow powered workflow graphs
+
+## Tech Stack
+
+- **Framework**: React 19.2
+- **Build Tool**: Vite 7.2
+- **UI Library**: Material-UI (MUI) 7.3
+- **Graph Visualization**: React Flow 11.11
+- **State Management**: React Hooks
+- **Real-time Communication**: WebSocket
+
+## Prerequisites
+
+- Node.js 18+ or newer
+- npm 9+ or newer
+
+## Getting Started
+
+### Installation
+
+1. Clone the repository
+2. Navigate to the project directory:
+
+ ```bash
+ cd agentic_ai/workflow/fraud_detection/ui
+ ```
+
+3. Install dependencies:
+
+ ```bash
+ npm install
+ ```
+
+4. Copy the environment file:
+
+ ```bash
+ cp .env.example .env
+ ```
+
+5. Configure your environment variables in `.env` if needed
+
+### Development
+
+Start the development server:
+
+```bash
+npm run dev
+```
+
+The application will open at `http://localhost:3000`
+
+### Build
+
+Build for production:
+
+```bash
+npm run build
+```
+
+Preview the production build:
+
+```bash
+npm run preview
+```
+
+### Docker
+
+Build and run the application in a Docker container:
+
+```bash
+# Build the Docker image
+docker build -t fraud-detection-ui:latest .
+
+# Run the container
+docker run -p 3000:3000 fraud-detection-ui:latest
+
+# Run with custom backend URL
+docker run -p 3000:3000 \
+ -e VITE_API_BASE_URL=http://your-backend:8001 \
+ -e VITE_WS_URL=ws://your-backend:8001/ws \
+ fraud-detection-ui:latest
+```
+
+The Docker image uses a multi-stage build with:
+
+- **Build stage**: Node.js 22 Alpine with all dependencies
+- **Production stage**: Node.js 22 Alpine serving with `serve`
+- **Security**: Runs as non-root user (nodejs:1001)
+- **Port**: Exposes port 3000
+- **Health check**: Built-in health monitoring
+
+### Linting
+
+Run ESLint:
+
+```bash
+npm run lint
+```
+
+Fix linting issues automatically:
+
+```bash
+npm run lint:fix
+```
+
+## Project Structure
+
+```text
+ui/
+βββ src/
+β βββ components/ # React components
+β β βββ AnalystDecisionPanel.jsx
+β β βββ ControlPanel.jsx
+β β βββ CustomNode.jsx
+β β βββ EventLog.jsx
+β β βββ WorkflowVisualizer.jsx
+β βββ hooks/ # Custom React hooks
+β β βββ useWebSocket.js
+β βββ utils/ # Utility functions
+β β βββ api.js
+β β βββ helpers.js
+β β βββ uiHelpers.jsx
+β βββ constants/ # Application constants
+β β βββ config.js
+β β βββ workflow.js
+β βββ theme/ # MUI theme configuration
+β β βββ index.js
+β βββ App.jsx # Main application component
+β βββ main.jsx # Application entry point
+β βββ index.css # Global styles
+βββ public/ # Static assets
+βββ .env # Environment variables
+βββ .env.example # Environment variables example
+βββ .eslintrc.cjs # ESLint configuration
+βββ .gitignore # Git ignore rules
+βββ index.html # HTML template
+βββ jsconfig.json # JavaScript configuration
+βββ package.json # Project dependencies
+βββ vite.config.js # Vite configuration
+βββ README.md # This file
+```
+
+## Path Aliases
+
+The project uses path aliases for cleaner imports:
+
+- `@/` β `src/`
+- `@components/` β `src/components/`
+- `@hooks/` β `src/hooks/`
+- `@utils/` β `src/utils/`
+- `@theme/` β `src/theme/`
+- `@constants/` β `src/constants/`
+
+Example:
+
+```javascript
+import WorkflowVisualizer from '@components/WorkflowVisualizer';
+import { useWebSocket } from '@hooks/useWebSocket';
+import theme from '@theme';
+```
+
+## Environment Variables
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `VITE_API_BASE_URL` | Backend API URL | `http://localhost:8001` |
+| `VITE_WS_URL` | WebSocket URL | `ws://localhost:8001/ws` |
+| `VITE_APP_TITLE` | Application title | `Fraud Detection Workflow Visualizer` |
+
+## API Integration
+
+The application connects to a backend API running on `http://localhost:8001` by default. The following endpoints are used:
+
+- `GET /api/alerts` - Fetch available alerts
+- `POST /api/workflow/start` - Start a workflow
+- `POST /api/workflow/decision` - Submit analyst decision
+- `WS /ws` - WebSocket connection for real-time updates
+
+## Components
+
+### Main Components
+
+- **App.jsx** - Main application component, orchestrates all child components
+- **WorkflowVisualizer** - Displays the workflow graph with real-time state updates
+- **ControlPanel** - Alert selection and workflow control interface
+- **AnalystDecisionPanel** - Human-in-the-loop decision making interface
+- **EventLog** - Real-time event logging display
+- **CustomNode** - Custom node component for workflow graph
+
+### Hooks
+
+- **useWebSocket** - Manages WebSocket connections with automatic reconnection
+
+## Development Guidelines
+
+### Code Style
+
+- Use functional components with hooks
+- Follow ESLint rules
+- Use path aliases for imports
+- Add JSDoc comments for functions
+- Keep components small and focused
+
+### Adding New Components
+
+1. Create component in `src/components/`
+2. Export from the component file
+3. Import using path alias: `import Component from '@components/Component'`
+
+### Adding New Constants
+
+1. Add constants to appropriate file in `src/constants/`
+2. Export named exports
+3. Import where needed: `import { CONSTANT } from '@constants/config'`
+
+## Troubleshooting
+
+### WebSocket Connection Issues
+
+- Ensure backend server is running
+- Check `VITE_WS_URL` in `.env`
+- Check browser console for connection errors
+
+### Build Issues
+
+- Clear node_modules and reinstall: `rm -rf node_modules && npm install`
+- Clear Vite cache: `rm -rf node_modules/.vite`
+
+## License
+
+See the main project LICENSE file.
+
+## Contributing
+
+Please read the main project CONTRIBUTING guidelines.
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh b/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh
new file mode 100644
index 000000000..62e9d52f5
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/docker-entrypoint.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+# Docker entrypoint script that injects runtime configuration into the React app
+
+set -e
+
+# Generate runtime config script
+CONFIG_SCRIPT="window.__CONFIG__ = {"
+
+if [ -n "$API_BASE_URL" ]; then
+ CONFIG_SCRIPT="${CONFIG_SCRIPT} API_BASE_URL: '${API_BASE_URL}',"
+fi
+
+if [ -n "$WS_URL" ]; then
+ CONFIG_SCRIPT="${CONFIG_SCRIPT} WS_URL: '${WS_URL}',"
+fi
+
+CONFIG_SCRIPT="${CONFIG_SCRIPT} };"
+
+echo "Injecting runtime config: $CONFIG_SCRIPT"
+
+# Create a config.js file in the dist folder
+echo "$CONFIG_SCRIPT" > /app/dist/config.js
+
+# Inject script tag into index.html if not already present
+if ! grep -q 'config.js' /app/dist/index.html; then
+ sed -i 's|||' /app/dist/index.html
+fi
+
+# Start the server
+exec serve -s dist -l 3000
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/index.html b/agentic_ai/workflow/fraud_detection_durable/ui/index.html
new file mode 100644
index 000000000..fcdac8966
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ Fraud Detection Workflow Visualizer
+
+
+
+
+
+
+
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json b/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json
new file mode 100644
index 000000000..40cfd0e81
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/package-lock.json
@@ -0,0 +1,5893 @@
+{
+ "name": "fraud-detection-ui",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "fraud-detection-ui",
+ "version": "1.0.0",
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mui/icons-material": "^7.3.6",
+ "@mui/material": "^7.3.6",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
+ "reactflow": "^11.11.4"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^5.1.2",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "vite": "^7.2.7"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.14.1",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
+ "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz",
+ "integrity": "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ }
+ },
+ "node_modules/@mui/icons-material": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.3.6.tgz",
+ "integrity": "sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@mui/material": "^7.3.6",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.6.tgz",
+ "integrity": "sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/core-downloads-tracker": "^7.3.6",
+ "@mui/system": "^7.3.6",
+ "@mui/types": "^7.4.9",
+ "@mui/utils": "^7.3.6",
+ "@popperjs/core": "^2.11.8",
+ "@types/react-transition-group": "^4.4.12",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material-pigment-css": "^7.3.6",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@mui/material-pigment-css": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.6.tgz",
+ "integrity": "sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/utils": "^7.3.6",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.6.tgz",
+ "integrity": "sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/sheet": "^1.4.0",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.6.tgz",
+ "integrity": "sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/private-theming": "^7.3.6",
+ "@mui/styled-engine": "^7.3.6",
+ "@mui/types": "^7.4.9",
+ "@mui/utils": "^7.3.6",
+ "clsx": "^2.1.1",
+ "csstype": "^3.1.3",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.4.9",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.9.tgz",
+ "integrity": "sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.6.tgz",
+ "integrity": "sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.28.4",
+ "@mui/types": "^7.4.9",
+ "@types/prop-types": "^15.7.15",
+ "clsx": "^2.1.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^19.2.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@reactflow/background": {
+ "version": "11.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
+ "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/controls": {
+ "version": "11.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz",
+ "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/core": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz",
+ "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3": "^7.4.0",
+ "@types/d3-drag": "^3.0.1",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/minimap": {
+ "version": "11.7.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz",
+ "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "@types/d3-selection": "^3.0.3",
+ "@types/d3-zoom": "^3.0.1",
+ "classcat": "^5.0.3",
+ "d3-selection": "^3.0.0",
+ "d3-zoom": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-resizer": {
+ "version": "2.2.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz",
+ "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.4",
+ "d3-drag": "^3.0.0",
+ "d3-selection": "^3.0.0",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@reactflow/node-toolbar": {
+ "version": "1.3.14",
+ "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz",
+ "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/core": "11.11.4",
+ "classcat": "^5.0.3",
+ "zustand": "^4.4.1"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/d3": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/d3-axis": "*",
+ "@types/d3-brush": "*",
+ "@types/d3-chord": "*",
+ "@types/d3-color": "*",
+ "@types/d3-contour": "*",
+ "@types/d3-delaunay": "*",
+ "@types/d3-dispatch": "*",
+ "@types/d3-drag": "*",
+ "@types/d3-dsv": "*",
+ "@types/d3-ease": "*",
+ "@types/d3-fetch": "*",
+ "@types/d3-force": "*",
+ "@types/d3-format": "*",
+ "@types/d3-geo": "*",
+ "@types/d3-hierarchy": "*",
+ "@types/d3-interpolate": "*",
+ "@types/d3-path": "*",
+ "@types/d3-polygon": "*",
+ "@types/d3-quadtree": "*",
+ "@types/d3-random": "*",
+ "@types/d3-scale": "*",
+ "@types/d3-scale-chromatic": "*",
+ "@types/d3-selection": "*",
+ "@types/d3-shape": "*",
+ "@types/d3-time": "*",
+ "@types/d3-time-format": "*",
+ "@types/d3-timer": "*",
+ "@types/d3-transition": "*",
+ "@types/d3-zoom": "*"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-axis": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-brush": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-chord": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-contour": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-array": "*",
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-dispatch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-drag": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-dsv": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-polygon": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-quadtree": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
+ "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-time-format": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-transition": {
+ "version": "3.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/d3-zoom": {
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.7",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
+ "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.12",
+ "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
+ "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.9",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
+ "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.0",
+ "es-object-atoms": "^1.1.1",
+ "get-intrinsic": "^1.3.0",
+ "is-string": "^1.1.1",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz",
+ "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz",
+ "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz",
+ "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
+ "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.5",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz",
+ "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001760",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
+ "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/classcat": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
+ "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==",
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
+ "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz",
+ "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz",
+ "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz",
+ "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.0.5",
+ "is-symbol": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
+ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.1",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
+ "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
+ "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz",
+ "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "functions-have-names": "^1.2.3",
+ "hasown": "^2.0.2",
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz",
+ "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz",
+ "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz",
+ "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz",
+ "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
+ "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
+ "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.1.tgz",
+ "integrity": "sha512-L7BnWgRbMwzMAubQcS7sXdPdNLmKlucPlopgAzx7FtYbksWZgEWiuYM5x9T6UqS2Ne0rsgQTq5kY2SGqpzUkYA==",
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/reactflow": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz",
+ "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@reactflow/background": "11.3.14",
+ "@reactflow/controls": "11.2.14",
+ "@reactflow/core": "11.11.4",
+ "@reactflow/minimap": "11.7.14",
+ "@reactflow/node-resizer": "2.2.14",
+ "@reactflow/node-toolbar": "1.3.14"
+ },
+ "peerDependencies": {
+ "react": ">=17",
+ "react-dom": ">=17"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
+ "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz",
+ "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+ "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz",
+ "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-object-atoms": "^1.0.0",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz",
+ "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.2",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz",
+ "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0",
+ "reflect.getprototypeof": "^1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
+ "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.2.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
+ "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
+ "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
+ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/package.json b/agentic_ai/workflow/fraud_detection_durable/ui/package.json
new file mode 100644
index 000000000..77e904866
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "fraud-detection-ui",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
+ "lint:fix": "eslint . --ext js,jsx --fix"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mui/icons-material": "^7.3.6",
+ "@mui/material": "^7.3.6",
+ "react": "^19.2.1",
+ "react-dom": "^19.2.1",
+ "reactflow": "^11.11.4"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^5.1.2",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "vite": "^7.2.7"
+ }
+}
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx
new file mode 100644
index 000000000..1c8421430
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/App.jsx
@@ -0,0 +1,391 @@
+import React, { useState, useCallback, useEffect, useRef } from 'react';
+import {
+ Box,
+ ThemeProvider,
+ createTheme,
+ CssBaseline,
+ AppBar,
+ Toolbar,
+ Typography,
+ Container,
+ Paper,
+ Grid,
+ Chip,
+} from '@mui/material';
+import SecurityIcon from '@mui/icons-material/Security';
+import CloudSyncIcon from '@mui/icons-material/CloudSync';
+import WorkflowVisualizer from './components/WorkflowVisualizer';
+import { API_CONFIG } from './constants/config';
+import ControlPanel from './components/ControlPanel';
+import AnalystDecisionPanel from './components/AnalystDecisionPanel';
+
+const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ },
+ secondary: {
+ main: '#dc004e',
+ },
+ success: {
+ main: '#4caf50',
+ },
+ warning: {
+ main: '#ff9800',
+ },
+ error: {
+ main: '#f44336',
+ },
+ },
+ typography: {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ },
+});
+
+function App() {
+ const [alerts, setAlerts] = useState([]);
+ const [selectedAlert, setSelectedAlert] = useState(null);
+ const [workflowRunning, setWorkflowRunning] = useState(false);
+ const [events, setEvents] = useState([]);
+ const [pendingDecision, setPendingDecision] = useState(null);
+ const [executorStates, setExecutorStates] = useState({});
+ const [instanceId, setInstanceId] = useState(null);
+ const [orchestrationStatus, setOrchestrationStatus] = useState(null);
+ const [stepDetails, setStepDetails] = useState({});
+
+ const ws = useRef(null);
+
+ // Load sample alerts on mount
+ useEffect(() => {
+ fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.ALERTS}`)
+ .then((res) => res.json())
+ .then((data) => setAlerts(data))
+ .catch((err) => console.error('Error loading alerts:', err));
+ }, []);
+
+ const addEvent = useCallback((event) => {
+ setEvents((prev) => [...prev, { ...event, timestamp: event.timestamp || new Date().toISOString() }]);
+ }, []);
+
+ // Connect to WebSocket when we have an instance ID
+ useEffect(() => {
+ if (!instanceId) return;
+
+ const wsUrl = `${API_CONFIG.WS_URL}/${instanceId}`;
+ console.log('Connecting to WebSocket:', wsUrl);
+
+ ws.current = new WebSocket(wsUrl);
+
+ ws.current.onopen = () => {
+ console.log('WebSocket connected for instance:', instanceId);
+ addEvent({ type: 'websocket_connected', message: `Connected to orchestration ${instanceId}` });
+ };
+
+ ws.current.onmessage = (event) => {
+ try {
+ const data = JSON.parse(event.data);
+ console.log('WebSocket message:', data);
+ handleWebSocketMessage(data);
+ } catch (error) {
+ console.error('Error parsing WebSocket message:', error);
+ }
+ };
+
+ ws.current.onclose = () => {
+ console.log('WebSocket disconnected');
+ };
+
+ ws.current.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ };
+
+ return () => {
+ if (ws.current) {
+ ws.current.close();
+ }
+ };
+ }, [instanceId]);
+
+ const handleWebSocketMessage = useCallback((data) => {
+ // Skip ping/pong messages
+ if (data.type === 'ping' || data.type === 'pong') return;
+
+ // Add to event log
+ addEvent(data);
+
+ // Handle different message types
+ if (data.type === 'status_update' || data.type === 'initial_status') {
+ setOrchestrationStatus(data.status);
+
+ // Update step details from backend (real tool calls and outputs)
+ if (data.step_details) {
+ setStepDetails(data.step_details);
+ }
+
+ // Map orchestration status to executor states based on custom_status
+ const customStatus = data.custom_status || '';
+
+ if (customStatus.includes('Running fraud analysis')) {
+ // Workflow is running - show specialist agents as active
+ setExecutorStates({
+ alert_router: 'completed',
+ usage_pattern_executor: 'running',
+ location_analysis_executor: 'running',
+ billing_charge_executor: 'running',
+ });
+ } else if (customStatus.includes('Awaiting analyst')) {
+ // Waiting for analyst - aggregation complete, waiting for human
+ setExecutorStates({
+ alert_router: 'completed',
+ usage_pattern_executor: 'completed',
+ location_analysis_executor: 'completed',
+ billing_charge_executor: 'completed',
+ fraud_risk_aggregator: 'completed',
+ review_gateway: 'running',
+ });
+ } else if (customStatus.includes('Executing')) {
+ // Executing analyst-approved action
+ setExecutorStates((prev) => ({
+ ...prev,
+ review_gateway: 'completed',
+ fraud_action_executor: 'running',
+ }));
+ } else if (customStatus.includes('Auto-clearing')) {
+ // Auto-clearing low risk
+ setExecutorStates({
+ alert_router: 'completed',
+ usage_pattern_executor: 'completed',
+ location_analysis_executor: 'completed',
+ billing_charge_executor: 'completed',
+ fraud_risk_aggregator: 'completed',
+ auto_clear_executor: 'running',
+ });
+ } else if (customStatus.includes('Sending notification') || customStatus.includes('Sending')) {
+ // Sending final notification
+ setExecutorStates((prev) => ({
+ ...prev,
+ fraud_action_executor: prev.fraud_action_executor === 'running' ? 'completed' : prev.fraud_action_executor,
+ auto_clear_executor: prev.auto_clear_executor === 'running' ? 'completed' : prev.auto_clear_executor,
+ final_notification_executor: 'running',
+ }));
+ } else if (customStatus.includes('Completed') || data.status === 'COMPLETED') {
+ // Completed - mark everything as done
+ setExecutorStates((prev) => ({
+ ...prev,
+ fraud_action_executor: prev.fraud_action_executor === 'running' ? 'completed' : prev.fraud_action_executor,
+ auto_clear_executor: prev.auto_clear_executor === 'running' ? 'completed' : prev.auto_clear_executor,
+ final_notification_executor: 'completed',
+ }));
+ }
+
+ // Check if decision is required
+ if (data.decision_required) {
+ setPendingDecision({
+ instance_id: instanceId,
+ alert_id: selectedAlert?.alert_id,
+ customer_id: selectedAlert?.customer_id,
+ });
+ setWorkflowRunning(false);
+ }
+
+ // Check if completed
+ if (data.status === 'COMPLETED' || data.status === 'FAILED') {
+ setWorkflowRunning(false);
+
+ // Determine which path was taken from the result
+ const actionTaken = data.result?.action_taken || '';
+ const riskScore = data.result?.risk_score || 0;
+
+ // Update step details from result if available
+ if (data.result?.step_details) {
+ setStepDetails(data.result.step_details);
+ }
+
+ // Set the complete final state based on the path taken
+ if (actionTaken === 'auto_clear' || riskScore < 0.6) {
+ // Low risk path: auto-clear
+ setExecutorStates({
+ alert_router: 'completed',
+ usage_pattern_executor: 'completed',
+ location_analysis_executor: 'completed',
+ billing_charge_executor: 'completed',
+ fraud_risk_aggregator: 'completed',
+ auto_clear_executor: 'completed',
+ final_notification_executor: 'completed',
+ });
+ } else {
+ // High risk path: review gateway β fraud action
+ setExecutorStates({
+ alert_router: 'completed',
+ usage_pattern_executor: 'completed',
+ location_analysis_executor: 'completed',
+ billing_charge_executor: 'completed',
+ fraud_risk_aggregator: 'completed',
+ review_gateway: 'completed',
+ fraud_action_executor: 'completed',
+ final_notification_executor: 'completed',
+ });
+ }
+ }
+
+ // Handle result
+ if (data.result) {
+ addEvent({ type: 'workflow_result', ...data.result });
+ }
+ }
+
+ if (data.type === 'decision_submitted') {
+ setPendingDecision(null);
+ setWorkflowRunning(true);
+ setExecutorStates((prev) => ({
+ ...prev,
+ review_gateway: 'completed',
+ fraud_action_executor: 'running',
+ }));
+ }
+ }, [instanceId, selectedAlert, addEvent]);
+
+ const handleStartWorkflow = useCallback(async (alert) => {
+ console.log('Starting workflow for alert:', alert);
+ setSelectedAlert(alert);
+ setWorkflowRunning(true);
+ setEvents([]);
+ setExecutorStates({ alert_router: 'running' });
+ setPendingDecision(null);
+ setInstanceId(null);
+ setOrchestrationStatus(null);
+ setStepDetails({}); // Reset step details for new workflow
+
+ try {
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.WORKFLOW_START}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ alert_id: alert.alert_id,
+ customer_id: alert.customer_id,
+ alert_type: alert.alert_type,
+ description: alert.description,
+ severity: alert.severity,
+ approval_timeout_hours: 0.05, // 3 minutes for demo
+ }),
+ });
+
+ const data = await response.json();
+ console.log('Workflow started:', data);
+
+ // Store instance ID - this triggers WebSocket connection
+ setInstanceId(data.instance_id);
+ addEvent({ type: 'workflow_started', instance_id: data.instance_id, alert_id: alert.alert_id });
+
+ } catch (error) {
+ console.error('Error starting workflow:', error);
+ setWorkflowRunning(false);
+ addEvent({ type: 'error', message: error.message });
+ }
+ }, [addEvent]);
+
+ const handleSubmitDecision = useCallback(async (decision) => {
+ console.log('Submitting decision:', decision);
+
+ try {
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.WORKFLOW_DECISION}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ instance_id: instanceId,
+ alert_id: selectedAlert?.alert_id,
+ approved_action: decision.approved_action,
+ analyst_notes: decision.analyst_notes,
+ analyst_id: 'analyst_ui',
+ }),
+ });
+
+ const data = await response.json();
+ console.log('Decision submitted:', data);
+ addEvent({ type: 'decision_submitted', action: decision.approved_action });
+
+ } catch (error) {
+ console.error('Error submitting decision:', error);
+ addEvent({ type: 'error', message: error.message });
+ }
+ }, [instanceId, selectedAlert, addEvent]);
+
+ return (
+
+
+
+ {/* App Bar */}
+
+
+
+
+ Durable Fraud Detection Workflow
+
+
+ {instanceId && (
+ }
+ label={`Instance: ${instanceId.substring(0, 20)}...`}
+ color={orchestrationStatus === 'RUNNING' ? 'primary' : orchestrationStatus === 'COMPLETED' ? 'success' : 'default'}
+ size="small"
+ />
+ )}
+
+ Hybrid Workflow + Durable Task
+
+
+
+
+
+ {/* Main Content */}
+
+
+ {/* Left Column - Controls and Decision Panel */}
+
+
+
+ {pendingDecision && (
+
+ )}
+
+
+ {/* Center Column - Workflow Visualization */}
+
+
+
+ Workflow Graph
+
+ {selectedAlert
+ ? `Alert: ${selectedAlert.alert_id} - ${selectedAlert.description}`
+ : 'Select an alert to start'}
+ {orchestrationStatus && ` | Status: ${orchestrationStatus}`}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/AnalystDecisionPanel.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/AnalystDecisionPanel.jsx
new file mode 100644
index 000000000..1c87aea79
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/AnalystDecisionPanel.jsx
@@ -0,0 +1,146 @@
+import { useState } from 'react';
+import {
+ Paper,
+ Box,
+ Typography,
+ Button,
+ TextField,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ Chip,
+ Alert,
+ Divider,
+} from '@mui/material';
+import GavelIcon from '@mui/icons-material/Gavel';
+import SendIcon from '@mui/icons-material/Send';
+import { ACTION_OPTIONS } from '../constants/workflow';
+
+/**
+ * Panel for analyst to make decisions on fraud alerts (Durable version)
+ * @param {Object} props - Component props
+ * @param {Object} props.decision - Decision request object with instance_id, alert_id, customer_id
+ * @param {Function} props.onSubmit - Callback to submit decision
+ */
+function AnalystDecisionPanel({ decision, onSubmit }) {
+ const [selectedAction, setSelectedAction] = useState(
+ decision.recommended_action || 'block'
+ );
+ const [notes, setNotes] = useState('');
+
+ const handleSubmit = () => {
+ onSubmit({
+ instance_id: decision.instance_id,
+ alert_id: decision.alert_id,
+ customer_id: decision.customer_id,
+ approved_action: selectedAction,
+ analyst_notes: notes || 'Analyst decision from UI',
+ analyst_id: 'analyst_ui',
+ });
+ };
+
+ return (
+
+
+
+ Analyst Review Required
+
+
+
+
+ Human Decision Needed
+
+
+
+
+
+ {/* Risk Assessment */}
+
+
+ Review Required
+
+
+ Alert ID:
+
+
+
+ Customer:
+
+
+
+
+ {/* Instance Info */}
+
+
+ Instance ID
+
+
+ {decision.instance_id}
+
+
+
+
+
+ {/* Decision Form */}
+
+ Your Decision
+
+
+
+ setNotes(e.target.value)}
+ placeholder="Add notes..."
+ sx={{ '& .MuiInputBase-input': { fontSize: '0.875rem' } }}
+ />
+
+ }
+ onClick={handleSubmit}
+ sx={{ mt: 0.5, py: 0.75 }}
+ >
+ Submit Decision
+
+
+ );
+}
+
+export default AnalystDecisionPanel;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/ControlPanel.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/ControlPanel.jsx
new file mode 100644
index 000000000..acacc11f1
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/ControlPanel.jsx
@@ -0,0 +1,128 @@
+import { useState } from 'react';
+import {
+ Paper,
+ Box,
+ Typography,
+ Button,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ Chip,
+ CircularProgress,
+} from '@mui/material';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import WarningIcon from '@mui/icons-material/Warning';
+import ErrorIcon from '@mui/icons-material/Error';
+import InfoIcon from '@mui/icons-material/Info';
+import { getSeverityIcon, getSeverityColor } from '../utils/uiHelpers';
+
+/**
+ * Control panel component for selecting alerts and starting workflows
+ * @param {Object} props - Component props
+ * @param {Array} props.alerts - Array of alert objects
+ * @param {Function} props.onStartWorkflow - Callback to start workflow
+ * @param {boolean} props.workflowRunning - Whether workflow is currently running
+ * @param {Object} props.selectedAlert - Currently selected alert object
+ */
+function ControlPanel({ alerts = [], onStartWorkflow, workflowRunning = false, selectedAlert }) {
+ const [selectedAlertId, setSelectedAlertId] = useState('');
+
+ const handleStartClick = () => {
+ const alert = alerts.find((a) => a.alert_id === selectedAlertId);
+ if (alert) {
+ onStartWorkflow(alert);
+ }
+ };
+
+ return (
+
+
+ Workflow Control
+
+
+
+ Select Alert
+
+
+
+ {selectedAlertId && !workflowRunning && (
+
+
+ Description:
+
+
+ {alerts.find((a) => a.alert_id === selectedAlertId)?.description}
+
+
+ a.alert_id === selectedAlertId)?.customer_id}`}
+ size="small"
+ variant="outlined"
+ sx={{ height: 18, fontSize: '0.7rem' }}
+ />
+ a.alert_id === selectedAlertId)?.alert_type}
+ size="small"
+ variant="outlined"
+ sx={{ height: 18, fontSize: '0.7rem' }}
+ />
+
+
+ )}
+
+ : }
+ onClick={handleStartClick}
+ disabled={!selectedAlertId || workflowRunning}
+ sx={{ mt: 0.5, py: 0.75, fontSize: '0.875rem' }}
+ >
+ {workflowRunning ? 'Running...' : 'Start Workflow'}
+
+
+ {selectedAlert && workflowRunning && (
+
+
+ Active Workflow
+
+
+ Processing {selectedAlert.alert_id}
+
+
+ )}
+
+ );
+}
+
+export default ControlPanel;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/CustomNode.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/CustomNode.jsx
new file mode 100644
index 000000000..52d13030e
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/CustomNode.jsx
@@ -0,0 +1,83 @@
+import { Handle, Position } from 'reactflow';
+import { Box, Typography, Paper, Chip } from '@mui/material';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
+import CircleIcon from '@mui/icons-material/Circle';
+import { getNodeStatusColor, getStatusLabel } from '../utils/uiHelpers';
+
+/**
+ * Gets the appropriate icon for node status
+ * @param {string} status - Node status
+ * @returns {JSX.Element} Icon component
+ */
+const getStatusIcon = (status) => {
+ switch (status) {
+ case 'running':
+ return ;
+ case 'completed':
+ return ;
+ default:
+ return ;
+ }
+};
+
+/**
+ * Custom node component for React Flow workflow visualization
+ * @param {Object} props - Component props
+ * @param {Object} props.data - Node data object
+ */
+function CustomNode({ data = {} }) {
+ const statusColor = getNodeStatusColor(data.status);
+ const isActive = data.status === 'running';
+
+ return (
+
+
+
+
+
+
+ {data.label}
+
+
+
+
+ {data.description && (
+
+ {data.description}
+
+ )}
+
+
+
+
+ );
+}
+
+export default CustomNode;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx
new file mode 100644
index 000000000..0d4312302
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/components/WorkflowVisualizer.jsx
@@ -0,0 +1,373 @@
+import { useMemo, useEffect, useState, useCallback } from 'react';
+import ReactFlow, {
+ Background,
+ Controls,
+ MiniMap,
+ useNodesState,
+ useEdgesState,
+} from 'reactflow';
+import 'reactflow/dist/style.css';
+import { Box, Popover, Paper, Typography, Chip, Divider, List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
+import FunctionsIcon from '@mui/icons-material/Functions';
+import OutputIcon from '@mui/icons-material/Output';
+import AccessTimeIcon from '@mui/icons-material/AccessTime';
+import CheckCircleIcon from '@mui/icons-material/CheckCircle';
+import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
+import CustomNode from './CustomNode';
+import { EXECUTOR_STATES } from '../constants/workflow';
+import { getNodeStatusColor, getStatusLabel } from '../utils/uiHelpers';
+
+const nodeTypes = {
+ custom: CustomNode,
+};
+
+// Define the workflow graph structure
+const initialNodes = [
+ {
+ id: 'alert_router',
+ type: 'custom',
+ position: { x: 400, y: 50 },
+ data: { label: 'Alert Router', status: 'idle', description: 'Routes alerts to analysts' },
+ },
+ {
+ id: 'usage_pattern_executor',
+ type: 'custom',
+ position: { x: 150, y: 200 },
+ data: { label: 'Usage Analyst', status: 'idle', description: 'Analyzes usage patterns' },
+ },
+ {
+ id: 'location_analysis_executor',
+ type: 'custom',
+ position: { x: 400, y: 200 },
+ data: { label: 'Location Analyst', status: 'idle', description: 'Analyzes location anomalies' },
+ },
+ {
+ id: 'billing_charge_executor',
+ type: 'custom',
+ position: { x: 650, y: 200 },
+ data: { label: 'Billing Analyst', status: 'idle', description: 'Analyzes billing patterns' },
+ },
+ {
+ id: 'fraud_risk_aggregator',
+ type: 'custom',
+ position: { x: 400, y: 350 },
+ data: { label: 'Risk Aggregator', status: 'idle', description: 'Aggregates risk assessment' },
+ },
+ {
+ id: 'review_gateway',
+ type: 'custom',
+ position: { x: 550, y: 500 },
+ data: { label: 'Review Gateway', status: 'idle', description: 'Human analyst review (pauses workflow)' },
+ },
+ {
+ id: 'auto_clear_executor',
+ type: 'custom',
+ position: { x: 250, y: 500 },
+ data: { label: 'Auto Clear', status: 'idle', description: 'Auto-clears low risk' },
+ },
+ {
+ id: 'fraud_action_executor',
+ type: 'custom',
+ position: { x: 550, y: 650 },
+ data: { label: 'Fraud Action', status: 'idle', description: 'Execute fraud action' },
+ },
+ {
+ id: 'final_notification_executor',
+ type: 'custom',
+ position: { x: 400, y: 800 },
+ data: { label: 'Final Notification', status: 'idle', description: 'Send notifications' },
+ },
+];
+
+const initialEdges = [
+ // Fan-out from alert router to analysts
+ { id: 'e1-1', source: 'alert_router', target: 'usage_pattern_executor', animated: true },
+ { id: 'e1-2', source: 'alert_router', target: 'location_analysis_executor', animated: true },
+ { id: 'e1-3', source: 'alert_router', target: 'billing_charge_executor', animated: true },
+
+ // Fan-in to aggregator
+ { id: 'e2-1', source: 'usage_pattern_executor', target: 'fraud_risk_aggregator' },
+ { id: 'e2-2', source: 'location_analysis_executor', target: 'fraud_risk_aggregator' },
+ { id: 'e2-3', source: 'billing_charge_executor', target: 'fraud_risk_aggregator' },
+
+ // Switch case from aggregator
+ { id: 'e3-1', source: 'fraud_risk_aggregator', target: 'review_gateway', label: 'High Risk', style: { stroke: '#f44336' } },
+ { id: 'e3-2', source: 'fraud_risk_aggregator', target: 'auto_clear_executor', label: 'Low Risk', style: { stroke: '#4caf50' } },
+
+ // Review gateway to fraud action (human review happens via request_info, then proceeds)
+ { id: 'e4-1', source: 'review_gateway', target: 'fraud_action_executor', animated: true, style: { stroke: '#ff9800' } },
+
+ // Final paths
+ { id: 'e5-1', source: 'auto_clear_executor', target: 'final_notification_executor' },
+ { id: 'e5-2', source: 'fraud_action_executor', target: 'final_notification_executor' },
+];
+
+/**
+ * Workflow visualizer component using React Flow
+ * Displays the fraud detection workflow as an interactive graph
+ * @param {Object} props - Component props
+ * @param {Object} props.executorStates - Map of executor IDs to their current states
+ * @param {Object} props.stepDetails - Real step details from backend (tool calls, outputs)
+ */
+function WorkflowVisualizer({ executorStates = {}, stepDetails = {} }) {
+ // Popover state
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [selectedNode, setSelectedNode] = useState(null);
+
+ // Static descriptions for nodes (fallback)
+ const nodeDescriptions = useMemo(() => ({
+ alert_router: 'Routes incoming fraud alerts to specialist analysts for parallel investigation.',
+ usage_pattern_executor: 'AI agent that analyzes customer data usage patterns to detect anomalies.',
+ location_analysis_executor: 'AI agent that detects geographic anomalies and suspicious location patterns.',
+ billing_charge_executor: 'AI agent that identifies unusual billing patterns and charge irregularities.',
+ fraud_risk_aggregator: 'Combines analyses from all specialists to calculate overall risk score.',
+ review_gateway: 'Human-in-the-loop checkpoint. Pauses workflow for analyst approval on high-risk cases.',
+ auto_clear_executor: 'Automatically clears alerts with low risk scores (< 0.6).',
+ fraud_action_executor: 'Executes the analyst-approved action (suspend, flag, or additional verification).',
+ final_notification_executor: 'Sends notification to customer and internal teams about the resolution.',
+ }), []);
+
+ // Get step info from backend data or fallback
+ const getStepInfo = useCallback((nodeId) => {
+ const backendData = stepDetails[nodeId];
+
+ if (backendData) {
+ // Real data from backend
+ return {
+ toolCalls: backendData.tool_calls || [],
+ output: backendData.output || '',
+ riskScore: backendData.risk_score,
+ hasRealData: true,
+ };
+ }
+
+ // Fallback static data for nodes without backend info
+ const staticToolCalls = {
+ alert_router: [{ name: 'distribute_alert', result: 'Sent to 3 analysts' }],
+ review_gateway: [{ name: 'wait_for_decision', result: 'Analyst decision pending' }],
+ auto_clear_executor: [{ name: 'clear_alert', result: '' }],
+ fraud_action_executor: [{ name: 'execute_action', result: '' }],
+ final_notification_executor: [{ name: 'send_notification', result: '' }],
+ };
+
+ return {
+ toolCalls: staticToolCalls[nodeId] || [],
+ output: '',
+ hasRealData: false,
+ };
+ }, [stepDetails]);
+
+ // Handle node click
+ const handleNodeClick = useCallback((event, node) => {
+ setAnchorEl(event.target);
+ setSelectedNode(node);
+ }, []);
+
+ const handleClosePopover = useCallback(() => {
+ setAnchorEl(null);
+ setSelectedNode(null);
+ }, []);
+
+ // Update nodes with current executor states
+ const nodes = useMemo(() => {
+ return initialNodes.map((node) => ({
+ ...node,
+ data: {
+ ...node.data,
+ status: executorStates[node.id] || EXECUTOR_STATES.IDLE,
+ },
+ }));
+ }, [executorStates]);
+
+ const [nodesState, setNodes, onNodesChange] = useNodesState(nodes);
+ const [edgesState, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ // Update nodes when executor states change
+ useEffect(() => {
+ setNodes(nodes);
+ }, [nodes, setNodes]);
+
+ // Get status icon
+ const getStatusIcon = (status) => {
+ if (status === 'running') return ;
+ if (status === 'completed') return ;
+ return ;
+ };
+
+ return (
+
+
+
+
+ {
+ const status = node.data.status;
+ if (status === 'running') return '#1976d2';
+ if (status === 'completed') return '#4caf50';
+ return '#9e9e9e';
+ }}
+ />
+
+
+ {/* Node Details Popover */}
+
+ {selectedNode && (() => {
+ const stepInfo = getStepInfo(selectedNode.id);
+ return (
+
+ {/* Header */}
+
+ {getStatusIcon(selectedNode.data.status)}
+
+ {selectedNode.data.label}
+
+
+
+ {/* Status chip + Risk Score */}
+
+
+ {stepInfo.riskScore !== undefined && (
+ = 0.6 ? 'error' : 'success'}
+ variant="outlined"
+ />
+ )}
+
+
+ {/* Description */}
+
+ {nodeDescriptions[selectedNode.id] || selectedNode.data.description}
+
+
+
+
+ {/* Function/Tool Calls */}
+
+
+ {stepInfo.hasRealData ? 'Tool Calls (Real)' : 'Expected Tools'}
+
+
+ {stepInfo.toolCalls.length > 0 ? (
+
+ {stepInfo.toolCalls.map((tc, idx) => (
+
+
+
+
+
+
+
+ {tc.result && (
+
+
+ {tc.result.length > 200 ? tc.result.slice(0, 200) + '...' : tc.result}
+
+
+ )}
+
+ ))}
+
+ ) : (
+
+ No tool calls recorded
+
+ )}
+
+ {/* Output Summary (if available) */}
+ {stepInfo.output && (
+ <>
+
+ Analysis Output
+
+
+
+ {stepInfo.output}
+
+
+ >
+ )}
+
+ );
+ })()}
+
+
+ );
+}
+
+export default WorkflowVisualizer;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js
new file mode 100644
index 000000000..617845f48
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/config.js
@@ -0,0 +1,53 @@
+/**
+ * Runtime configuration support
+ * Priority: window.__CONFIG__ (runtime) > import.meta.env (build-time) > defaults
+ *
+ * When served from the same origin as the API (production), use relative URLs.
+ * For local dev with separate frontend server, use localhost URLs.
+ */
+const runtimeConfig = typeof window !== 'undefined' ? window.__CONFIG__ || {} : {};
+
+// Determine if we're running from same origin as API (production mode)
+const isSameOrigin = typeof window !== 'undefined' &&
+ !window.location.port.includes('5173') && // Not Vite dev server
+ !window.location.port.includes('3000'); // Not React dev server
+
+// In production (same origin), use relative URLs. In dev, use localhost.
+const defaultBaseUrl = isSameOrigin ? '' : 'http://localhost:8002';
+const defaultWsUrl = isSameOrigin
+ ? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`
+ : 'ws://localhost:8002/ws';
+
+/**
+ * API configuration constants
+ */
+export const API_CONFIG = {
+ BASE_URL: runtimeConfig.API_BASE_URL || import.meta.env.VITE_API_BASE_URL || defaultBaseUrl,
+ WS_URL: runtimeConfig.WS_URL || import.meta.env.VITE_WS_URL || defaultWsUrl,
+ ENDPOINTS: {
+ ALERTS: '/api/alerts',
+ WORKFLOW_START: '/api/workflow/start',
+ WORKFLOW_DECISION: '/api/workflow/decision',
+ },
+};
+
+/**
+ * WebSocket configuration
+ */
+export const WS_CONFIG = {
+ RECONNECT_DELAY: 3000,
+ MAX_RECONNECT_ATTEMPTS: 10,
+};
+
+/**
+ * Application constants
+ */
+export const APP_CONFIG = {
+ TITLE: import.meta.env.VITE_APP_TITLE || 'Fraud Detection Workflow Visualizer',
+};
+
+export default {
+ API_CONFIG,
+ WS_CONFIG,
+ APP_CONFIG,
+};
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/workflow.js b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/workflow.js
new file mode 100644
index 000000000..07d791540
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/constants/workflow.js
@@ -0,0 +1,53 @@
+/**
+ * Workflow event types
+ */
+export const EVENT_TYPES = {
+ WORKFLOW_INITIALIZING: 'workflow_initializing',
+ WORKFLOW_STARTED: 'workflow_started',
+ WORKFLOW_COMPLETED: 'workflow_completed',
+ WORKFLOW_ERROR: 'workflow_error',
+ EXECUTOR_INVOKED: 'executor_invoked',
+ EXECUTOR_COMPLETED: 'executor_completed',
+ DECISION_REQUIRED: 'decision_required',
+ STATUS_CHANGE: 'status_change',
+ WORKFLOW_OUTPUT: 'workflow_output',
+};
+
+/**
+ * Alert severity levels
+ */
+export const SEVERITY_LEVELS = {
+ HIGH: 'high',
+ MEDIUM: 'medium',
+ LOW: 'low',
+};
+
+/**
+ * Executor states
+ */
+export const EXECUTOR_STATES = {
+ IDLE: 'idle',
+ RUNNING: 'running',
+ COMPLETED: 'completed',
+ ERROR: 'error',
+};
+
+/**
+ * Fraud action options
+ */
+export const ACTION_OPTIONS = [
+ { value: 'clear', label: 'Clear - No Action Needed', color: 'success' },
+ { value: 'lock_account', label: 'Lock Account', color: 'error' },
+ { value: 'refund_charges', label: 'Refund Charges', color: 'warning' },
+ { value: 'both', label: 'Lock Account & Refund', color: 'error' },
+];
+
+/**
+ * WebSocket ready states
+ */
+export const WS_READY_STATES = {
+ CONNECTING: 'CONNECTING',
+ OPEN: 'OPEN',
+ CLOSING: 'CLOSING',
+ CLOSED: 'CLOSED',
+};
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/index.css b/agentic_ai/workflow/fraud_detection_durable/ui/src/index.css
new file mode 100644
index 000000000..0979488ae
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/index.css
@@ -0,0 +1,18 @@
+body {
+ margin: 0;
+ font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f5f5f5;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+#root {
+ height: 100vh;
+ width: 100vw;
+}
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/main.jsx b/agentic_ai/workflow/fraud_detection_durable/ui/src/main.jsx
new file mode 100644
index 000000000..8e3e8340e
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/main.jsx
@@ -0,0 +1,18 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import App from './App';
+import './index.css';
+
+const rootElement = document.getElementById('root');
+
+if (!rootElement) {
+ throw new Error('Failed to find the root element');
+}
+
+const root = createRoot(rootElement);
+
+root.render(
+
+
+
+);
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/theme/index.js b/agentic_ai/workflow/fraud_detection_durable/ui/src/theme/index.js
new file mode 100644
index 000000000..799f49a2f
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/theme/index.js
@@ -0,0 +1,84 @@
+import { createTheme } from '@mui/material';
+
+/**
+ * Application theme configuration
+ * Uses Material-UI theme customization
+ */
+export const theme = createTheme({
+ palette: {
+ mode: 'light',
+ primary: {
+ main: '#1976d2',
+ light: '#42a5f5',
+ dark: '#1565c0',
+ contrastText: '#fff',
+ },
+ secondary: {
+ main: '#dc004e',
+ light: '#f33a6a',
+ dark: '#9a0036',
+ contrastText: '#fff',
+ },
+ success: {
+ main: '#4caf50',
+ light: '#81c784',
+ dark: '#388e3c',
+ },
+ warning: {
+ main: '#ff9800',
+ light: '#ffb74d',
+ dark: '#f57c00',
+ },
+ error: {
+ main: '#f44336',
+ light: '#e57373',
+ dark: '#d32f2f',
+ },
+ info: {
+ main: '#2196f3',
+ light: '#64b5f6',
+ dark: '#1976d2',
+ },
+ background: {
+ default: '#f5f5f5',
+ paper: '#ffffff',
+ },
+ },
+ typography: {
+ fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
+ h6: {
+ fontWeight: 500,
+ },
+ subtitle1: {
+ fontWeight: 500,
+ },
+ body1: {
+ fontSize: '0.875rem',
+ },
+ body2: {
+ fontSize: '0.75rem',
+ },
+ },
+ shape: {
+ borderRadius: 8,
+ },
+ components: {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ textTransform: 'none',
+ borderRadius: 8,
+ },
+ },
+ },
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ borderRadius: 8,
+ },
+ },
+ },
+ },
+});
+
+export default theme;
diff --git a/agentic_ai/workflow/fraud_detection_durable/ui/src/utils/api.js b/agentic_ai/workflow/fraud_detection_durable/ui/src/utils/api.js
new file mode 100644
index 000000000..26ea9986c
--- /dev/null
+++ b/agentic_ai/workflow/fraud_detection_durable/ui/src/utils/api.js
@@ -0,0 +1,73 @@
+import { API_CONFIG } from '../constants/config';
+
+/**
+ * Fetches alerts from the API
+ * @returns {Promise} Array of alerts
+ */
+export const fetchAlerts = async () => {
+ try {
+ const response = await fetch(`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.ALERTS}`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ return data.alerts || [];
+ } catch (error) {
+ console.error('Error loading alerts:', error);
+ throw error;
+ }
+};
+
+/**
+ * Starts a workflow for a given alert
+ * @param {Object} alert - The alert object
+ * @returns {Promise