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.
Files changed (66) hide show
  1. {agentensemble-0.2.0 → agentensemble-0.3.0}/PKG-INFO +103 -24
  2. {agentensemble-0.2.0 → agentensemble-0.3.0}/README.md +102 -23
  3. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/__init__.py +4 -2
  4. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/react_agent.py +145 -33
  5. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/mistral_provider.py +22 -0
  6. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/runner.py +7 -1
  7. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tracing/__init__.py +2 -2
  8. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tracing/hooks.py +40 -1
  9. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/PKG-INFO +103 -24
  10. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/SOURCES.txt +2 -1
  11. {agentensemble-0.2.0 → agentensemble-0.3.0}/pyproject.toml +1 -1
  12. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_runner.py +21 -0
  13. agentensemble-0.3.0/tests/test_tracing.py +49 -0
  14. {agentensemble-0.2.0 → agentensemble-0.3.0}/MANIFEST.in +0 -0
  15. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/__init__.py +0 -0
  16. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/base.py +0 -0
  17. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/hybrid_agent.py +0 -0
  18. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/rag_agent.py +0 -0
  19. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/stategraph_agent.py +0 -0
  20. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/agents/structured_agent.py +0 -0
  21. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/core/__init__.py +0 -0
  22. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/core/protocol.py +0 -0
  23. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/graph/__init__.py +0 -0
  24. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/graph/workflow.py +0 -0
  25. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/__init__.py +0 -0
  26. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/llm/interface.py +0 -0
  27. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/__init__.py +0 -0
  28. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/session.py +0 -0
  29. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/memory/sqlite_session.py +0 -0
  30. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/__init__.py +0 -0
  31. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/debate.py +0 -0
  32. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/ensemble.py +0 -0
  33. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/pipeline.py +0 -0
  34. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/supervisor.py +0 -0
  35. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/orchestration/swarm.py +0 -0
  36. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/planner/__init__.py +0 -0
  37. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/planner/planner_agent.py +0 -0
  38. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/py.typed +0 -0
  39. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/router/__init__.py +0 -0
  40. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/router/router_agent.py +0 -0
  41. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/__init__.py +0 -0
  42. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/benchmark.py +0 -0
  43. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/comparison.py +0 -0
  44. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/testing/metrics.py +0 -0
  45. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/__init__.py +0 -0
  46. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/adapters.py +0 -0
  47. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/function_tool.py +0 -0
  48. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/protocol.py +0 -0
  49. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/rag.py +0 -0
  50. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/registry.py +0 -0
  51. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/scraper.py +0 -0
  52. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/search.py +0 -0
  53. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/tools/validator.py +0 -0
  54. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/utils/__init__.py +0 -0
  55. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble/utils/llm.py +0 -0
  56. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/dependency_links.txt +0 -0
  57. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/requires.txt +0 -0
  58. {agentensemble-0.2.0 → agentensemble-0.3.0}/agentensemble.egg-info/top_level.txt +0 -0
  59. {agentensemble-0.2.0 → agentensemble-0.3.0}/setup.cfg +0 -0
  60. {agentensemble-0.2.0 → agentensemble-0.3.0}/setup.py +0 -0
  61. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_agents.py +0 -0
  62. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_comparison.py +0 -0
  63. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_memory.py +0 -0
  64. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_orchestration.py +0 -0
  65. {agentensemble-0.2.0 → agentensemble-0.3.0}/tests/test_react_session.py +0 -0
  66. {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.2.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="800"/>
79
+ <img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="900"/>
80
80
  </p>
81
81
 
82
- *Architecture, functionalities, and use cases — what AgentEnsemble is capable of.*
83
-
84
- ```
85
- ┌─────────────────────────────────────────────────────────────────┐
86
- │ AgentEnsemble │
87
- ├─────────────┬─────────────┬─────────────┬───────────────────────┤
88
- │ Agents │ Orchestration│ Tools │ Memory & Runner │
89
- ├─────────────┼─────────────┼─────────────┼───────────────────────┤
90
- │ ReActAgent │ Ensemble │ SearchTool │ Session (InMemory, │
91
- │ StateGraph │ Supervisor │ RAGTool │ SQLite) │
92
- │ RAGAgent │ Swarm │ @function_ │ Runner + RunConfig │
93
- │ HybridAgent │ Pipeline │ tool │ TraceHooks │
94
- │ RouterAgent │ Debate │ Validation │ │
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. RAGAgent — Document Q&A (optional: `pip install agentensemble[rag]`)
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
- ### 12. StructuredAgent — Pydantic output (optional: `langchain>=1.1`)
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 10 demos with real API calls.
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 for runs, LLM calls, tools
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 | LangSmith | No | No |
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**: Attach `TraceHooks` to capture run/LLM/tool events
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="800"/>
26
+ <img src="docs/agentensemble-architecture.png" alt="AgentEnsemble Architecture" width="900"/>
27
27
  </p>
28
28
 
29
- *Architecture, functionalities, and use cases — what AgentEnsemble is capable of.*
30
-
31
- ```
32
- ┌─────────────────────────────────────────────────────────────────┐
33
- │ AgentEnsemble │
34
- ├─────────────┬─────────────┬─────────────┬───────────────────────┤
35
- │ Agents │ Orchestration│ Tools │ Memory & Runner │
36
- ├─────────────┼─────────────┼─────────────┼───────────────────────┤
37
- │ ReActAgent │ Ensemble │ SearchTool │ Session (InMemory, │
38
- │ StateGraph │ Supervisor │ RAGTool │ SQLite) │
39
- │ RAGAgent │ Swarm │ @function_ │ Runner + RunConfig │
40
- │ HybridAgent │ Pipeline │ tool │ TraceHooks │
41
- │ RouterAgent │ Debate │ Validation │ │
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. RAGAgent — Document Q&A (optional: `pip install agentensemble[rag]`)
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
- ### 12. StructuredAgent — Pydantic output (optional: `langchain>=1.1`)
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 10 demos with real API calls.
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 for runs, LLM calls, tools
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 | LangSmith | No | No |
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**: Attach `TraceHooks` to capture run/LLM/tool events
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.2.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
- state = AgentState(query=query, context=kwargs.get("context", {}))
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
- system_prompt = self.llm.get_react_system_prompt(tool_schemas, self.max_iterations)
229
- history = self._session_to_messages()
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
- messages.append(
272
- LLMMessage(
273
- role="tool",
274
- content=result_str,
275
- tool_call_id=tc.id or f"call_{tc.name}",
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[len(history) + 2:] # Skip system, history; keep user + new
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
  }