agentensemble 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentensemble-0.2.0 → agentensemble-0.3.0}/PKG-INFO +103 -24
- {agentensemble-0.2.0 → agentensemble-0.3.0}/README.md +102 -23
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/__init__.py +4 -2
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/react_agent.py +145 -33
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/mistral_provider.py +22 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/runner.py +7 -1
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tracing/__init__.py +2 -2
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tracing/hooks.py +40 -1
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/PKG-INFO +103 -24
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/SOURCES.txt +2 -1
- {agentensemble-0.2.0 → agentensemble-0.3.0}/pyproject.toml +1 -1
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_runner.py +21 -0
- agentensemble-0.3.0/tests/test_tracing.py +49 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/MANIFEST.in +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/base.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/hybrid_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/rag_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/stategraph_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/structured_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/core/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/core/protocol.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/graph/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/graph/workflow.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/interface.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/session.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/sqlite_session.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/debate.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/ensemble.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/pipeline.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/supervisor.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/swarm.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/planner/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/planner/planner_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/py.typed +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/router/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/router/router_agent.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/benchmark.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/comparison.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/metrics.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/adapters.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/function_tool.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/protocol.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/rag.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/registry.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/scraper.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/search.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/validator.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/utils/__init__.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/utils/llm.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/dependency_links.txt +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/requires.txt +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/top_level.txt +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/setup.cfg +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/setup.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_agents.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_comparison.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_memory.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_orchestration.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_react_session.py +0 -0
- {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentensemble
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Orchestrate AI agents in perfect harmony - A comprehensive Python framework for building, testing, and orchestrating multi-agent AI systems
|
|
5
5
|
Author-email: Irfan Ali <irfanali29@hotmail.com>
|
|
6
6
|
Maintainer-email: Irfan Ali <irfanali29@hotmail.com>
|
|
@@ -76,25 +76,43 @@ AgentEnsemble is a modern framework for building and orchestrating AI agent syst
|
|
|
76
76
|
## Architecture
|
|
77
77
|
|
|
78
78
|
<p align="center">
|
|
79
|
-
<img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="
|
|
79
|
+
<img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="900"/>
|
|
80
80
|
</p>
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
│
|
|
87
|
-
|
|
88
|
-
│
|
|
89
|
-
|
|
90
|
-
│ ReActAgent
|
|
91
|
-
│
|
|
92
|
-
│ RAGAgent
|
|
93
|
-
│ HybridAgent
|
|
94
|
-
│ RouterAgent │
|
|
95
|
-
│ PlannerAgent│
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
### Framework layers
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
|
86
|
+
│ AgentEnsemble — Layered Architecture │
|
|
87
|
+
├─────────────────┬─────────────────────┬─────────────────────┬────────────────────────────┤
|
|
88
|
+
│ AGENTS │ ORCHESTRATION │ TOOLS │ MEMORY & RUNNER │
|
|
89
|
+
├─────────────────┼─────────────────────┼─────────────────────┼────────────────────────────┤
|
|
90
|
+
│ ReActAgent │ Supervisor │ SearchTool │ Session (InMemory, SQLite) │
|
|
91
|
+
│ StateGraphAgent │ Swarm (parallel) │ RAGTool │ RunConfig, RunHooks │
|
|
92
|
+
│ RAGAgent │ Pipeline (seq) │ @function_tool │ TraceHooks (usage, cost) │
|
|
93
|
+
│ HybridAgent │ Debate (consensus) │ ValidationTool │ interrupt_before_tools │
|
|
94
|
+
│ RouterAgent │ Ensemble + Router │ ToolRegistry │ retries, session memory │
|
|
95
|
+
│ PlannerAgent │ WorkflowGraph │ │ │
|
|
96
|
+
└─────────────────┴─────────────────────┴─────────────────────┴────────────────────────────┘
|
|
97
|
+
│ │ │ │
|
|
98
|
+
└────────────────────┴────────────────────┴─────────────────────────┘
|
|
99
|
+
│
|
|
100
|
+
┌───────────▼───────────┐
|
|
101
|
+
│ Use Case Spectrum │
|
|
102
|
+
└───────────────────────┘
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Use case spectrum
|
|
106
|
+
|
|
107
|
+
| Layer | Use cases |
|
|
108
|
+
|-------|-----------|
|
|
109
|
+
| **Research & fact-checking** | ReActAgent + SearchTool; Swarm for parallel verification |
|
|
110
|
+
| **Document Q&A** | RAGAgent + RAGTool; index URLs, ask questions |
|
|
111
|
+
| **Multi-agent coordination** | Supervisor, Swarm, Pipeline; RouterAgent for LLM-based selection |
|
|
112
|
+
| **Debate & consensus** | DebateOrchestrator; solvers propose → aggregator votes |
|
|
113
|
+
| **Custom workflows** | WorkflowGraph (DAG), StateGraphAgent (nodes), PlannerAgent (decompose) |
|
|
114
|
+
| **Human-in-loop** | `interrupt_before_tools` — pause before sensitive tools, resume with approval |
|
|
115
|
+
| **Production pipelines** | Runner + RunConfig, retries, TraceHooks (usage/cost), Session memory |
|
|
98
116
|
|
|
99
117
|
---
|
|
100
118
|
|
|
@@ -367,7 +385,65 @@ Runner.run(agent, "What about its population?", config=config) # Remembers cont
|
|
|
367
385
|
|
|
368
386
|
---
|
|
369
387
|
|
|
370
|
-
### 11.
|
|
388
|
+
### 11. TraceHooks — Observability, token usage, cost estimation
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from agentensemble import ReActAgent, Runner, RunConfig, TraceHooks, SearchTool
|
|
392
|
+
|
|
393
|
+
agent = ReActAgent(name="research", tools=[SearchTool()], max_iterations=3)
|
|
394
|
+
hooks = TraceHooks()
|
|
395
|
+
|
|
396
|
+
config = RunConfig(trace_hooks=hooks)
|
|
397
|
+
result = Runner.run(agent, "Who won the 2024 Nobel Prize in Physics?", config=config)
|
|
398
|
+
|
|
399
|
+
print(result["result"][:100], "...")
|
|
400
|
+
print("Usage:", result["metadata"].get("total_usage"))
|
|
401
|
+
print("Cost (USD):", result["metadata"].get("cost_usd"))
|
|
402
|
+
print("Events:", [e.type.value for e in hooks.events])
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Output:**
|
|
406
|
+
|
|
407
|
+
```
|
|
408
|
+
The 2024 Nobel Prize in Physics was awarded jointly to John J. Hopfield and Geoffrey E. Hinton...
|
|
409
|
+
Usage: {'input_tokens': 1247, 'output_tokens': 89, 'total_tokens': 1336}
|
|
410
|
+
Cost (USD): 0.00293
|
|
411
|
+
Events: ['run_start', 'llm_start', 'llm_end', 'tool_start', 'tool_end', 'llm_start', 'llm_end', 'run_end']
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
### 12. Human-in-loop — Pause before sensitive tools, resume with approval
|
|
417
|
+
|
|
418
|
+
```python
|
|
419
|
+
from agentensemble import ReActAgent, Runner, RunConfig, function_tool
|
|
420
|
+
|
|
421
|
+
@function_tool(description="Execute a payment")
|
|
422
|
+
def execute_payment(amount: float, recipient: str) -> str:
|
|
423
|
+
return f"Paid ${amount} to {recipient}"
|
|
424
|
+
|
|
425
|
+
agent = ReActAgent(tools=[execute_payment], max_iterations=2)
|
|
426
|
+
config = RunConfig(interrupt_before_tools=["execute_payment"])
|
|
427
|
+
|
|
428
|
+
result = Runner.run(agent, "Pay $50 to Alice", config=config)
|
|
429
|
+
|
|
430
|
+
if result.get("__interrupted__"):
|
|
431
|
+
print("Paused for approval:", result["pending_tool"])
|
|
432
|
+
# Simulate human approval
|
|
433
|
+
result = Runner.run(agent, "...", config=config, resume=result["run_state"], resume_value="Paid $50 to Alice")
|
|
434
|
+
print(result["result"])
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Output:**
|
|
438
|
+
|
|
439
|
+
```
|
|
440
|
+
Paused for approval: {'name': 'execute_payment', 'arguments': {'amount': 50.0, 'recipient': 'Alice'}}
|
|
441
|
+
Paid $50 to Alice
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
### 13. RAGAgent — Document Q&A (optional: `pip install agentensemble[rag]`)
|
|
371
447
|
|
|
372
448
|
```python
|
|
373
449
|
from agentensemble import RAGAgent, RAGTool
|
|
@@ -388,7 +464,7 @@ manageable subtasks that can be executed by specialized agents...
|
|
|
388
464
|
|
|
389
465
|
---
|
|
390
466
|
|
|
391
|
-
###
|
|
467
|
+
### 14. StructuredAgent — Pydantic output (optional: `langchain>=1.1`)
|
|
392
468
|
|
|
393
469
|
```python
|
|
394
470
|
from pydantic import BaseModel, Field
|
|
@@ -469,7 +545,7 @@ pip install agentensemble[search]
|
|
|
469
545
|
PYTHONPATH=. python examples/showcase_all.py
|
|
470
546
|
```
|
|
471
547
|
|
|
472
|
-
Runs all
|
|
548
|
+
Runs all demos with real API calls.
|
|
473
549
|
|
|
474
550
|
---
|
|
475
551
|
|
|
@@ -507,7 +583,8 @@ Runs all 10 demos with real API calls.
|
|
|
507
583
|
|
|
508
584
|
- **Session** — `InMemorySession`, `SQLiteSession` for multi-turn context
|
|
509
585
|
- **Runner** — `Runner.run()`, `Runner.arun()` with `RunConfig`, `RunHooks`, retries
|
|
510
|
-
- **TraceHooks** — Observability
|
|
586
|
+
- **TraceHooks** — Observability: run/LLM/tool events, token usage, cost estimation
|
|
587
|
+
- **Human-in-loop** — `interrupt_before_tools=["pay", "send_email"]` to pause for approval
|
|
511
588
|
|
|
512
589
|
---
|
|
513
590
|
|
|
@@ -542,7 +619,8 @@ agentensemble/
|
|
|
542
619
|
| Graph workflows | WorkflowGraph, StateGraph | Native | No | No |
|
|
543
620
|
| Debate pattern | DebateOrchestrator | Manual | Yes | Yes |
|
|
544
621
|
| Session memory | InMemory, SQLite | Checkpointer | No | No |
|
|
545
|
-
| Observability | TraceHooks
|
|
622
|
+
| Observability | TraceHooks, usage, cost | LangSmith | No | No |
|
|
623
|
+
| Human-in-loop | interrupt_before_tools | interrupt() | No | No |
|
|
546
624
|
|
|
547
625
|
---
|
|
548
626
|
|
|
@@ -550,7 +628,8 @@ agentensemble/
|
|
|
550
628
|
|
|
551
629
|
- **Retries**: `RunConfig(max_retries=2, retry_on=(RateLimitError, TimeoutError))`
|
|
552
630
|
- **Session**: `Runner.run(agent, query, config=RunConfig(session=SQLiteSession()))`
|
|
553
|
-
- **Tracing**:
|
|
631
|
+
- **Tracing**: `RunConfig(trace_hooks=TraceHooks())` — captures run/LLM/tool events, usage, cost
|
|
632
|
+
- **Human-in-loop**: `RunConfig(interrupt_before_tools=["pay"])` — pause before sensitive tools; resume with `Runner.run(agent, ..., resume=run_state, resume_value="Approved")`
|
|
554
633
|
- **Benchmarks**: `AgentComparison`, `Benchmark.research_tasks()`, `Metrics`
|
|
555
634
|
|
|
556
635
|
---
|
|
@@ -23,25 +23,43 @@ AgentEnsemble is a modern framework for building and orchestrating AI agent syst
|
|
|
23
23
|
## Architecture
|
|
24
24
|
|
|
25
25
|
<p align="center">
|
|
26
|
-
<img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="
|
|
26
|
+
<img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="900"/>
|
|
27
27
|
</p>
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
│
|
|
34
|
-
|
|
35
|
-
│
|
|
36
|
-
|
|
37
|
-
│ ReActAgent
|
|
38
|
-
│
|
|
39
|
-
│ RAGAgent
|
|
40
|
-
│ HybridAgent
|
|
41
|
-
│ RouterAgent │
|
|
42
|
-
│ PlannerAgent│
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
### Framework layers
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
|
33
|
+
│ AgentEnsemble — Layered Architecture │
|
|
34
|
+
├─────────────────┬─────────────────────┬─────────────────────┬────────────────────────────┤
|
|
35
|
+
│ AGENTS │ ORCHESTRATION │ TOOLS │ MEMORY & RUNNER │
|
|
36
|
+
├─────────────────┼─────────────────────┼─────────────────────┼────────────────────────────┤
|
|
37
|
+
│ ReActAgent │ Supervisor │ SearchTool │ Session (InMemory, SQLite) │
|
|
38
|
+
│ StateGraphAgent │ Swarm (parallel) │ RAGTool │ RunConfig, RunHooks │
|
|
39
|
+
│ RAGAgent │ Pipeline (seq) │ @function_tool │ TraceHooks (usage, cost) │
|
|
40
|
+
│ HybridAgent │ Debate (consensus) │ ValidationTool │ interrupt_before_tools │
|
|
41
|
+
│ RouterAgent │ Ensemble + Router │ ToolRegistry │ retries, session memory │
|
|
42
|
+
│ PlannerAgent │ WorkflowGraph │ │ │
|
|
43
|
+
└─────────────────┴─────────────────────┴─────────────────────┴────────────────────────────┘
|
|
44
|
+
│ │ │ │
|
|
45
|
+
└────────────────────┴────────────────────┴─────────────────────────┘
|
|
46
|
+
│
|
|
47
|
+
┌───────────▼───────────┐
|
|
48
|
+
│ Use Case Spectrum │
|
|
49
|
+
└───────────────────────┘
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Use case spectrum
|
|
53
|
+
|
|
54
|
+
| Layer | Use cases |
|
|
55
|
+
|-------|-----------|
|
|
56
|
+
| **Research & fact-checking** | ReActAgent + SearchTool; Swarm for parallel verification |
|
|
57
|
+
| **Document Q&A** | RAGAgent + RAGTool; index URLs, ask questions |
|
|
58
|
+
| **Multi-agent coordination** | Supervisor, Swarm, Pipeline; RouterAgent for LLM-based selection |
|
|
59
|
+
| **Debate & consensus** | DebateOrchestrator; solvers propose → aggregator votes |
|
|
60
|
+
| **Custom workflows** | WorkflowGraph (DAG), StateGraphAgent (nodes), PlannerAgent (decompose) |
|
|
61
|
+
| **Human-in-loop** | `interrupt_before_tools` — pause before sensitive tools, resume with approval |
|
|
62
|
+
| **Production pipelines** | Runner + RunConfig, retries, TraceHooks (usage/cost), Session memory |
|
|
45
63
|
|
|
46
64
|
---
|
|
47
65
|
|
|
@@ -314,7 +332,65 @@ Runner.run(agent, "What about its population?", config=config) # Remembers cont
|
|
|
314
332
|
|
|
315
333
|
---
|
|
316
334
|
|
|
317
|
-
### 11.
|
|
335
|
+
### 11. TraceHooks — Observability, token usage, cost estimation
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from agentensemble import ReActAgent, Runner, RunConfig, TraceHooks, SearchTool
|
|
339
|
+
|
|
340
|
+
agent = ReActAgent(name="research", tools=[SearchTool()], max_iterations=3)
|
|
341
|
+
hooks = TraceHooks()
|
|
342
|
+
|
|
343
|
+
config = RunConfig(trace_hooks=hooks)
|
|
344
|
+
result = Runner.run(agent, "Who won the 2024 Nobel Prize in Physics?", config=config)
|
|
345
|
+
|
|
346
|
+
print(result["result"][:100], "...")
|
|
347
|
+
print("Usage:", result["metadata"].get("total_usage"))
|
|
348
|
+
print("Cost (USD):", result["metadata"].get("cost_usd"))
|
|
349
|
+
print("Events:", [e.type.value for e in hooks.events])
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Output:**
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
The 2024 Nobel Prize in Physics was awarded jointly to John J. Hopfield and Geoffrey E. Hinton...
|
|
356
|
+
Usage: {'input_tokens': 1247, 'output_tokens': 89, 'total_tokens': 1336}
|
|
357
|
+
Cost (USD): 0.00293
|
|
358
|
+
Events: ['run_start', 'llm_start', 'llm_end', 'tool_start', 'tool_end', 'llm_start', 'llm_end', 'run_end']
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
### 12. Human-in-loop — Pause before sensitive tools, resume with approval
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
from agentensemble import ReActAgent, Runner, RunConfig, function_tool
|
|
367
|
+
|
|
368
|
+
@function_tool(description="Execute a payment")
|
|
369
|
+
def execute_payment(amount: float, recipient: str) -> str:
|
|
370
|
+
return f"Paid ${amount} to {recipient}"
|
|
371
|
+
|
|
372
|
+
agent = ReActAgent(tools=[execute_payment], max_iterations=2)
|
|
373
|
+
config = RunConfig(interrupt_before_tools=["execute_payment"])
|
|
374
|
+
|
|
375
|
+
result = Runner.run(agent, "Pay $50 to Alice", config=config)
|
|
376
|
+
|
|
377
|
+
if result.get("__interrupted__"):
|
|
378
|
+
print("Paused for approval:", result["pending_tool"])
|
|
379
|
+
# Simulate human approval
|
|
380
|
+
result = Runner.run(agent, "...", config=config, resume=result["run_state"], resume_value="Paid $50 to Alice")
|
|
381
|
+
print(result["result"])
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Output:**
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
Paused for approval: {'name': 'execute_payment', 'arguments': {'amount': 50.0, 'recipient': 'Alice'}}
|
|
388
|
+
Paid $50 to Alice
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
### 13. RAGAgent — Document Q&A (optional: `pip install agentensemble[rag]`)
|
|
318
394
|
|
|
319
395
|
```python
|
|
320
396
|
from agentensemble import RAGAgent, RAGTool
|
|
@@ -335,7 +411,7 @@ manageable subtasks that can be executed by specialized agents...
|
|
|
335
411
|
|
|
336
412
|
---
|
|
337
413
|
|
|
338
|
-
###
|
|
414
|
+
### 14. StructuredAgent — Pydantic output (optional: `langchain>=1.1`)
|
|
339
415
|
|
|
340
416
|
```python
|
|
341
417
|
from pydantic import BaseModel, Field
|
|
@@ -416,7 +492,7 @@ pip install agentensemble[search]
|
|
|
416
492
|
PYTHONPATH=. python examples/showcase_all.py
|
|
417
493
|
```
|
|
418
494
|
|
|
419
|
-
Runs all
|
|
495
|
+
Runs all demos with real API calls.
|
|
420
496
|
|
|
421
497
|
---
|
|
422
498
|
|
|
@@ -454,7 +530,8 @@ Runs all 10 demos with real API calls.
|
|
|
454
530
|
|
|
455
531
|
- **Session** — `InMemorySession`, `SQLiteSession` for multi-turn context
|
|
456
532
|
- **Runner** — `Runner.run()`, `Runner.arun()` with `RunConfig`, `RunHooks`, retries
|
|
457
|
-
- **TraceHooks** — Observability
|
|
533
|
+
- **TraceHooks** — Observability: run/LLM/tool events, token usage, cost estimation
|
|
534
|
+
- **Human-in-loop** — `interrupt_before_tools=["pay", "send_email"]` to pause for approval
|
|
458
535
|
|
|
459
536
|
---
|
|
460
537
|
|
|
@@ -489,7 +566,8 @@ agentensemble/
|
|
|
489
566
|
| Graph workflows | WorkflowGraph, StateGraph | Native | No | No |
|
|
490
567
|
| Debate pattern | DebateOrchestrator | Manual | Yes | Yes |
|
|
491
568
|
| Session memory | InMemory, SQLite | Checkpointer | No | No |
|
|
492
|
-
| Observability | TraceHooks
|
|
569
|
+
| Observability | TraceHooks, usage, cost | LangSmith | No | No |
|
|
570
|
+
| Human-in-loop | interrupt_before_tools | interrupt() | No | No |
|
|
493
571
|
|
|
494
572
|
---
|
|
495
573
|
|
|
@@ -497,7 +575,8 @@ agentensemble/
|
|
|
497
575
|
|
|
498
576
|
- **Retries**: `RunConfig(max_retries=2, retry_on=(RateLimitError, TimeoutError))`
|
|
499
577
|
- **Session**: `Runner.run(agent, query, config=RunConfig(session=SQLiteSession()))`
|
|
500
|
-
- **Tracing**:
|
|
578
|
+
- **Tracing**: `RunConfig(trace_hooks=TraceHooks())` — captures run/LLM/tool events, usage, cost
|
|
579
|
+
- **Human-in-loop**: `RunConfig(interrupt_before_tools=["pay"])` — pause before sensitive tools; resume with `Runner.run(agent, ..., resume=run_state, resume_value="Approved")`
|
|
501
580
|
- **Benchmarks**: `AgentComparison`, `Benchmark.research_tasks()`, `Metrics`
|
|
502
581
|
|
|
503
582
|
---
|
|
@@ -4,7 +4,7 @@ AgentEnsemble - Orchestrate AI agents in perfect harmony
|
|
|
4
4
|
A comprehensive framework for building, testing, and orchestrating multi-agent AI systems.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
8
|
__author__ = "Irfan Ali"
|
|
9
9
|
|
|
10
10
|
# Core imports
|
|
@@ -52,7 +52,7 @@ from agentensemble.core import AgentProtocol
|
|
|
52
52
|
from agentensemble.router import RouterAgent
|
|
53
53
|
from agentensemble.planner import PlannerAgent
|
|
54
54
|
from agentensemble.graph import WorkflowGraph, Node, Edge
|
|
55
|
-
from agentensemble.tracing import TraceHooks, TraceEvent
|
|
55
|
+
from agentensemble.tracing import TraceHooks, TraceEvent, TraceEventType, estimate_cost
|
|
56
56
|
|
|
57
57
|
__all__ = [
|
|
58
58
|
# Version
|
|
@@ -101,6 +101,8 @@ __all__ = [
|
|
|
101
101
|
# Tracing
|
|
102
102
|
"TraceHooks",
|
|
103
103
|
"TraceEvent",
|
|
104
|
+
"TraceEventType",
|
|
105
|
+
"estimate_cost",
|
|
104
106
|
]
|
|
105
107
|
|
|
106
108
|
# Add StructuredAgent if available
|
|
@@ -10,12 +10,22 @@ Research basis:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import asyncio
|
|
13
|
+
import time
|
|
13
14
|
from typing import Any, AsyncIterator, Dict, List, Optional
|
|
14
15
|
|
|
15
16
|
from agentensemble.agents.base import BaseAgent, AgentState
|
|
16
17
|
from agentensemble.llm.interface import LLMMessage, LLMProvider, ToolCall
|
|
17
18
|
from agentensemble.tools.adapters import get_tool_schemas, ainvoke_tool
|
|
18
19
|
|
|
20
|
+
try:
|
|
21
|
+
from agentensemble.tracing import TraceEventType, TraceEvent, estimate_cost
|
|
22
|
+
TRACING_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
TRACING_AVAILABLE = False
|
|
25
|
+
TraceEventType = None
|
|
26
|
+
TraceEvent = None
|
|
27
|
+
estimate_cost = None
|
|
28
|
+
|
|
19
29
|
try:
|
|
20
30
|
from agentensemble.llm.mistral_provider import MistralLLMProvider
|
|
21
31
|
LLM_AVAILABLE = True
|
|
@@ -213,6 +223,35 @@ class ReActAgent(BaseAgent):
|
|
|
213
223
|
raw.append(d)
|
|
214
224
|
self.session.add_messages(raw)
|
|
215
225
|
|
|
226
|
+
def _messages_to_dicts(self, msgs: List[LLMMessage]) -> List[Dict[str, Any]]:
|
|
227
|
+
"""Serialize messages for run_state (human-in-loop resume)."""
|
|
228
|
+
out = []
|
|
229
|
+
for m in msgs:
|
|
230
|
+
d: Dict[str, Any] = {"role": m.role, "content": m.content if isinstance(m.content, str) else ""}
|
|
231
|
+
if m.tool_call_id:
|
|
232
|
+
d["tool_call_id"] = m.tool_call_id
|
|
233
|
+
if m.tool_calls:
|
|
234
|
+
d["tool_calls"] = [{"id": t.id, "name": t.name, "arguments": t.arguments} for t in m.tool_calls]
|
|
235
|
+
out.append(d)
|
|
236
|
+
return out
|
|
237
|
+
|
|
238
|
+
def _dicts_to_messages(self, raw: List[Dict[str, Any]]) -> List[LLMMessage]:
|
|
239
|
+
"""Deserialize messages from run_state."""
|
|
240
|
+
out: List[LLMMessage] = []
|
|
241
|
+
for m in raw:
|
|
242
|
+
tc = None
|
|
243
|
+
if m.get("tool_calls"):
|
|
244
|
+
tc = [ToolCall(id=x.get("id", ""), name=x.get("name", ""), arguments=x.get("arguments", {})) for x in m["tool_calls"]]
|
|
245
|
+
out.append(
|
|
246
|
+
LLMMessage(
|
|
247
|
+
role=m["role"],
|
|
248
|
+
content=m.get("content", ""),
|
|
249
|
+
tool_call_id=m.get("tool_call_id"),
|
|
250
|
+
tool_calls=tc,
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
return out
|
|
254
|
+
|
|
216
255
|
async def _run_loop(self, query: str, **kwargs: Any) -> Dict[str, Any]:
|
|
217
256
|
"""Async ReAct loop: Reason -> Act -> Observe until final answer."""
|
|
218
257
|
if self.llm is None:
|
|
@@ -221,43 +260,99 @@ class ReActAgent(BaseAgent):
|
|
|
221
260
|
"or pass llm=YourLLMProvider()."
|
|
222
261
|
)
|
|
223
262
|
|
|
224
|
-
|
|
263
|
+
run_config = kwargs.get("run_config")
|
|
264
|
+
trace_hooks = getattr(run_config, "trace_hooks", None) if run_config else None
|
|
265
|
+
interrupt_tools = getattr(run_config, "interrupt_before_tools", None) or []
|
|
266
|
+
resume_state = kwargs.get("resume")
|
|
267
|
+
resume_value = kwargs.get("resume_value")
|
|
268
|
+
|
|
269
|
+
if resume_state and resume_value is not None:
|
|
270
|
+
messages = self._dicts_to_messages(resume_state["messages"])
|
|
271
|
+
state = AgentState(**resume_state["state"])
|
|
272
|
+
pending = resume_state["pending_tc"]
|
|
273
|
+
messages.append(
|
|
274
|
+
LLMMessage(role="tool", content=str(resume_value), tool_call_id=pending.get("id", ""))
|
|
275
|
+
)
|
|
276
|
+
state = self._update_state(state, tool_calls=[{"name": pending.get("name", ""), "result": str(resume_value)}])
|
|
277
|
+
else:
|
|
278
|
+
state = AgentState(query=query, context=kwargs.get("context", {}))
|
|
279
|
+
history = self._session_to_messages()
|
|
280
|
+
messages = [
|
|
281
|
+
LLMMessage(role="system", content=""),
|
|
282
|
+
*history,
|
|
283
|
+
LLMMessage(role="user", content=query),
|
|
284
|
+
]
|
|
285
|
+
system_prompt = self.llm.get_react_system_prompt(
|
|
286
|
+
get_tool_schemas(self.tools) if self.tools else [],
|
|
287
|
+
self.max_iterations,
|
|
288
|
+
)
|
|
289
|
+
messages[0] = LLMMessage(role="system", content=system_prompt)
|
|
290
|
+
|
|
225
291
|
tool_schemas = get_tool_schemas(self.tools) if self.tools else []
|
|
226
292
|
tool_map = {t.name: t for t in self.tools} if self.tools else {}
|
|
293
|
+
history_len = len(self._session_to_messages())
|
|
294
|
+
total_usage: Dict[str, int] = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0}
|
|
227
295
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
messages: List[LLMMessage] = [
|
|
231
|
-
LLMMessage(role="system", content=system_prompt),
|
|
232
|
-
*history,
|
|
233
|
-
LLMMessage(role="user", content=query),
|
|
234
|
-
]
|
|
296
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
297
|
+
trace_hooks.emit(TraceEvent(type=TraceEventType.RUN_START, agent=self.name, data={"query": query[:200]}))
|
|
235
298
|
|
|
236
299
|
for iteration in range(self.max_iterations):
|
|
237
300
|
if not self._validate_state(state):
|
|
238
301
|
break
|
|
239
302
|
|
|
240
|
-
# After tool results, omit tools to encourage final answer
|
|
241
303
|
has_tool_results = any(m.role == "tool" for m in messages)
|
|
242
304
|
use_tools = tool_schemas and not has_tool_results
|
|
243
305
|
|
|
306
|
+
llm_start = time.perf_counter()
|
|
307
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
308
|
+
trace_hooks.emit(TraceEvent(type=TraceEventType.LLM_START, agent=self.name, data={}))
|
|
309
|
+
|
|
244
310
|
response = await self.llm.agenerate(
|
|
245
311
|
messages,
|
|
246
312
|
tools=tool_schemas if use_tools else None,
|
|
247
313
|
tool_choice="auto" if use_tools else None,
|
|
248
314
|
)
|
|
249
315
|
|
|
316
|
+
if response.usage:
|
|
317
|
+
for k in total_usage:
|
|
318
|
+
total_usage[k] = total_usage.get(k, 0) + response.usage.get(k, 0)
|
|
319
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
320
|
+
llm_data: Dict[str, Any] = {}
|
|
321
|
+
if response.usage:
|
|
322
|
+
llm_data["usage"] = response.usage
|
|
323
|
+
if estimate_cost:
|
|
324
|
+
model = getattr(self.llm, "model_name", "mistral-large-latest")
|
|
325
|
+
llm_data["cost_usd"] = estimate_cost(response.usage, model=model)
|
|
326
|
+
trace_hooks.emit(
|
|
327
|
+
TraceEvent(
|
|
328
|
+
type=TraceEventType.LLM_END,
|
|
329
|
+
agent=self.name,
|
|
330
|
+
data=llm_data,
|
|
331
|
+
duration_ms=(time.perf_counter() - llm_start) * 1000,
|
|
332
|
+
)
|
|
333
|
+
)
|
|
334
|
+
|
|
250
335
|
if response.tool_calls:
|
|
251
|
-
# Append assistant message with tool_calls (required for multi-turn)
|
|
252
336
|
messages.append(
|
|
253
|
-
LLMMessage(
|
|
254
|
-
role="assistant",
|
|
255
|
-
content=response.content or "",
|
|
256
|
-
tool_calls=response.tool_calls,
|
|
257
|
-
)
|
|
337
|
+
LLMMessage(role="assistant", content=response.content or "", tool_calls=response.tool_calls)
|
|
258
338
|
)
|
|
259
|
-
# Execute each tool and append results (preserve tool_call id for matching)
|
|
260
339
|
for tc in response.tool_calls:
|
|
340
|
+
if interrupt_tools and tc.name in interrupt_tools:
|
|
341
|
+
run_state = {
|
|
342
|
+
"messages": self._messages_to_dicts(messages),
|
|
343
|
+
"state": state.model_dump(),
|
|
344
|
+
"pending_tc": {"id": tc.id or f"call_{tc.name}", "name": tc.name, "arguments": tc.arguments},
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
"__interrupted__": True,
|
|
348
|
+
"run_state": run_state,
|
|
349
|
+
"pending_tool": {"name": tc.name, "arguments": tc.arguments},
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
tool_start = time.perf_counter()
|
|
353
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
354
|
+
trace_hooks.emit(TraceEvent(type=TraceEventType.TOOL_START, agent=self.name, data={"tool": tc.name}))
|
|
355
|
+
|
|
261
356
|
tool = tool_map.get(tc.name)
|
|
262
357
|
if tool:
|
|
263
358
|
try:
|
|
@@ -268,33 +363,50 @@ class ReActAgent(BaseAgent):
|
|
|
268
363
|
else:
|
|
269
364
|
result_str = f"Tool '{tc.name}' not found"
|
|
270
365
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
366
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
367
|
+
trace_hooks.emit(
|
|
368
|
+
TraceEvent(
|
|
369
|
+
type=TraceEventType.TOOL_END,
|
|
370
|
+
agent=self.name,
|
|
371
|
+
data={"tool": tc.name, "result_preview": result_str[:100]},
|
|
372
|
+
duration_ms=(time.perf_counter() - tool_start) * 1000,
|
|
373
|
+
)
|
|
276
374
|
)
|
|
375
|
+
|
|
376
|
+
messages.append(
|
|
377
|
+
LLMMessage(role="tool", content=result_str, tool_call_id=tc.id or f"call_{tc.name}")
|
|
277
378
|
)
|
|
278
|
-
state = self._update_state(
|
|
279
|
-
state,
|
|
280
|
-
tool_calls=[{"name": tc.name, "result": result_str}],
|
|
281
|
-
)
|
|
379
|
+
state = self._update_state(state, tool_calls=[{"name": tc.name, "result": result_str}])
|
|
282
380
|
else:
|
|
283
|
-
# Final answer
|
|
284
381
|
state.result = response.content or state.result
|
|
285
382
|
break
|
|
286
383
|
|
|
287
|
-
# Persist to session (user + assistant/tool messages from this turn)
|
|
288
384
|
if self.session:
|
|
289
|
-
to_save = messages[
|
|
385
|
+
to_save = messages[history_len + 2:]
|
|
290
386
|
if to_save:
|
|
291
387
|
self._messages_to_session(to_save)
|
|
292
388
|
|
|
389
|
+
metadata: Dict[str, Any] = {
|
|
390
|
+
"iterations": state.iteration_count,
|
|
391
|
+
"tool_calls": len(state.tool_calls),
|
|
392
|
+
"agent": self.name,
|
|
393
|
+
}
|
|
394
|
+
if total_usage.get("total_tokens"):
|
|
395
|
+
metadata["total_usage"] = total_usage
|
|
396
|
+
if estimate_cost:
|
|
397
|
+
model = getattr(self.llm, "model_name", "mistral-large-latest")
|
|
398
|
+
metadata["cost_usd"] = estimate_cost(total_usage, model=model)
|
|
399
|
+
|
|
400
|
+
if trace_hooks and TRACING_AVAILABLE:
|
|
401
|
+
trace_hooks.emit(
|
|
402
|
+
TraceEvent(
|
|
403
|
+
type=TraceEventType.RUN_END,
|
|
404
|
+
agent=self.name,
|
|
405
|
+
data={"query": query[:200], "usage": total_usage, "result_preview": str(state.result or "")[:200]},
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
|
|
293
409
|
return {
|
|
294
410
|
"result": state.result or "No result generated",
|
|
295
|
-
"metadata":
|
|
296
|
-
"iterations": state.iteration_count,
|
|
297
|
-
"tool_calls": len(state.tool_calls),
|
|
298
|
-
"agent": self.name,
|
|
299
|
-
},
|
|
411
|
+
"metadata": metadata,
|
|
300
412
|
}
|