agentensemble 0.4.0__tar.gz → 0.5.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 (73) hide show
  1. {agentensemble-0.4.0 → agentensemble-0.5.0}/CHANGELOG.md +23 -0
  2. {agentensemble-0.4.0/agentensemble.egg-info → agentensemble-0.5.0}/PKG-INFO +25 -10
  3. {agentensemble-0.4.0 → agentensemble-0.5.0}/README.md +22 -8
  4. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/__init__.py +1 -1
  5. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/hybrid_agent.py +4 -12
  6. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/react_agent.py +6 -12
  7. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/structured_agent.py +2 -14
  8. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/llm/__init__.py +14 -1
  9. agentensemble-0.5.0/agentensemble/llm/factory.py +63 -0
  10. agentensemble-0.4.0/agentensemble/llm/mistral_provider.py → agentensemble-0.5.0/agentensemble/llm/langchain_base.py +57 -77
  11. agentensemble-0.5.0/agentensemble/llm/mistral_provider.py +77 -0
  12. agentensemble-0.5.0/agentensemble/llm/openai_provider.py +79 -0
  13. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/router/router_agent.py +3 -11
  14. agentensemble-0.5.0/agentensemble/utils/__init__.py +17 -0
  15. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/utils/llm.py +51 -1
  16. {agentensemble-0.4.0 → agentensemble-0.5.0/agentensemble.egg-info}/PKG-INFO +25 -10
  17. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble.egg-info/SOURCES.txt +4 -0
  18. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble.egg-info/requires.txt +1 -0
  19. {agentensemble-0.4.0 → agentensemble-0.5.0}/pyproject.toml +3 -1
  20. agentensemble-0.5.0/tests/test_llm_providers.py +165 -0
  21. agentensemble-0.4.0/agentensemble/utils/__init__.py +0 -10
  22. {agentensemble-0.4.0 → agentensemble-0.5.0}/LICENSE +0 -0
  23. {agentensemble-0.4.0 → agentensemble-0.5.0}/MANIFEST.in +0 -0
  24. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/__init__.py +0 -0
  25. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/base.py +0 -0
  26. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/rag_agent.py +0 -0
  27. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/agents/stategraph_agent.py +0 -0
  28. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/core/__init__.py +0 -0
  29. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/core/protocol.py +0 -0
  30. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/graph/__init__.py +0 -0
  31. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/graph/workflow.py +0 -0
  32. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/llm/interface.py +0 -0
  33. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/memory/__init__.py +0 -0
  34. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/memory/session.py +0 -0
  35. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/memory/sqlite_session.py +0 -0
  36. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/__init__.py +0 -0
  37. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/debate.py +0 -0
  38. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/ensemble.py +0 -0
  39. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/pipeline.py +0 -0
  40. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/supervisor.py +0 -0
  41. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/orchestration/swarm.py +0 -0
  42. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/planner/__init__.py +0 -0
  43. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/planner/planner_agent.py +0 -0
  44. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/py.typed +0 -0
  45. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/router/__init__.py +0 -0
  46. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/runner.py +0 -0
  47. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/testing/__init__.py +0 -0
  48. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/testing/benchmark.py +0 -0
  49. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/testing/comparison.py +0 -0
  50. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/testing/metrics.py +0 -0
  51. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/__init__.py +0 -0
  52. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/adapters.py +0 -0
  53. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/function_tool.py +0 -0
  54. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/protocol.py +0 -0
  55. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/rag.py +0 -0
  56. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/registry.py +0 -0
  57. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/scraper.py +0 -0
  58. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/search.py +0 -0
  59. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tools/validator.py +0 -0
  60. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tracing/__init__.py +0 -0
  61. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble/tracing/hooks.py +0 -0
  62. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble.egg-info/dependency_links.txt +0 -0
  63. {agentensemble-0.4.0 → agentensemble-0.5.0}/agentensemble.egg-info/top_level.txt +0 -0
  64. {agentensemble-0.4.0 → agentensemble-0.5.0}/setup.cfg +0 -0
  65. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_agents.py +0 -0
  66. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_comparison.py +0 -0
  67. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_memory.py +0 -0
  68. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_orchestration.py +0 -0
  69. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_react_session.py +0 -0
  70. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_release.py +0 -0
  71. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_runner.py +0 -0
  72. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_tools.py +0 -0
  73. {agentensemble-0.4.0 → agentensemble-0.5.0}/tests/test_tracing.py +0 -0
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2026-10-31
11
+
12
+ ### Added
13
+ - **OpenAI support.** New `OpenAILLMProvider` (LangChain `ChatOpenAI`, native tool calling).
14
+ Default model `gpt-4.1-mini`, configurable via `OPENAI_MODEL`; `OPENAI_BASE_URL` for
15
+ OpenAI-compatible endpoints. Temperature is omitted for reasoning models (o-series, gpt-5).
16
+ - `get_default_provider()` / `resolve_provider_name()`: agents now pick OpenAI when
17
+ `OPENAI_API_KEY` is set, otherwise Mistral; `AGENTENSEMBLE_LLM_PROVIDER` forces one.
18
+ - `agentensemble.utils.get_chat_model()` and `get_openai_model()`; `StructuredAgent` uses the
19
+ same OpenAI-or-Mistral selection.
20
+ - `langchain-openai` is now a core dependency.
21
+
22
+ ### Fixed
23
+ - Calling a sync agent API twice (e.g. `agent.run()` then `agent.run()`, or `run()` followed by
24
+ `asyncio.run(agent.arun())`) failed with `RuntimeError: Event loop is closed`. Each sync call
25
+ uses `asyncio.run()`, and the LLM's async HTTP client stayed bound to the first, closed loop.
26
+ Providers now rebuild their chat model when used on a new event loop, and each OpenAI model
27
+ gets its own async HTTP client instead of the process-wide cached one.
28
+
29
+ ### Changed
30
+ - Shared LangChain conversion logic moved to `agentensemble.llm.langchain_base`
31
+ (`LangChainChatProvider`); `MistralLLMProvider` keeps its public API.
32
+
10
33
  ## [0.4.0] - 2026-10-31
11
34
 
12
35
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentensemble
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Build, orchestrate, trace, and test multi-agent LLM systems in Python: ReAct, RAG, router, planner, workflow graphs, swarms, debates, and pipelines.
5
5
  Author-email: Irfan Ali <irfanali29@hotmail.com>
6
6
  Maintainer-email: Irfan Ali <irfanali29@hotmail.com>
@@ -10,7 +10,7 @@ Project-URL: Documentation, https://github.com/irfanalidv/AgentEnsemble#readme
10
10
  Project-URL: Repository, https://github.com/irfanalidv/AgentEnsemble
11
11
  Project-URL: Issues, https://github.com/irfanalidv/AgentEnsemble/issues
12
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,rag,react-agent,tool-calling,function-calling,agent-testing,agentic-ai,state-graph,workflow,tracing
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
14
  Classifier: Development Status :: 4 - Beta
15
15
  Classifier: Intended Audience :: Developers
16
16
  Classifier: Intended Audience :: Science/Research
@@ -31,6 +31,7 @@ Requires-Dist: langchain>=1.1.0
31
31
  Requires-Dist: langchain-core>=1.0.0
32
32
  Requires-Dist: langchain-community>=0.3.0
33
33
  Requires-Dist: langchain-mistralai>=0.2.0
34
+ Requires-Dist: langchain-openai>=1.0.0
34
35
  Requires-Dist: pydantic>=2.0.0
35
36
  Requires-Dist: typing-extensions>=4.5.0
36
37
  Requires-Dist: python-dotenv>=1.0.0
@@ -121,15 +122,29 @@ print(graph.run("What is multi-agent AI?")["result"])
121
122
  # Summary for: What is multi-agent AI?
122
123
  ```
123
124
 
124
- LLM-powered agents (`ReActAgent`, `HybridAgent`, `RouterAgent`, `StructuredAgent`) use Mistral by
125
- default. Set `MISTRAL_API_KEY`, or pass any `LLMProvider` implementation via `llm=...`.
125
+ LLM-powered agents (`ReActAgent`, `HybridAgent`, `RouterAgent`, `StructuredAgent`) work with
126
+ **OpenAI** or **Mistral**. The provider is picked from your environment: OpenAI if
127
+ `OPENAI_API_KEY` is set, otherwise Mistral if `MISTRAL_API_KEY` is set. You can also pass any
128
+ `LLMProvider` explicitly via `llm=...`:
129
+
130
+ ```python
131
+ from agentensemble import ReActAgent
132
+ from agentensemble.llm import OpenAILLMProvider, MistralLLMProvider
133
+
134
+ agent = ReActAgent(tools=[...], llm=OpenAILLMProvider(model="gpt-4.1-mini"))
135
+ agent = ReActAgent(tools=[...], llm=MistralLLMProvider(model="mistral-small-latest"))
136
+ ```
126
137
 
127
138
  ## Configuration
128
139
 
129
140
  | Variable | Purpose |
130
141
  | --- | --- |
131
- | `MISTRAL_API_KEY` | Required for the default Mistral LLM provider (ReAct, Hybrid, Router, Structured agents). |
142
+ | `OPENAI_API_KEY` | Enables the OpenAI provider (preferred when set). |
143
+ | `OPENAI_MODEL` | Optional default model for `OpenAILLMProvider` (default: `gpt-4.1-mini`). Reasoning models such as `gpt-5-mini` work too; temperature is left at the API default for them. |
144
+ | `OPENAI_BASE_URL` | Optional OpenAI-compatible endpoint (Azure OpenAI proxy, vLLM, LM Studio, ...). |
145
+ | `MISTRAL_API_KEY` | Enables the Mistral provider (used when no OpenAI key is set). |
132
146
  | `MISTRAL_MODEL` | Optional default model for `MistralLLMProvider` (default: `mistral-large-latest`). Use e.g. `mistral-small-latest` if your plan doesn't include the large model. |
147
+ | `AGENTENSEMBLE_LLM_PROVIDER` | Optional. Force `openai` or `mistral` when both keys are set. |
133
148
  | `SERPER_API_KEY` | Optional. When set, `SearchTool()` uses Serper; otherwise it falls back to DuckDuckGo (needs the `search` extra). |
134
149
 
135
150
  Copy `.env.example` to `.env` and fill in your keys (`KEY=value`, no spaces around `=`); the
@@ -144,7 +159,7 @@ Without an API key, LLM agents can still be constructed (and inspected or regist
144
159
 
145
160
  - **Modular** — Agents, tools, orchestration, and memory are decoupled
146
161
  - **Async-first** — `run()` and `arun()` on all agents; parallel execution via `asyncio.gather`
147
- - **Provider-agnostic** — LLM interface supports Mistral (OpenAI/Anthropic-ready)
162
+ - **Provider-agnostic** — one LLM interface, OpenAI and Mistral built in
148
163
  - **Production patterns** — Runner, retries, hooks, tracing, session memory
149
164
 
150
165
  ---
@@ -196,7 +211,7 @@ Without an API key, LLM agents can still be constructed (and inspected or regist
196
211
 
197
212
  ```bash
198
213
  pip install "agentensemble[search]"
199
- export MISTRAL_API_KEY=...
214
+ export OPENAI_API_KEY=... # or: export MISTRAL_API_KEY=...
200
215
  ```
201
216
 
202
217
  ```python
@@ -213,7 +228,7 @@ print(result["result"])
213
228
 
214
229
  ## Examples — Code + Output
215
230
 
216
- Each example is runnable. Requires `MISTRAL_API_KEY` (and the `search` extra for `SearchTool`; `SERPER_API_KEY` is optional). Outputs below are from the author's runs; LLM answers will vary.
231
+ Each example is runnable. Requires `OPENAI_API_KEY` or `MISTRAL_API_KEY` (and the `search` extra for `SearchTool`; `SERPER_API_KEY` is optional). Outputs below are from the author's runs; LLM answers will vary.
217
232
 
218
233
  ---
219
234
 
@@ -697,7 +712,7 @@ Runs all demos with real API calls.
697
712
  agentensemble/
698
713
  ├── agents/ # ReAct, StateGraph, RAG, Hybrid, Structured
699
714
  ├── core/ # AgentProtocol, RunnableProtocol
700
- ├── llm/ # LLMProvider, MistralLLMProvider
715
+ ├── llm/ # LLMProvider, OpenAILLMProvider, MistralLLMProvider, get_default_provider
701
716
  ├── memory/ # Session, InMemorySession, SQLiteSession
702
717
  ├── orchestration/ # Ensemble, Supervisor, Swarm, Pipeline, Debate
703
718
  ├── tools/ # Tool protocol, @function_tool, built-ins
@@ -715,7 +730,7 @@ agentensemble/
715
730
 
716
731
  | Feature | AgentEnsemble | LangGraph | CrewAI | AutoGen |
717
732
  | ------------------ | ------------------------- | ------------ | ------ | ------- |
718
- | Multi-provider LLM | Mistral (extensible) | Any | Any | Any |
733
+ | Multi-provider LLM | OpenAI, Mistral (extensible) | Any | Any | Any |
719
734
  | Async API | `arun()` everywhere | Yes | Yes | Yes |
720
735
  | Router agents | RouterAgent | Manual | Yes | Manual |
721
736
  | Planner pattern | PlannerAgent | Manual | Yes | Manual |
@@ -62,15 +62,29 @@ print(graph.run("What is multi-agent AI?")["result"])
62
62
  # Summary for: What is multi-agent AI?
63
63
  ```
64
64
 
65
- LLM-powered agents (`ReActAgent`, `HybridAgent`, `RouterAgent`, `StructuredAgent`) use Mistral by
66
- default. Set `MISTRAL_API_KEY`, or pass any `LLMProvider` implementation via `llm=...`.
65
+ LLM-powered agents (`ReActAgent`, `HybridAgent`, `RouterAgent`, `StructuredAgent`) work with
66
+ **OpenAI** or **Mistral**. The provider is picked from your environment: OpenAI if
67
+ `OPENAI_API_KEY` is set, otherwise Mistral if `MISTRAL_API_KEY` is set. You can also pass any
68
+ `LLMProvider` explicitly via `llm=...`:
69
+
70
+ ```python
71
+ from agentensemble import ReActAgent
72
+ from agentensemble.llm import OpenAILLMProvider, MistralLLMProvider
73
+
74
+ agent = ReActAgent(tools=[...], llm=OpenAILLMProvider(model="gpt-4.1-mini"))
75
+ agent = ReActAgent(tools=[...], llm=MistralLLMProvider(model="mistral-small-latest"))
76
+ ```
67
77
 
68
78
  ## Configuration
69
79
 
70
80
  | Variable | Purpose |
71
81
  | --- | --- |
72
- | `MISTRAL_API_KEY` | Required for the default Mistral LLM provider (ReAct, Hybrid, Router, Structured agents). |
82
+ | `OPENAI_API_KEY` | Enables the OpenAI provider (preferred when set). |
83
+ | `OPENAI_MODEL` | Optional default model for `OpenAILLMProvider` (default: `gpt-4.1-mini`). Reasoning models such as `gpt-5-mini` work too; temperature is left at the API default for them. |
84
+ | `OPENAI_BASE_URL` | Optional OpenAI-compatible endpoint (Azure OpenAI proxy, vLLM, LM Studio, ...). |
85
+ | `MISTRAL_API_KEY` | Enables the Mistral provider (used when no OpenAI key is set). |
73
86
  | `MISTRAL_MODEL` | Optional default model for `MistralLLMProvider` (default: `mistral-large-latest`). Use e.g. `mistral-small-latest` if your plan doesn't include the large model. |
87
+ | `AGENTENSEMBLE_LLM_PROVIDER` | Optional. Force `openai` or `mistral` when both keys are set. |
74
88
  | `SERPER_API_KEY` | Optional. When set, `SearchTool()` uses Serper; otherwise it falls back to DuckDuckGo (needs the `search` extra). |
75
89
 
76
90
  Copy `.env.example` to `.env` and fill in your keys (`KEY=value`, no spaces around `=`); the
@@ -85,7 +99,7 @@ Without an API key, LLM agents can still be constructed (and inspected or regist
85
99
 
86
100
  - **Modular** — Agents, tools, orchestration, and memory are decoupled
87
101
  - **Async-first** — `run()` and `arun()` on all agents; parallel execution via `asyncio.gather`
88
- - **Provider-agnostic** — LLM interface supports Mistral (OpenAI/Anthropic-ready)
102
+ - **Provider-agnostic** — one LLM interface, OpenAI and Mistral built in
89
103
  - **Production patterns** — Runner, retries, hooks, tracing, session memory
90
104
 
91
105
  ---
@@ -137,7 +151,7 @@ Without an API key, LLM agents can still be constructed (and inspected or regist
137
151
 
138
152
  ```bash
139
153
  pip install "agentensemble[search]"
140
- export MISTRAL_API_KEY=...
154
+ export OPENAI_API_KEY=... # or: export MISTRAL_API_KEY=...
141
155
  ```
142
156
 
143
157
  ```python
@@ -154,7 +168,7 @@ print(result["result"])
154
168
 
155
169
  ## Examples — Code + Output
156
170
 
157
- Each example is runnable. Requires `MISTRAL_API_KEY` (and the `search` extra for `SearchTool`; `SERPER_API_KEY` is optional). Outputs below are from the author's runs; LLM answers will vary.
171
+ Each example is runnable. Requires `OPENAI_API_KEY` or `MISTRAL_API_KEY` (and the `search` extra for `SearchTool`; `SERPER_API_KEY` is optional). Outputs below are from the author's runs; LLM answers will vary.
158
172
 
159
173
  ---
160
174
 
@@ -638,7 +652,7 @@ Runs all demos with real API calls.
638
652
  agentensemble/
639
653
  ├── agents/ # ReAct, StateGraph, RAG, Hybrid, Structured
640
654
  ├── core/ # AgentProtocol, RunnableProtocol
641
- ├── llm/ # LLMProvider, MistralLLMProvider
655
+ ├── llm/ # LLMProvider, OpenAILLMProvider, MistralLLMProvider, get_default_provider
642
656
  ├── memory/ # Session, InMemorySession, SQLiteSession
643
657
  ├── orchestration/ # Ensemble, Supervisor, Swarm, Pipeline, Debate
644
658
  ├── tools/ # Tool protocol, @function_tool, built-ins
@@ -656,7 +670,7 @@ agentensemble/
656
670
 
657
671
  | Feature | AgentEnsemble | LangGraph | CrewAI | AutoGen |
658
672
  | ------------------ | ------------------------- | ------------ | ------ | ------- |
659
- | Multi-provider LLM | Mistral (extensible) | Any | Any | Any |
673
+ | Multi-provider LLM | OpenAI, Mistral (extensible) | Any | Any | Any |
660
674
  | Async API | `arun()` everywhere | Yes | Yes | Yes |
661
675
  | Router agents | RouterAgent | Manual | Yes | Manual |
662
676
  | Planner pattern | PlannerAgent | Manual | Yes | Manual |
@@ -9,7 +9,7 @@ from importlib.metadata import PackageNotFoundError, version
9
9
  try:
10
10
  __version__ = version("agentensemble")
11
11
  except PackageNotFoundError: # running from a source checkout without installation
12
- __version__ = "0.4.0"
12
+ __version__ = "0.5.0"
13
13
  __author__ = "Irfan Ali"
14
14
 
15
15
  # Core imports
@@ -9,17 +9,9 @@ import asyncio
9
9
  from typing import Any
10
10
 
11
11
  from agentensemble.agents.base import AgentState, BaseAgent
12
+ from agentensemble.llm.factory import get_default_provider
12
13
  from agentensemble.llm.interface import LLMMessage, LLMProvider, ToolSchema
13
14
 
14
- try:
15
- from agentensemble.llm.mistral_provider import MistralLLMProvider
16
-
17
- LLM_AVAILABLE = True
18
- except ImportError:
19
- LLM_AVAILABLE = False
20
- MistralLLMProvider = None
21
-
22
-
23
15
  # Routing tool: LLM chooses next action via function calling
24
16
  ROUTE_ACTION_SCHEMA = ToolSchema(
25
17
  name="choose_next_action",
@@ -66,16 +58,16 @@ class HybridAgent(BaseAgent):
66
58
  tools: Available tools (search, rag, validator)
67
59
  max_iterations: Maximum iterations
68
60
  early_stopping: Enable early stopping when answer found
69
- llm: LLM provider for routing (default: MistralLLMProvider if available).
61
+ llm: LLM provider for routing (default: OpenAI or Mistral, picked from the environment).
70
62
  If None, uses fixed pipeline: SEARCH -> RAG -> VALIDATE -> ANSWER
71
63
  **kwargs: Additional configuration
72
64
  """
73
65
  super().__init__(name, tools, max_iterations, **kwargs)
74
66
  self.early_stopping = early_stopping
75
67
  self.llm = llm
76
- if self.llm is None and LLM_AVAILABLE and MistralLLMProvider:
68
+ if self.llm is None:
77
69
  try:
78
- self.llm = MistralLLMProvider(temperature=0.2)
70
+ self.llm = get_default_provider(temperature=0.2)
79
71
  except (ValueError, ImportError):
80
72
  self.llm = None # Fall back to fixed pipeline if no API key
81
73
 
@@ -28,13 +28,7 @@ except ImportError:
28
28
  TraceEvent = None
29
29
  estimate_cost = None
30
30
 
31
- try:
32
- from agentensemble.llm.mistral_provider import MistralLLMProvider
33
-
34
- LLM_AVAILABLE = True
35
- except ImportError:
36
- LLM_AVAILABLE = False
37
- MistralLLMProvider = None
31
+ from agentensemble.llm.factory import get_default_provider
38
32
 
39
33
 
40
34
  class ReActAgent(BaseAgent):
@@ -61,16 +55,16 @@ class ReActAgent(BaseAgent):
61
55
  name: Agent name
62
56
  tools: Available tools (SearchTool, RAGTool, FunctionTool, etc.)
63
57
  max_iterations: Maximum reasoning/acting cycles
64
- llm: LLM provider (default: MistralLLMProvider if available)
58
+ llm: LLM provider (default: OpenAI or Mistral, picked from the environment)
65
59
  session: Optional Session for multi-turn conversation history
66
60
  **kwargs: Additional configuration
67
61
  """
68
62
  super().__init__(name, tools, max_iterations, **kwargs)
69
63
  self.llm = llm
70
64
  self.session = session
71
- if self.llm is None and LLM_AVAILABLE and MistralLLMProvider:
65
+ if self.llm is None:
72
66
  try:
73
- self.llm = MistralLLMProvider(temperature=0.2)
67
+ self.llm = get_default_provider(temperature=0.2)
74
68
  except (ValueError, ImportError):
75
69
  # No API key yet: the agent can still be constructed and inspected;
76
70
  # run()/stream() raise a clear RuntimeError until an LLM is available.
@@ -117,7 +111,7 @@ class ReActAgent(BaseAgent):
117
111
  """Stream events during ReAct loop."""
118
112
  if self.llm is None:
119
113
  raise RuntimeError(
120
- "LLM required for ReActAgent. Install langchain-mistralai and set MISTRAL_API_KEY."
114
+ "LLM required for ReActAgent. Set OPENAI_API_KEY or MISTRAL_API_KEY, or pass llm=."
121
115
  )
122
116
 
123
117
  state = AgentState(query=query, context=kwargs.get("context", {}))
@@ -277,7 +271,7 @@ class ReActAgent(BaseAgent):
277
271
  """Async ReAct loop: Reason -> Act -> Observe until final answer."""
278
272
  if self.llm is None:
279
273
  raise RuntimeError(
280
- "LLM required for ReActAgent. Install langchain-mistralai and set MISTRAL_API_KEY, "
274
+ "LLM required for ReActAgent. Set OPENAI_API_KEY or MISTRAL_API_KEY, or pass llm=, "
281
275
  "or pass llm=YourLLMProvider()."
282
276
  )
283
277
 
@@ -14,13 +14,7 @@ from typing import Any
14
14
 
15
15
  from pydantic import BaseModel
16
16
 
17
- try:
18
- from agentensemble.utils.llm import get_mistral_model
19
-
20
- LLM_AVAILABLE = True
21
- except ImportError:
22
- LLM_AVAILABLE = False
23
- get_mistral_model = None
17
+ from agentensemble.utils.llm import get_chat_model
24
18
 
25
19
  # Try langchain>=1.1 approach (create_agent with response_format)
26
20
  try:
@@ -72,15 +66,9 @@ class StructuredAgent:
72
66
  system_prompt: System prompt for the agent
73
67
  **kwargs: Additional configuration
74
68
  """
75
- if not LLM_AVAILABLE:
76
- raise ImportError(
77
- "langchain-mistralai is required for StructuredAgent. "
78
- "Install with: pip install langchain-mistralai"
79
- )
80
-
81
69
  self.name = name
82
70
  self.tools = tools or []
83
- self.model = model or get_mistral_model()
71
+ self.model = model or get_chat_model(temperature=0.2)
84
72
  self.response_format = response_format
85
73
  self.system_prompt = system_prompt
86
74
  self.config = kwargs
@@ -2,12 +2,13 @@
2
2
  LLM Provider Interface
3
3
 
4
4
  Provider-agnostic LLM abstraction for agent reasoning and tool calling.
5
- Supports Mistral, OpenAI, Anthropic, and local models via a unified interface.
5
+ Ships OpenAI and Mistral providers; any LangChain chat model can be wrapped the same way.
6
6
 
7
7
  Architecture: Clean separation between provider-specific implementations
8
8
  and agent logic. Enables multi-provider support without vendor lock-in.
9
9
  """
10
10
 
11
+ from agentensemble.llm.factory import get_default_provider, resolve_provider_name
11
12
  from agentensemble.llm.interface import (
12
13
  LLMMessage,
13
14
  LLMProvider,
@@ -24,6 +25,14 @@ except ImportError:
24
25
  MISTRAL_AVAILABLE = False
25
26
  MistralLLMProvider = None
26
27
 
28
+ try:
29
+ from agentensemble.llm.openai_provider import OpenAILLMProvider
30
+
31
+ OPENAI_AVAILABLE = True
32
+ except ImportError:
33
+ OPENAI_AVAILABLE = False
34
+ OpenAILLMProvider = None
35
+
27
36
  __all__ = [
28
37
  "LLMProvider",
29
38
  "LLMMessage",
@@ -32,4 +41,8 @@ __all__ = [
32
41
  "ToolSchema",
33
42
  "MistralLLMProvider",
34
43
  "MISTRAL_AVAILABLE",
44
+ "OpenAILLMProvider",
45
+ "OPENAI_AVAILABLE",
46
+ "get_default_provider",
47
+ "resolve_provider_name",
35
48
  ]
@@ -0,0 +1,63 @@
1
+ """
2
+ Default LLM provider selection.
3
+
4
+ Order of precedence:
5
+ 1. ``AGENTENSEMBLE_LLM_PROVIDER`` env var ("openai" or "mistral"), if set.
6
+ 2. OpenAI, if ``OPENAI_API_KEY`` is set.
7
+ 3. Mistral, if ``MISTRAL_API_KEY`` is set.
8
+ """
9
+
10
+ import os
11
+ from typing import Any
12
+
13
+ from agentensemble.llm.interface import LLMProvider
14
+
15
+ PROVIDERS = ("openai", "mistral")
16
+
17
+
18
+ def _build(name: str, temperature: float, **kwargs: Any) -> LLMProvider:
19
+ if name == "openai":
20
+ from agentensemble.llm.openai_provider import OpenAILLMProvider
21
+
22
+ return OpenAILLMProvider(temperature=temperature, **kwargs)
23
+ if name == "mistral":
24
+ from agentensemble.llm.mistral_provider import MistralLLMProvider
25
+
26
+ return MistralLLMProvider(temperature=temperature, **kwargs)
27
+ raise ValueError(f"Unknown LLM provider: {name!r}. Supported: {', '.join(PROVIDERS)}")
28
+
29
+
30
+ def resolve_provider_name() -> str | None:
31
+ """Return the provider that ``get_default_provider`` would use, or None if no key is set."""
32
+ explicit = os.getenv("AGENTENSEMBLE_LLM_PROVIDER", "").strip().lower()
33
+ if explicit:
34
+ return explicit
35
+ if os.getenv("OPENAI_API_KEY"):
36
+ return "openai"
37
+ if os.getenv("MISTRAL_API_KEY"):
38
+ return "mistral"
39
+ return None
40
+
41
+
42
+ def get_default_provider(
43
+ temperature: float = 0.2, provider: str | None = None, **kwargs: Any
44
+ ) -> LLMProvider:
45
+ """
46
+ Build the default LLM provider from the environment.
47
+
48
+ Args:
49
+ temperature: Sampling temperature passed to the provider.
50
+ provider: Force a provider ("openai" or "mistral") instead of auto-detecting.
51
+ **kwargs: Passed to the provider constructor (e.g. ``model=``).
52
+
53
+ Raises:
54
+ ValueError: If no provider can be determined or its API key is missing.
55
+ ImportError: If the provider's LangChain integration is not installed.
56
+ """
57
+ name = (provider or resolve_provider_name() or "").lower()
58
+ if not name:
59
+ raise ValueError(
60
+ "No LLM provider configured. Set OPENAI_API_KEY or MISTRAL_API_KEY "
61
+ "(or AGENTENSEMBLE_LLM_PROVIDER), or pass llm= explicitly."
62
+ )
63
+ return _build(name, temperature, **kwargs)
@@ -1,13 +1,27 @@
1
1
  """
2
- Mistral LLM Provider
2
+ Shared LangChain plumbing for chat-model providers.
3
3
 
4
- Implements LLMProvider using Mistral AI via LangChain.
5
- Uses bind_tools for native function calling (preferred over text parsing).
4
+ Converts between AgentEnsemble's provider-agnostic message/tool models and
5
+ LangChain's chat-model API. Concrete providers (Mistral, OpenAI) only need to
6
+ build the underlying LangChain chat model.
6
7
  """
7
8
 
9
+ import asyncio
8
10
  import json
9
11
  from typing import Any
10
12
 
13
+ from langchain_core.messages import (
14
+ AIMessage,
15
+ BaseMessage,
16
+ HumanMessage,
17
+ SystemMessage,
18
+ ToolMessage,
19
+ )
20
+ from langchain_core.messages import (
21
+ ToolCall as LCToolCall,
22
+ )
23
+ from langchain_core.runnables import Runnable
24
+
11
25
  from agentensemble.llm.interface import (
12
26
  LLMMessage,
13
27
  LLMProvider,
@@ -16,30 +30,9 @@ from agentensemble.llm.interface import (
16
30
  ToolSchema,
17
31
  )
18
32
 
19
- try:
20
- from langchain_core.messages import (
21
- AIMessage,
22
- BaseMessage,
23
- HumanMessage,
24
- SystemMessage,
25
- ToolMessage,
26
- )
27
- from langchain_core.messages import (
28
- ToolCall as LCToolCall,
29
- )
30
- from langchain_core.runnables import Runnable
31
- from langchain_mistralai import ChatMistralAI
32
-
33
- LANGCHAIN_AVAILABLE = True
34
- except ImportError:
35
- LANGCHAIN_AVAILABLE = False
36
- ChatMistralAI = None
37
- LCToolCall = None
38
- Runnable = None
39
-
40
33
 
41
34
  def _tool_schema_to_langchain(schema: ToolSchema) -> dict[str, Any]:
42
- """Convert ToolSchema to OpenAI-style format for Mistral."""
35
+ """Convert ToolSchema to OpenAI-style function format (also accepted by Mistral)."""
43
36
  return {
44
37
  "type": "function",
45
38
  "function": {
@@ -52,7 +45,7 @@ def _tool_schema_to_langchain(schema: ToolSchema) -> dict[str, Any]:
52
45
  }
53
46
 
54
47
 
55
- def _messages_to_langchain(messages: list[LLMMessage]) -> list["BaseMessage"]:
48
+ def _messages_to_langchain(messages: list[LLMMessage]) -> list[BaseMessage]:
56
49
  """Convert LLMMessage list to LangChain format."""
57
50
  result: list[BaseMessage] = []
58
51
  for msg in messages:
@@ -86,7 +79,7 @@ def _messages_to_langchain(messages: list[LLMMessage]) -> list["BaseMessage"]:
86
79
  return result
87
80
 
88
81
 
89
- def _extract_usage(msg: "AIMessage") -> dict[str, int] | None:
82
+ def _extract_usage(msg: AIMessage) -> dict[str, int] | None:
90
83
  """Extract token usage from LangChain AIMessage (usage_metadata or response_metadata)."""
91
84
  um = getattr(msg, "usage_metadata", None)
92
85
  if um and isinstance(um, dict):
@@ -96,7 +89,7 @@ def _extract_usage(msg: "AIMessage") -> dict[str, int] | None:
96
89
  "total_tokens": um.get("total_tokens", 0),
97
90
  }
98
91
  rm = getattr(msg, "response_metadata", None) or {}
99
- usage = rm.get("usage") or rm.get("usage_metadata") or {}
92
+ usage = rm.get("usage") or rm.get("usage_metadata") or rm.get("token_usage") or {}
100
93
  if usage:
101
94
  return {
102
95
  "input_tokens": usage.get("input_tokens", usage.get("prompt_tokens", 0)),
@@ -129,7 +122,7 @@ def _parse_ai_message(msg: AIMessage) -> LLMResponse:
129
122
  args = {}
130
123
  tool_calls.append(
131
124
  ToolCall(
132
- id=tc_id,
125
+ id=tc_id or "",
133
126
  name=tc_name,
134
127
  arguments=args if isinstance(args, dict) else {},
135
128
  )
@@ -143,71 +136,57 @@ def _parse_ai_message(msg: AIMessage) -> LLMResponse:
143
136
  )
144
137
 
145
138
 
146
- class MistralLLMProvider(LLMProvider):
139
+ class LangChainChatProvider(LLMProvider):
147
140
  """
148
- Mistral AI provider using LangChain's ChatMistralAI.
141
+ Base provider wrapping any LangChain chat model that supports ``bind_tools``.
149
142
 
150
- Uses bind_tools for native function calling—cleaner than
151
- THOUGHT/ACTION/PAUSE text parsing (LangChain 2024 best practice).
143
+ Subclasses implement ``_build_model()``, call ``_init_model()`` at the end of
144
+ ``__init__``, set ``self.model_name``, and may override ``REQUIRED_TOOL_CHOICE``
145
+ with the provider's spelling of "must call a tool".
146
+
147
+ Async HTTP clients are bound to the event loop they were first used on. The sync
148
+ agent APIs call ``asyncio.run()`` (a fresh loop per call), so the model is rebuilt
149
+ whenever ``agenerate`` runs on a different loop than last time.
152
150
  """
153
151
 
154
- def __init__(
155
- self,
156
- model: str | None = None,
157
- temperature: float = 0.2,
158
- api_key: str | None = None,
159
- **kwargs: Any,
160
- ):
161
- """
162
- Initialize Mistral provider.
163
-
164
- Args:
165
- model: Model name (e.g., mistral-large-latest, mistral-small-latest). Defaults to
166
- the ``MISTRAL_MODEL`` env var, else ``mistral-large-latest``.
167
- temperature: Lower = more deterministic (0.2 recommended for tool use)
168
- api_key: Mistral API key (defaults to MISTRAL_API_KEY env)
169
- **kwargs: Passed to ChatMistralAI
170
- """
171
- if not LANGCHAIN_AVAILABLE:
172
- raise ImportError(
173
- "langchain-mistralai and langchain-core required. "
174
- "Install: pip install langchain-mistralai langchain-core"
175
- )
152
+ #: Value sent to the API when callers ask for ``tool_choice="required"``.
153
+ REQUIRED_TOOL_CHOICE: str = "required"
176
154
 
177
- import os
155
+ _model: Any
156
+ _model_loop: asyncio.AbstractEventLoop | None = None
157
+ model_name: str
178
158
 
179
- model = model or os.getenv("MISTRAL_MODEL") or "mistral-large-latest"
180
- key = api_key or os.getenv("MISTRAL_API_KEY")
181
- if not key:
182
- raise ValueError("MISTRAL_API_KEY required. Set in .env or pass api_key.")
159
+ def _build_model(self) -> Any:
160
+ """Create the underlying LangChain chat model."""
161
+ raise NotImplementedError
183
162
 
184
- self._model = ChatMistralAI(
185
- model=model,
186
- temperature=temperature,
187
- mistral_api_key=key,
188
- **kwargs,
189
- )
190
- self.model_name = model
163
+ def _init_model(self) -> None:
164
+ self._model = self._build_model()
165
+ self._model_loop = None
166
+
167
+ def _ensure_model_for_running_loop(self) -> None:
168
+ loop = asyncio.get_running_loop()
169
+ if self._model_loop is loop:
170
+ return
171
+ if self._model_loop is not None:
172
+ self._model = self._build_model()
173
+ self._model_loop = loop
191
174
 
192
175
  def _bind_tools(
193
176
  self,
194
177
  tools: list[ToolSchema] | None = None,
195
178
  tool_choice: str | None = None,
196
- ) -> "Runnable":
179
+ ) -> Runnable:
197
180
  """Bind tools to model for function calling."""
198
181
  if not tools:
199
182
  return self._model
200
183
 
201
184
  tool_defs = [_tool_schema_to_langchain(t) for t in tools]
202
- bound = self._model.bind_tools(tool_defs)
203
-
204
- if tool_choice == "required" and tools:
205
- # Mistral: use "any" to force tool use (required -> any per Mistral API)
206
- bound = bound.bind(tool_choice="any")
207
- elif tool_choice and tool_choice != "auto":
208
- bound = bound.bind(tool_choice=tool_choice)
209
-
210
- return bound
185
+ if tool_choice == "required":
186
+ return self._model.bind_tools(tool_defs, tool_choice=self.REQUIRED_TOOL_CHOICE)
187
+ if tool_choice and tool_choice != "auto":
188
+ return self._model.bind_tools(tool_defs, tool_choice=tool_choice)
189
+ return self._model.bind_tools(tool_defs)
211
190
 
212
191
  def generate(
213
192
  self,
@@ -230,6 +209,7 @@ class MistralLLMProvider(LLMProvider):
230
209
  **kwargs: Any,
231
210
  ) -> LLMResponse:
232
211
  """Async generation with optional tool calling."""
212
+ self._ensure_model_for_running_loop()
233
213
  lc_messages = _messages_to_langchain(messages)
234
214
  bound = self._bind_tools(tools, tool_choice)
235
215
  response = await bound.ainvoke(lc_messages, **kwargs)