agentensemble 0.4.0__tar.gz → 0.6.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 (82) hide show
  1. agentensemble-0.6.0/CHANGELOG.md +135 -0
  2. agentensemble-0.6.0/PKG-INFO +455 -0
  3. agentensemble-0.6.0/README.md +394 -0
  4. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/__init__.py +2 -2
  5. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/__init__.py +1 -1
  6. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/hybrid_agent.py +21 -21
  7. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/rag_agent.py +5 -2
  8. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/react_agent.py +53 -70
  9. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/stategraph_agent.py +4 -2
  10. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/structured_agent.py +13 -27
  11. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/graph/workflow.py +25 -8
  12. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/llm/__init__.py +15 -2
  13. agentensemble-0.6.0/agentensemble/llm/factory.py +63 -0
  14. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/llm/interface.py +1 -1
  15. agentensemble-0.4.0/agentensemble/llm/mistral_provider.py → agentensemble-0.6.0/agentensemble/llm/langchain_base.py +58 -78
  16. agentensemble-0.6.0/agentensemble/llm/mistral_provider.py +77 -0
  17. agentensemble-0.6.0/agentensemble/llm/openai_provider.py +79 -0
  18. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/memory/sqlite_session.py +23 -6
  19. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/debate.py +3 -1
  20. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/ensemble.py +3 -4
  21. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/py.typed +0 -1
  22. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/router/router_agent.py +12 -18
  23. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/runner.py +5 -12
  24. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/testing/benchmark.py +2 -3
  25. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/testing/comparison.py +1 -3
  26. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/adapters.py +19 -6
  27. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/protocol.py +1 -3
  28. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/rag.py +99 -30
  29. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/registry.py +1 -1
  30. agentensemble-0.6.0/agentensemble/tools/scraper.py +207 -0
  31. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/search.py +16 -14
  32. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/validator.py +9 -11
  33. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tracing/hooks.py +22 -6
  34. agentensemble-0.6.0/agentensemble/utils/__init__.py +21 -0
  35. agentensemble-0.6.0/agentensemble/utils/llm.py +207 -0
  36. agentensemble-0.6.0/agentensemble.egg-info/PKG-INFO +455 -0
  37. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble.egg-info/SOURCES.txt +8 -1
  38. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble.egg-info/requires.txt +2 -0
  39. {agentensemble-0.4.0 → agentensemble-0.6.0}/pyproject.toml +19 -7
  40. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_agents.py +1 -1
  41. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_comparison.py +3 -3
  42. agentensemble-0.6.0/tests/test_components.py +444 -0
  43. agentensemble-0.6.0/tests/test_llm_providers.py +165 -0
  44. agentensemble-0.6.0/tests/test_react_loop.py +200 -0
  45. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_runner.py +1 -1
  46. agentensemble-0.6.0/tests/test_utils_structured.py +114 -0
  47. agentensemble-0.4.0/CHANGELOG.md +0 -73
  48. agentensemble-0.4.0/PKG-INFO +0 -764
  49. agentensemble-0.4.0/README.md +0 -705
  50. agentensemble-0.4.0/agentensemble/tools/scraper.py +0 -106
  51. agentensemble-0.4.0/agentensemble/utils/__init__.py +0 -10
  52. agentensemble-0.4.0/agentensemble/utils/llm.py +0 -107
  53. agentensemble-0.4.0/agentensemble.egg-info/PKG-INFO +0 -764
  54. {agentensemble-0.4.0 → agentensemble-0.6.0}/LICENSE +0 -0
  55. {agentensemble-0.4.0 → agentensemble-0.6.0}/MANIFEST.in +0 -0
  56. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/agents/base.py +0 -0
  57. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/core/__init__.py +0 -0
  58. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/core/protocol.py +0 -0
  59. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/graph/__init__.py +0 -0
  60. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/memory/__init__.py +0 -0
  61. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/memory/session.py +0 -0
  62. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/__init__.py +0 -0
  63. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/pipeline.py +0 -0
  64. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/supervisor.py +0 -0
  65. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/orchestration/swarm.py +0 -0
  66. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/planner/__init__.py +0 -0
  67. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/planner/planner_agent.py +0 -0
  68. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/router/__init__.py +0 -0
  69. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/testing/__init__.py +0 -0
  70. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/testing/metrics.py +0 -0
  71. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/__init__.py +0 -0
  72. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tools/function_tool.py +0 -0
  73. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble/tracing/__init__.py +0 -0
  74. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble.egg-info/dependency_links.txt +0 -0
  75. {agentensemble-0.4.0 → agentensemble-0.6.0}/agentensemble.egg-info/top_level.txt +0 -0
  76. {agentensemble-0.4.0 → agentensemble-0.6.0}/setup.cfg +0 -0
  77. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_memory.py +0 -0
  78. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_orchestration.py +0 -0
  79. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_react_session.py +0 -0
  80. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_release.py +0 -0
  81. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_tools.py +0 -0
  82. {agentensemble-0.4.0 → agentensemble-0.6.0}/tests/test_tracing.py +0 -0
@@ -0,0 +1,135 @@
1
+ # Changelog
2
+
3
+ All notable changes to AgentEnsemble will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.6.0] - 2026-10-31
11
+
12
+ ### Fixed
13
+ - `ReActAgent` session memory: the user's question and the final answer were never saved, so
14
+ follow-up turns lost context. A turn now stores the question, tool exchanges and answer.
15
+ - `ReActAgent` disabled tools on every turn after the first turn that used one (tool messages
16
+ in the history counted as "already called"). Only the current turn is checked now.
17
+ - Session history windows could start with an orphan `tool` message, which chat APIs reject.
18
+ - `RunConfig(session=...)` had no effect on `ReActAgent`.
19
+ - `SQLiteSession` leaked one open connection per call (`with sqlite3.connect()` does not close).
20
+ - `WorkflowGraph`: a node without a matching outgoing edge re-ran until the 50-step limit
21
+ instead of ending the run. `WorkflowGraph` and `StateGraphAgent` reported every node as
22
+ visited; `nodes_visited` now lists the nodes that ran.
23
+ - `ScraperTool(provider="simple")` returned placeholder text instead of fetching the page.
24
+ - `RAGAgent` always reported `fallback_attempts=0` and re-fetched URLs on every fallback.
25
+ - `RAGTool` and `SearchTool` had no tool description, so models saw "Tool: rag" and often
26
+ skipped them. URL loading in `RAGTool` had no timeout and failures were printed.
27
+ - `HybridAgent` blocked the event loop while running sync tools.
28
+ - `import agentensemble` logged a langchain-community USER_AGENT warning.
29
+ - LangChain tool calls were built with `type="function"` instead of `"tool_call"`.
30
+
31
+ ### Added
32
+ - `ScraperTool` fetches with httpx (timeout, redirects, `max_chars`, http(s) only) and converts
33
+ HTML to text; `ScraperTool.fetch()` raises `ScraperError`. New `html_to_text()` helper.
34
+ - `RAGTool.index_texts()` indexes local text without network; `RAGTool(description=...,
35
+ fetch_timeout=...)`.
36
+ - `get_default_embeddings()` / `get_openai_embeddings()`: `RAGTool` embeddings follow the same
37
+ OpenAI-first provider selection as chat models.
38
+ - `WorkflowGraph(max_steps=...)` and a `steps` count in its metadata.
39
+ - `estimate_cost` knows the `gpt-4.1` family and matches dated model names by prefix.
40
+ - Four offline-runnable examples (customer-support RAG, ticket triage, order assistant with
41
+ approval, CSV analytics) and a rule-based `LLMProvider` for running examples without a key.
42
+ - CodeQL workflow, pre-commit config, mypy and coverage gate (85%) in CI; `httpx` is now a
43
+ declared dependency.
44
+
45
+ ### Changed
46
+ - Library code logs through `logging` instead of printing.
47
+ - Ruff rules broadened (SIM, C4, RUF, PTH, RET, PIE); mypy runs with `check_untyped_defs`.
48
+ - README rewritten: architecture diagrams, configuration and API tables, references.
49
+
50
+ ## [0.5.0] - 2026-10-31
51
+
52
+ ### Added
53
+ - **OpenAI support.** New `OpenAILLMProvider` (LangChain `ChatOpenAI`, native tool calling).
54
+ Default model `gpt-4.1-mini`, configurable via `OPENAI_MODEL`; `OPENAI_BASE_URL` for
55
+ OpenAI-compatible endpoints. Temperature is omitted for reasoning models (o-series, gpt-5).
56
+ - `get_default_provider()` / `resolve_provider_name()`: agents now pick OpenAI when
57
+ `OPENAI_API_KEY` is set, otherwise Mistral; `AGENTENSEMBLE_LLM_PROVIDER` forces one.
58
+ - `agentensemble.utils.get_chat_model()` and `get_openai_model()`; `StructuredAgent` uses the
59
+ same OpenAI-or-Mistral selection.
60
+ - `langchain-openai` is now a core dependency.
61
+
62
+ ### Fixed
63
+ - Calling a sync agent API twice (e.g. `agent.run()` then `agent.run()`, or `run()` followed by
64
+ `asyncio.run(agent.arun())`) failed with `RuntimeError: Event loop is closed`. Each sync call
65
+ uses `asyncio.run()`, and the LLM's async HTTP client stayed bound to the first, closed loop.
66
+ Providers now rebuild their chat model when used on a new event loop, and each OpenAI model
67
+ gets its own async HTTP client instead of the process-wide cached one.
68
+
69
+ ### Changed
70
+ - Shared LangChain conversion logic moved to `agentensemble.llm.langchain_base`
71
+ (`LangChainChatProvider`); `MistralLLMProvider` keeps its public API.
72
+
73
+ ## [0.4.0] - 2026-10-31
74
+
75
+ ### Fixed
76
+ - `ReActAgent` can now be constructed without `MISTRAL_API_KEY` (matching `HybridAgent` and
77
+ `RouterAgent`); `run()` / `astream()` raise a clear `RuntimeError` until an LLM is configured.
78
+ Previously the constructor raised `ValueError`, which broke 6 tests and any offline usage.
79
+ - `SearchTool`'s DuckDuckGo fallback works again: `langchain-community` now needs the `ddgs`
80
+ package (the `search` extra previously installed the old `duckduckgo-search`).
81
+ - `SearchTool` creates its backend lazily on first use, so it can be registered in a
82
+ `ToolRegistry` without optional dependencies; a missing backend raises an `ImportError` with
83
+ the `pip install 'agentensemble[search]'` hint. Unknown providers still fail fast.
84
+ - Tool schema type inference uses identity checks (`ann is str`), not equality.
85
+
86
+ ### Added
87
+ - `MISTRAL_MODEL` environment variable to choose the default model for `MistralLLMProvider`.
88
+ - `__version__` read from installed package metadata (`importlib.metadata`).
89
+ - GitHub Actions CI (ruff lint + format check, pytest on Python 3.10–3.13).
90
+ - New tests: ReActAgent tool-calling loop with a fake LLM, SearchTool behaviour, version, model
91
+ override (64 tests total).
92
+ - SECURITY.md, CODE_OF_CONDUCT.md, issue/PR templates, Dependabot, CITATION.cff.
93
+
94
+ ### Changed
95
+ - Requires Python 3.10+ (3.9 is end-of-life); classifiers for 3.10–3.13.
96
+ - Packaging is `pyproject.toml`-only (removed `setup.py` and `requirements.txt`), PEP 639
97
+ `license = "MIT"`, LICENSE and CHANGELOG shipped in the sdist.
98
+ - Dependency floors raised to the versions the code actually needs: `langchain>=1.1`,
99
+ `langchain-core>=1.0`, `langchain-community>=0.3`, `langchain-mistralai>=0.2`. The unused
100
+ direct `langgraph` dependency was dropped (it is still installed transitively by `langchain`).
101
+ - `dev` extra now includes `build` and `twine`; `black` replaced by `ruff format`.
102
+ - Codebase formatted with `ruff format` and modernised type hints (`X | None`).
103
+
104
+ ## [0.3.0] - 2026-03-10
105
+
106
+ ### Added
107
+ - Observability (`TraceHooks`, token usage, cost estimation), human-in-the-loop
108
+ (`interrupt_before_tools`), architecture diagram.
109
+
110
+ ## [0.2.0] - 2026-03-10
111
+
112
+ ### Added
113
+ - `RouterAgent`, `PlannerAgent`, `WorkflowGraph`, tracing.
114
+
115
+ ## [0.1.0] - 2025-12-09
116
+
117
+ ### Added
118
+ - Initial release
119
+ - ReActAgent implementation
120
+ - StateGraphAgent implementation
121
+ - RAGAgent with fallback strategies
122
+ - HybridAgent with intelligent routing
123
+ - Ensemble orchestration patterns (Supervisor, Swarm, Pipeline)
124
+ - Tool ecosystem (Search, Scraper, RAG, Validator)
125
+ - Tool registry for centralized management
126
+ - Testing framework (AgentComparison, Benchmark, Metrics)
127
+ - Base agent class and state management
128
+ - Comprehensive documentation
129
+
130
+ ### Infrastructure
131
+ - Python 3.9+ support
132
+ - pyproject.toml for modern packaging
133
+ - Development dependencies (pytest, black, ruff, mypy)
134
+ - MIT License
135
+ - Contributing guidelines
@@ -0,0 +1,455 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentensemble
3
+ Version: 0.6.0
4
+ Summary: Build, orchestrate, trace, and test multi-agent LLM systems in Python: ReAct, RAG, router, planner, workflow graphs, swarms, debates, and pipelines.
5
+ Author-email: Irfan Ali <irfanali29@hotmail.com>
6
+ Maintainer-email: Irfan Ali <irfanali29@hotmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/irfanalidv/AgentEnsemble
9
+ Project-URL: Documentation, https://github.com/irfanalidv/AgentEnsemble#readme
10
+ Project-URL: Repository, https://github.com/irfanalidv/AgentEnsemble
11
+ Project-URL: Issues, https://github.com/irfanalidv/AgentEnsemble/issues
12
+ Project-URL: Changelog, https://github.com/irfanalidv/AgentEnsemble/blob/main/CHANGELOG.md
13
+ Keywords: ai-agents,multi-agent,multi-agent-systems,agent-orchestration,orchestration,llm,langchain,langgraph,mistral,openai,rag,react-agent,tool-calling,function-calling,agent-testing,agentic-ai,state-graph,workflow,tracing
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Programming Language :: Python :: 3 :: Only
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: langchain>=1.1.0
31
+ Requires-Dist: langchain-core>=1.0.0
32
+ Requires-Dist: langchain-community>=0.3.0
33
+ Requires-Dist: langchain-mistralai>=0.2.0
34
+ Requires-Dist: langchain-openai>=1.0.0
35
+ Requires-Dist: httpx>=0.25.0
36
+ Requires-Dist: pydantic>=2.0.0
37
+ Requires-Dist: typing-extensions>=4.5.0
38
+ Requires-Dist: python-dotenv>=1.0.0
39
+ Provides-Extra: dev
40
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
41
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
42
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
43
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
44
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
45
+ Requires-Dist: build>=1.0.0; extra == "dev"
46
+ Requires-Dist: twine>=5.0.0; extra == "dev"
47
+ Provides-Extra: observability
48
+ Requires-Dist: langsmith>=0.1.0; extra == "observability"
49
+ Requires-Dist: opentelemetry-api>=1.20.0; extra == "observability"
50
+ Provides-Extra: rag
51
+ Requires-Dist: chromadb>=0.4.0; extra == "rag"
52
+ Requires-Dist: sentence-transformers>=2.2.0; extra == "rag"
53
+ Requires-Dist: langchain-text-splitters>=0.0.1; extra == "rag"
54
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == "rag"
55
+ Provides-Extra: search
56
+ Requires-Dist: ddgs>=9.0.0; extra == "search"
57
+ Requires-Dist: google-search-results>=2.4.2; extra == "search"
58
+ Provides-Extra: all
59
+ Requires-Dist: agentensemble[observability,rag,search]; extra == "all"
60
+ Dynamic: license-file
61
+
62
+ <div align="center">
63
+
64
+ # AgentEnsemble
65
+
66
+ **Build, orchestrate, trace, and test multi-agent LLM systems in Python, with OpenAI or Mistral.**
67
+
68
+ [![PyPI version](https://img.shields.io/pypi/v/agentensemble.svg)](https://pypi.org/project/agentensemble/)
69
+ [![Python versions](https://img.shields.io/pypi/pyversions/agentensemble.svg)](https://pypi.org/project/agentensemble/)
70
+ [![Downloads](https://static.pepy.tech/badge/agentensemble)](https://pepy.tech/projects/agentensemble)
71
+ [![Tests](https://img.shields.io/github/actions/workflow/status/irfanalidv/AgentEnsemble/tests.yml?branch=main&label=tests)](https://github.com/irfanalidv/AgentEnsemble/actions/workflows/tests.yml)
72
+ [![CodeQL](https://img.shields.io/github/actions/workflow/status/irfanalidv/AgentEnsemble/codeql.yml?branch=main&label=CodeQL)](https://github.com/irfanalidv/AgentEnsemble/actions/workflows/codeql.yml)
73
+ [![License: MIT](https://img.shields.io/pypi/l/agentensemble.svg)](https://github.com/irfanalidv/AgentEnsemble/blob/main/LICENSE)
74
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
75
+ [![typed: mypy](https://img.shields.io/badge/typed-mypy-blue.svg)](https://mypy-lang.org/)
76
+ [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/irfanalidv/AgentEnsemble/blob/main/CONTRIBUTING.md)
77
+
78
+ </div>
79
+
80
+ ## Why AgentEnsemble
81
+
82
+ Most agent projects need the same handful of pieces: a tool-calling loop, a way to combine
83
+ several agents, memory between turns, a pause before risky actions, and visibility into tokens
84
+ and cost. AgentEnsemble gives you those pieces as small, plain-Python classes behind one
85
+ `run()` / `arun()` interface, so you can start with one agent and grow into routers, pipelines,
86
+ swarms and debates without changing how you call them. LangChain is used only inside the LLM
87
+ providers; agents, tools and results are your own objects and plain dicts.
88
+
89
+ ## Features
90
+
91
+ | Area | What you get |
92
+ | --- | --- |
93
+ | **Agents** | `ReActAgent` (native tool calling), `HybridAgent` (search → RAG → validate), `RAGAgent`, `StructuredAgent` (Pydantic output), `StateGraphAgent`, `RouterAgent`, `PlannerAgent` |
94
+ | **Orchestration** | `Ensemble` with supervisor / swarm (parallel) / pipeline modes, `DebateOrchestrator`, `WorkflowGraph` with conditional edges |
95
+ | **Tools** | `@function_tool` (JSON schema from type hints), `SearchTool` (Serper or DuckDuckGo), `RAGTool` (URLs or local text), `ScraperTool` (httpx or Playwright), `ValidationTool`, `ToolRegistry` |
96
+ | **LLM providers** | OpenAI and Mistral built in, picked from the environment (OpenAI first); any `LLMProvider` subclass works |
97
+ | **Runtime** | `Runner` + `RunConfig`: sessions (`InMemorySession`, `SQLiteSession`), retries, lifecycle hooks, human-in-the-loop interrupts |
98
+ | **Observability** | `TraceHooks` events for runs, LLM calls and tools, token usage, cost estimates, `astream()` events for UIs |
99
+ | **Evaluation** | `AgentComparison`, `Benchmark`, `Metrics` to compare agents on the same tasks |
100
+ | **Quality** | Typed (`py.typed`, mypy-clean), tested with fake LLMs (no network in CI), Python 3.10 to 3.13 |
101
+
102
+ ## Install
103
+
104
+ ```bash
105
+ pip install agentensemble # or: uv add agentensemble
106
+ ```
107
+
108
+ | Extra | Adds | Install |
109
+ | --- | --- | --- |
110
+ | `search` | DuckDuckGo (`ddgs`) and Serper backends for `SearchTool` | `pip install "agentensemble[search]"` |
111
+ | `rag` | Chroma, sentence-transformers, text splitters, BeautifulSoup for `RAGTool` | `pip install "agentensemble[rag]"` |
112
+ | `observability` | LangSmith and OpenTelemetry API | `pip install "agentensemble[observability]"` |
113
+ | `all` | All of the above | `pip install "agentensemble[all]"` |
114
+
115
+ ## Quickstart
116
+
117
+ Needs `OPENAI_API_KEY` (or `MISTRAL_API_KEY`) in the environment or a `.env` file.
118
+
119
+ ```python
120
+ from agentensemble import ReActAgent, function_tool
121
+
122
+
123
+ @function_tool(description="Look up the shipping status of an order by id")
124
+ def order_status(order_id: str) -> str:
125
+ return {"A100": "shipped", "B200": "processing"}.get(order_id, "not found")
126
+
127
+
128
+ agent = ReActAgent(tools=[order_status]) # OpenAI if OPENAI_API_KEY is set, else Mistral
129
+ result = agent.run("Has order A100 shipped yet?")
130
+ print(result["result"])
131
+ print(result["metadata"])
132
+ ```
133
+
134
+ Output from a run with `gpt-4.1-mini` (model wording varies):
135
+
136
+ ```text
137
+ Yes, order A100 has shipped.
138
+ {'iterations': 1, 'tool_calls': 1, 'agent': 'react_agent', 'total_usage': {'input_tokens': 439, 'output_tokens': 24, 'total_tokens': 463}, 'cost_usd': 0.00021400000000000002}
139
+ ```
140
+
141
+ No key yet? Graph workflows run without an LLM, and every script in
142
+ [`examples/`](https://github.com/irfanalidv/AgentEnsemble/tree/main/examples) marked "offline"
143
+ runs with a built-in rule-based stand-in model:
144
+
145
+ ```python
146
+ from agentensemble import WorkflowGraph
147
+
148
+ graph = (
149
+ WorkflowGraph(entry="classify")
150
+ .add_node("classify", lambda s: {"context": {"refund": "refund" in s["query"].lower()}})
151
+ .add_node("refund", lambda s: {"result": "Routed to the refunds team"})
152
+ .add_node("general", lambda s: {"result": "Routed to general support"})
153
+ .add_edge("classify", "refund", lambda s: s["context"]["refund"])
154
+ .add_edge("classify", "general")
155
+ )
156
+ out = graph.run("I want a refund for my order")
157
+ print(out["result"], out["metadata"]["nodes_visited"])
158
+ # Routed to the refunds team ['classify', 'refund']
159
+ ```
160
+
161
+ ## How it works
162
+
163
+ ```mermaid
164
+ flowchart LR
165
+ App["Your code"] --> Runner["Runner.run / arun<br/>RunConfig: session, retries,<br/>hooks, interrupts"]
166
+ Runner --> Orchestration["Ensemble · DebateOrchestrator<br/>RouterAgent · PlannerAgent"]
167
+ Runner --> Agent
168
+ Orchestration -->|"agent.arun(query)"| Agent["Agents<br/>ReActAgent · HybridAgent<br/>RAGAgent · StructuredAgent"]
169
+ Agent -->|"LLMMessage list + ToolSchema list"| Provider["LLMProvider<br/>OpenAILLMProvider · MistralLLMProvider<br/>or your own"]
170
+ Provider -->|"LLMResponse: content / tool_calls / usage"| Agent
171
+ Agent -->|"ainvoke_tool(name, args)"| Tools["Tools<br/>@function_tool · SearchTool · RAGTool<br/>ScraperTool · ValidationTool"]
172
+ Agent <-->|"load / save turn"| Session["Session<br/>InMemorySession · SQLiteSession"]
173
+ Agent -->|"TraceEvent"| Trace["TraceHooks<br/>usage · cost · timings"]
174
+ ```
175
+
176
+ One `ReActAgent` turn, including the optional human approval step:
177
+
178
+ ```mermaid
179
+ sequenceDiagram
180
+ participant R as Runner
181
+ participant A as ReActAgent
182
+ participant S as Session
183
+ participant L as LLMProvider
184
+ participant T as Tool
185
+ R->>A: arun(query, run_config, session)
186
+ A->>S: get_messages(limit=20)
187
+ A->>L: agenerate(system + history + query, tools)
188
+ L-->>A: tool_calls
189
+ alt tool listed in interrupt_before_tools
190
+ A-->>R: {"__interrupted__": True, "run_state", "pending_tool"}
191
+ R->>A: arun(..., resume=run_state, resume_value=approved_result)
192
+ else
193
+ A->>T: ainvoke_tool(name, arguments)
194
+ T-->>A: result (errors are returned as text)
195
+ end
196
+ A->>L: agenerate(... + tool results), no tools
197
+ L-->>A: final answer
198
+ A->>S: add_messages(user, tool exchange, answer)
199
+ A-->>R: {"result": ..., "metadata": {iterations, tool_calls, total_usage, cost_usd}}
200
+ ```
201
+
202
+ **Design decisions**
203
+
204
+ - **One provider interface.** Agents only see `LLMProvider.generate` / `agenerate` and Pydantic
205
+ message models (`agentensemble/llm/interface.py`). The OpenAI and Mistral providers share
206
+ one LangChain adapter (`llm/langchain_base.py`); adding a provider means subclassing
207
+ `LLMProvider` or `LangChainChatProvider`.
208
+ - **Native tool calling.** Tools are described by JSON schemas (inferred from type hints by
209
+ `@function_tool`) and called through the model's function-calling API, not parsed from text.
210
+ - **Bounded tool use per turn.** After the first round of tool results in a turn, the model is
211
+ asked to answer without tools. This keeps cost and latency predictable; `max_iterations`
212
+ caps the loop as well.
213
+ - **Async first.** Every agent has `arun()`; `run()` wraps it with `asyncio.run()`. Providers
214
+ rebuild their HTTP client when used on a new event loop, so repeated sync calls work.
215
+ - **Plain results.** Agents return `{"result": str, "metadata": dict}`. The human-in-the-loop
216
+ `run_state` is a JSON-serialisable dict, so a paused run can be stored and resumed later.
217
+ - **Usable without keys.** LLM agents can be constructed, inspected and registered without an
218
+ API key; `run()` raises a clear `RuntimeError` until a provider is configured.
219
+
220
+ <details>
221
+ <summary>Module map</summary>
222
+
223
+ ```text
224
+ agentensemble/
225
+ ├── agents/ ReActAgent, HybridAgent, RAGAgent, StateGraphAgent, StructuredAgent, BaseAgent
226
+ ├── llm/ LLMProvider, OpenAILLMProvider, MistralLLMProvider, get_default_provider
227
+ ├── tools/ @function_tool, SearchTool, RAGTool, ScraperTool, ValidationTool, ToolRegistry
228
+ ├── orchestration/ Ensemble, Supervisor/Swarm/PipelineOrchestrator, DebateOrchestrator
229
+ ├── router/ RouterAgent (LLM picks an agent via a constrained tool call)
230
+ ├── planner/ PlannerAgent (rule-based decomposition; override _decompose for LLM planning)
231
+ ├── graph/ WorkflowGraph, Edge
232
+ ├── memory/ Session protocol, InMemorySession, SQLiteSession
233
+ ├── tracing/ TraceHooks, TraceEvent, estimate_cost
234
+ ├── testing/ AgentComparison, Benchmark, Metrics
235
+ ├── utils/ get_chat_model, get_default_embeddings, provider helpers
236
+ └── runner.py Runner, RunConfig, RunHooks
237
+ ```
238
+
239
+ <p align="center">
240
+ <img src="https://raw.githubusercontent.com/irfanalidv/AgentEnsemble/main/docs/agentensemble-architecture.png" alt="AgentEnsemble components" width="820"/>
241
+ </p>
242
+
243
+ </details>
244
+
245
+ ## Use cases and examples
246
+
247
+ Runnable scripts in [`examples/`](https://github.com/irfanalidv/AgentEnsemble/tree/main/examples).
248
+ The first four run offline (no key) and switch to OpenAI or Mistral when a key is set; pass
249
+ `--offline` to force offline mode.
250
+
251
+ | Script | Use case | Needs |
252
+ | --- | --- | --- |
253
+ | [`customer_support_rag.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/customer_support_rag.py) | Answer customer questions from a help-center FAQ with `RAGTool.index_texts` and `ReActAgent` | `[rag]` extra; offline |
254
+ | [`support_ticket_triage.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/support_ticket_triage.py) | Route tickets with `WorkflowGraph`; a `ValidationTool` gate escalates bad refund replies | offline |
255
+ | [`order_assistant_approval.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/order_assistant_approval.py) | Order bot with `SQLiteSession` memory, `TraceHooks`, and human approval before refunds | offline |
256
+ | [`sales_csv_analyst.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/sales_csv_analyst.py) | Answer revenue questions over a CSV with deterministic `@function_tool` analytics | offline |
257
+ | [`graph_workflow_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/graph_workflow_example.py), [`runner_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/runner_example.py), [`research_agent_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/research_agent_example.py) | Graphs, `Runner` hooks, planner → router → executor | offline |
258
+ | [`react_llm_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/react_llm_example.py), [`swarm_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/swarm_example.py), [`debate_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/debate_example.py), [`streaming_example.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/streaming_example.py), [`showcase_all.py`](https://github.com/irfanalidv/AgentEnsemble/blob/main/examples/showcase_all.py) | Search agents, parallel swarm, debate, streaming events, full tour | API key (+ `[search]`) |
259
+
260
+ <details>
261
+ <summary>Recipes (need <code>OPENAI_API_KEY</code> or <code>MISTRAL_API_KEY</code>)</summary>
262
+
263
+ **Human approval before a sensitive tool**
264
+
265
+ ```python
266
+ from agentensemble import ReActAgent, RunConfig, Runner, function_tool
267
+
268
+
269
+ @function_tool(description="Send money to a recipient")
270
+ def execute_payment(amount: float, recipient: str) -> str:
271
+ return f"Paid ${amount:.2f} to {recipient}"
272
+
273
+
274
+ agent = ReActAgent(tools=[execute_payment])
275
+ config = RunConfig(interrupt_before_tools=["execute_payment"])
276
+
277
+ result = Runner.run(agent, "Pay $50 to Alice", config=config)
278
+ if result.get("__interrupted__"):
279
+ pending = result["pending_tool"] # {'name': 'execute_payment', 'arguments': {...}}
280
+ approved = execute_payment.run(**pending["arguments"]) # after a human says yes
281
+ result = Runner.run(
282
+ agent,
283
+ "Pay $50 to Alice",
284
+ config=config,
285
+ resume=result["run_state"],
286
+ resume_value=approved,
287
+ )
288
+ print(result["result"])
289
+ ```
290
+
291
+ **Multi-turn memory**
292
+
293
+ ```python
294
+ from agentensemble import InMemorySession, ReActAgent, RunConfig, Runner
295
+
296
+ agent = ReActAgent(name="chatbot")
297
+ config = RunConfig(session=InMemorySession("chat-1")) # or SQLiteSession("chat-1", db_path=...)
298
+ Runner.run(agent, "My name is Priya and I live in Lisbon.", config=config)
299
+ print(Runner.run(agent, "Which city do I live in?", config=config)["result"]) # ... Lisbon.
300
+ ```
301
+
302
+ **Route to the right specialist**
303
+
304
+ ```python
305
+ from agentensemble import ReActAgent, RouterAgent
306
+
307
+ router = RouterAgent(
308
+ agents={"billing": ReActAgent(name="billing"), "tech_support": ReActAgent(name="tech_support")}
309
+ )
310
+ out = router.run("I was charged twice for my subscription this month")
311
+ print(out["metadata"]["routed_to"]) # billing
312
+ ```
313
+
314
+ **Structured output**
315
+
316
+ ```python
317
+ from pydantic import BaseModel, Field
318
+ from agentensemble import StructuredAgent
319
+
320
+
321
+ class ProductReview(BaseModel):
322
+ product: str = Field(description="Product name")
323
+ rating: int = Field(description="1-5 stars")
324
+ summary: str = Field(description="Brief summary")
325
+
326
+
327
+ agent = StructuredAgent(response_format=ProductReview)
328
+ result = agent.run("Review: Acme X1 headphones - 4 stars, great sound, short battery life")
329
+ print(result["structured_response"])
330
+ # product='Acme X1 headphones' rating=4 summary='Great sound quality but battery life is short.'
331
+ ```
332
+
333
+ **Parallel swarm and debate** (`ReActAgent(tools=[])` answers from the model alone)
334
+
335
+ ```python
336
+ from agentensemble import DebateOrchestrator, ReActAgent, SwarmOrchestrator
337
+
338
+ swarm = SwarmOrchestrator(
339
+ agents={"optimist": ReActAgent(name="optimist"), "skeptic": ReActAgent(name="skeptic")}
340
+ )
341
+ views = swarm.perform("Should a 5-person startup self-host its LLM?")
342
+
343
+ debate = DebateOrchestrator(
344
+ solvers=[ReActAgent(name="s1"), ReActAgent(name="s2")],
345
+ aggregator=ReActAgent(name="judge"),
346
+ rounds=2,
347
+ )
348
+ print(debate.debate("A bat and a ball cost $1.10; the bat costs $1 more. Ball price?")["result"])
349
+ ```
350
+
351
+ **Tracing, usage and cost**
352
+
353
+ ```python
354
+ from agentensemble import ReActAgent, RunConfig, Runner, TraceHooks
355
+
356
+ hooks = TraceHooks(on_event=lambda e: print(e.type.value, e.duration_ms))
357
+ result = Runner.run(ReActAgent(), "Hello!", config=RunConfig(trace_hooks=hooks))
358
+ print(result["metadata"].get("total_usage"), result["metadata"].get("cost_usd"))
359
+ ```
360
+
361
+ </details>
362
+
363
+ ## Configuration
364
+
365
+ **Environment variables** (a `.env` file in the working directory is loaded automatically; see
366
+ [`.env.example`](https://github.com/irfanalidv/AgentEnsemble/blob/main/.env.example)):
367
+
368
+ | Variable | Purpose | Default |
369
+ | --- | --- | --- |
370
+ | `OPENAI_API_KEY` | Enables OpenAI; preferred when set | none |
371
+ | `OPENAI_MODEL` | Default model for `OpenAILLMProvider`. Reasoning models (`o*`, `gpt-5*`) run at the API's default temperature | `gpt-4.1-mini` |
372
+ | `OPENAI_BASE_URL` | OpenAI-compatible endpoint (vLLM, LM Studio, proxies) | OpenAI |
373
+ | `MISTRAL_API_KEY` | Enables Mistral; used when no OpenAI key is set | none |
374
+ | `MISTRAL_MODEL` | Default model for `MistralLLMProvider` | `mistral-large-latest` |
375
+ | `AGENTENSEMBLE_LLM_PROVIDER` | Force `openai` or `mistral` | auto |
376
+ | `SERPER_API_KEY` | `SearchTool()` uses Serper when set, else DuckDuckGo | none |
377
+
378
+ **Main options**
379
+
380
+ | Where | Option | Default | Meaning |
381
+ | --- | --- | --- | --- |
382
+ | `ReActAgent` / `HybridAgent` | `llm=` | from env | Any `LLMProvider` instance |
383
+ | | `max_iterations=` | 10 / 15 | Upper bound on loop steps |
384
+ | | `session=` | `None` | Session used when none is passed via `RunConfig` |
385
+ | `RunConfig` | `session`, `hooks`, `trace_hooks` | `None` | Memory, lifecycle callbacks, tracing |
386
+ | | `interrupt_before_tools` | `None` | Tool names that pause the run for approval |
387
+ | | `max_retries`, `retry_on` | `0`, `(Exception,)` | Retry policy for the whole run |
388
+ | `RAGTool` | `embeddings=`, `vector_store=` | env provider, in-memory | Bring your own embeddings or store |
389
+ | | `chunk_size`, `chunk_overlap`, `fetch_timeout` | 1000, 200, 30 s | Splitting and URL loading |
390
+ | `ScraperTool` | `provider`, `timeout`, `max_chars` | `"simple"`, 15 s, 20000 | httpx or Playwright fetch |
391
+ | `WorkflowGraph` | `entry`, `exit_nodes`, `max_steps` | `"start"`, `["end"]`, 50 | Graph execution limits |
392
+
393
+ ## API overview
394
+
395
+ | Object | Kind | Summary |
396
+ | --- | --- | --- |
397
+ | `ReActAgent(tools, llm, session, max_iterations)` | agent | Tool-calling loop; `run`, `arun`, `astream` |
398
+ | `HybridAgent(tools, llm, early_stopping)` | agent | LLM (or fixed) routing over search / RAG / validate |
399
+ | `RAGAgent(tools, fallback_strategies)` | agent | Queries a `RAGTool`, retries with query variations |
400
+ | `StructuredAgent(response_format, model, tools)` | agent | Returns a Pydantic object in `structured_response` |
401
+ | `StateGraphAgent(nodes)` / `WorkflowGraph(nodes, edges)` | graph | Function nodes with routing and conditional edges |
402
+ | `RouterAgent(agents, llm)` | agent | Picks one agent per query; `route_only()` for `Ensemble` |
403
+ | `PlannerAgent(executor, max_subtasks)` | agent | Splits a task into subtasks and runs them |
404
+ | `Ensemble(agents, conductor, router)` | orchestration | `"supervisor"`, `"swarm"` (parallel) or `"pipeline"` |
405
+ | `DebateOrchestrator(solvers, aggregator, rounds)` | orchestration | Proposals, refinement rounds, aggregator decision |
406
+ | `Runner.run / arun(agent, input, config)` | runtime | Applies `RunConfig`: session, hooks, retries, interrupts |
407
+ | `function_tool(name, description)` | tools | Decorator turning a typed function into a tool |
408
+ | `SearchTool`, `RAGTool`, `ScraperTool`, `ValidationTool` | tools | Built-in tools |
409
+ | `InMemorySession`, `SQLiteSession` | memory | Conversation history stores |
410
+ | `TraceHooks`, `TraceEvent`, `estimate_cost` | tracing | Events, token usage, USD estimates from list prices |
411
+ | `get_default_provider()`, `OpenAILLMProvider`, `MistralLLMProvider` | llm | Provider selection and implementations |
412
+ | `AgentComparison`, `Benchmark`, `Metrics` | evaluation | Run agents on the same cases and compare |
413
+
414
+ ## Research and references
415
+
416
+ - **ReAct** (`ReActAgent`): S. Yao, J. Zhao, D. Yu, N. Du, I. Shafran, K. Narasimhan, Y. Cao.
417
+ *ReAct: Synergizing Reasoning and Acting in Language Models.* ICLR 2023. AgentEnsemble runs
418
+ the reason / act / observe loop through native function calling instead of text parsing.
419
+ - **Multi-agent debate** (`DebateOrchestrator`): Y. Du, S. Li, A. Torralba, J. B. Tenenbaum,
420
+ I. Mordatch. *Improving Factuality and Reasoning in Language Models through Multiagent
421
+ Debate.* ICML 2024. Solvers see each other's answers for several rounds before an aggregator
422
+ decides.
423
+ - **Retrieval-augmented generation** (`RAGTool`, `RAGAgent`): P. Lewis et al.
424
+ *Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks.* NeurIPS 2020. Here the
425
+ retriever is a LangChain vector store and the retrieved passages are handed to the agent's LLM.
426
+
427
+ ## When to use it
428
+
429
+ AgentEnsemble fits when you want readable agent code with a small API, OpenAI or Mistral, and
430
+ built-in sessions, tracing and approvals, without adopting a large framework. If you need
431
+ durable, checkpointed graph execution across processes, a very large integration catalogue, or
432
+ a provider other than OpenAI-compatible endpoints and Mistral out of the box, a larger framework
433
+ such as LangGraph may suit you better; AgentEnsemble agents can still be called from it, since
434
+ they are plain Python objects.
435
+
436
+ ## Roadmap
437
+
438
+ - LLM-based planning in `PlannerAgent` (today it is rule-based; `_decompose` can be overridden).
439
+ - Optional multi-round tool use per turn in `ReActAgent`.
440
+ - Token-level streaming in `astream()` (today it streams step events).
441
+ - Async-native `SQLiteSession` and a Redis session.
442
+ - Anthropic and other providers through the same `LLMProvider` interface.
443
+
444
+ ## Contributing, security, citation, license
445
+
446
+ - Contributions are welcome: see [CONTRIBUTING.md](https://github.com/irfanalidv/AgentEnsemble/blob/main/CONTRIBUTING.md)
447
+ and the [Code of Conduct](https://github.com/irfanalidv/AgentEnsemble/blob/main/CODE_OF_CONDUCT.md).
448
+ CI runs `ruff check`, `ruff format --check`, `mypy agentensemble` and `pytest --cov` on
449
+ Python 3.10 to 3.13.
450
+ - Report vulnerabilities as described in [SECURITY.md](https://github.com/irfanalidv/AgentEnsemble/blob/main/SECURITY.md).
451
+ - If you use AgentEnsemble in research, please cite it with
452
+ [CITATION.cff](https://github.com/irfanalidv/AgentEnsemble/blob/main/CITATION.cff) (GitHub's
453
+ "Cite this repository" button reads it).
454
+ - Changes are listed in the [CHANGELOG](https://github.com/irfanalidv/AgentEnsemble/blob/main/CHANGELOG.md).
455
+ - MIT licensed: see [LICENSE](https://github.com/irfanalidv/AgentEnsemble/blob/main/LICENSE).