hexastack-ai 0.1.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.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexastack-ai
3
+ Version: 0.1.0
4
+ Summary: AI engine, LLM provider integration (LiteLLM, Instructor, PydanticAI), and CQRS agent reflection for Hexastack
5
+ Author: Richard West
6
+ Author-email: Richard West <dopplereffect.us@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Dist: hexastack-core
9
+ Requires-Dist: hexastack-cqrs
10
+ Requires-Dist: instructor>=1.7.0
11
+ Requires-Dist: litellm>=1.50.0
12
+ Requires-Dist: pydantic>=2.10.0
13
+ Requires-Dist: pydantic-ai>=0.0.24
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+
17
+ ![hexastack-ai](../../docs/assets/static/logos/packages/hexastack_ai.png)
18
+
19
+ # hexastack-ai
20
+
21
+ AI engine, LLM provider integration (LiteLLM, Instructor, PydanticAI), and CQRS agent tool reflection for the **Hexastack** hexagonal architecture framework.
22
+
23
+ [![PyPI: hexastack-ai](https://img.shields.io/pypi/v/hexastack-ai.svg)](https://pypi.org/project/hexastack-ai/)
24
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
25
+ [![Coverage](https://codecov.io/github/TheTrueSCU/hexastack/graph/badge.svg?component=hexastack_ai)](https://codecov.io/github/TheTrueSCU/hexastack)
26
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../../LICENSE)
27
+
28
+ ---
29
+
30
+ ## 1. Overview
31
+
32
+ `hexastack-ai` integrates an agnostic, production-grade AI stack directly into the Hexastack architecture:
33
+ - **LiteLLM**: Unified driver abstraction across 100+ LLM providers (OpenAI, Anthropic Claude, Google Gemini, Ollama, Groq, Bedrock).
34
+ - **Instructor**: Self-correcting structured output validation returning strongly typed Pydantic models.
35
+ - **PydanticAI**: Type-safe agent loop orchestration and tool execution.
36
+ - **CQRS Tool Reflection**: Automatically turns Hexastack CQRS Commands and Queries into callable AI Agent tools.
37
+
38
+ ---
39
+
40
+ ## 2. Architecture & Relationships
41
+
42
+ ```mermaid
43
+ graph TD
44
+ subgraph Core ["hexastack-core"]
45
+ PORT["LlmProviderPort"]
46
+ MEM["InMemoryLlmProvider"]
47
+ end
48
+
49
+ subgraph AI ["hexastack-ai"]
50
+ BOOT["AiBootstrapper (order=18)"]
51
+ ADAPTER["LiteLlmAdapter"]
52
+ TOOLS["create_cqrs_agent / create_tool_for_message"]
53
+ AGENT_ADAPTER["PydanticAiAgentAdapter"]
54
+ end
55
+
56
+ subgraph CQRS ["hexastack-cqrs"]
57
+ PIPELINE["ExecutionPipeline"]
58
+ CMDS["Commands & Queries"]
59
+ end
60
+
61
+ subgraph UpstreamAI ["Agnostic AI Stack"]
62
+ LITE["LiteLLM (100+ Providers)"]
63
+ INST["Instructor (Schema Validation)"]
64
+ PY_AI["PydanticAI (Agent Execution)"]
65
+ end
66
+
67
+ BOOT -->|binds into DI| PORT
68
+ ADAPTER -. implements .-> PORT
69
+ ADAPTER --> LITE
70
+ ADAPTER --> INST
71
+ TOOLS --> PIPELINE
72
+ TOOLS --> PY_AI
73
+ AGENT_ADAPTER --> PY_AI
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 3. Installation
79
+
80
+ ```bash
81
+ # Standalone install
82
+ pip install hexastack-ai
83
+
84
+ # Via umbrella package
85
+ pip install "hexastack[ai]"
86
+ ```
87
+
88
+ ---
89
+
90
+ ## 4. Configuration Reference
91
+
92
+ ```toml
93
+ [hexastack.ai]
94
+ # Provider: "memory" (default for testing), "litellm", "openai", "anthropic", "gemini", "ollama"
95
+ provider = "litellm"
96
+ model = "gpt-4o-mini"
97
+ temperature = 0.2
98
+ max_tokens = 2048
99
+ api_key = "sk-..." # Or set standard env var OPENAI_API_KEY / ANTHROPIC_API_KEY
100
+
101
+ # LiteLLM Dialect Settings
102
+ [hexastack.ai.litellm]
103
+ drop_params = true
104
+ num_retries = 3
105
+ timeout = 60.0
106
+ api_base = "http://localhost:4000" # Optional LiteLLM proxy URL
107
+
108
+ # Ollama Local Dialect Settings
109
+ [hexastack.ai.ollama]
110
+ base_url = "http://localhost:11434"
111
+
112
+ # PydanticAI Agent Settings
113
+ [hexastack.ai.agent]
114
+ max_turns = 10
115
+ system_prompt = "You are a helpful AI assistant."
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 5. Usage Examples
121
+
122
+ ### 1. Structured Output Extraction
123
+
124
+ ```python
125
+ from pydantic import BaseModel
126
+ from hexastack_core.ports.ai import LlmProviderPort
127
+
128
+
129
+ class InvoiceDTO(BaseModel):
130
+ customer_id: str
131
+ total_amount: float
132
+ items: list[str]
133
+
134
+
135
+ def extract_invoice(llm: LlmProviderPort, text: str) -> InvoiceDTO:
136
+ return llm.generate_structured(
137
+ prompt=f"Extract invoice details from: {text}",
138
+ response_schema=InvoiceDTO,
139
+ )
140
+ ```
141
+
142
+ ### 2. Auto-Reflecting CQRS Commands as Agent Tools
143
+
144
+ ```python
145
+ from hexastack_core.domain import Command, Query
146
+ from hexastack_ai.infra.tools import create_cqrs_agent
147
+
148
+
149
+ class CancelSubscriptionCommand(Command):
150
+ user_id: str
151
+ reason: str
152
+
153
+
154
+ class GetUserPlanQuery(Query[str]):
155
+ user_id: str
156
+
157
+
158
+ # Create PydanticAI agent with CQRS handlers as native tools:
159
+ agent = create_cqrs_agent(
160
+ pipeline=runtime.pipeline,
161
+ messages=[CancelSubscriptionCommand, GetUserPlanQuery],
162
+ model="anthropic/claude-3-5-sonnet",
163
+ system_prompt="You are a customer support agent.",
164
+ )
165
+
166
+ # Agent selects appropriate tools and executes them via Hexastack ExecutionPipeline:
167
+ result = agent.run_sync("Cancel subscription for user 123 due to pricing.")
168
+ ```
@@ -0,0 +1,152 @@
1
+ ![hexastack-ai](../../docs/assets/static/logos/packages/hexastack_ai.png)
2
+
3
+ # hexastack-ai
4
+
5
+ AI engine, LLM provider integration (LiteLLM, Instructor, PydanticAI), and CQRS agent tool reflection for the **Hexastack** hexagonal architecture framework.
6
+
7
+ [![PyPI: hexastack-ai](https://img.shields.io/pypi/v/hexastack-ai.svg)](https://pypi.org/project/hexastack-ai/)
8
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
9
+ [![Coverage](https://codecov.io/github/TheTrueSCU/hexastack/graph/badge.svg?component=hexastack_ai)](https://codecov.io/github/TheTrueSCU/hexastack)
10
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](../../LICENSE)
11
+
12
+ ---
13
+
14
+ ## 1. Overview
15
+
16
+ `hexastack-ai` integrates an agnostic, production-grade AI stack directly into the Hexastack architecture:
17
+ - **LiteLLM**: Unified driver abstraction across 100+ LLM providers (OpenAI, Anthropic Claude, Google Gemini, Ollama, Groq, Bedrock).
18
+ - **Instructor**: Self-correcting structured output validation returning strongly typed Pydantic models.
19
+ - **PydanticAI**: Type-safe agent loop orchestration and tool execution.
20
+ - **CQRS Tool Reflection**: Automatically turns Hexastack CQRS Commands and Queries into callable AI Agent tools.
21
+
22
+ ---
23
+
24
+ ## 2. Architecture & Relationships
25
+
26
+ ```mermaid
27
+ graph TD
28
+ subgraph Core ["hexastack-core"]
29
+ PORT["LlmProviderPort"]
30
+ MEM["InMemoryLlmProvider"]
31
+ end
32
+
33
+ subgraph AI ["hexastack-ai"]
34
+ BOOT["AiBootstrapper (order=18)"]
35
+ ADAPTER["LiteLlmAdapter"]
36
+ TOOLS["create_cqrs_agent / create_tool_for_message"]
37
+ AGENT_ADAPTER["PydanticAiAgentAdapter"]
38
+ end
39
+
40
+ subgraph CQRS ["hexastack-cqrs"]
41
+ PIPELINE["ExecutionPipeline"]
42
+ CMDS["Commands & Queries"]
43
+ end
44
+
45
+ subgraph UpstreamAI ["Agnostic AI Stack"]
46
+ LITE["LiteLLM (100+ Providers)"]
47
+ INST["Instructor (Schema Validation)"]
48
+ PY_AI["PydanticAI (Agent Execution)"]
49
+ end
50
+
51
+ BOOT -->|binds into DI| PORT
52
+ ADAPTER -. implements .-> PORT
53
+ ADAPTER --> LITE
54
+ ADAPTER --> INST
55
+ TOOLS --> PIPELINE
56
+ TOOLS --> PY_AI
57
+ AGENT_ADAPTER --> PY_AI
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 3. Installation
63
+
64
+ ```bash
65
+ # Standalone install
66
+ pip install hexastack-ai
67
+
68
+ # Via umbrella package
69
+ pip install "hexastack[ai]"
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 4. Configuration Reference
75
+
76
+ ```toml
77
+ [hexastack.ai]
78
+ # Provider: "memory" (default for testing), "litellm", "openai", "anthropic", "gemini", "ollama"
79
+ provider = "litellm"
80
+ model = "gpt-4o-mini"
81
+ temperature = 0.2
82
+ max_tokens = 2048
83
+ api_key = "sk-..." # Or set standard env var OPENAI_API_KEY / ANTHROPIC_API_KEY
84
+
85
+ # LiteLLM Dialect Settings
86
+ [hexastack.ai.litellm]
87
+ drop_params = true
88
+ num_retries = 3
89
+ timeout = 60.0
90
+ api_base = "http://localhost:4000" # Optional LiteLLM proxy URL
91
+
92
+ # Ollama Local Dialect Settings
93
+ [hexastack.ai.ollama]
94
+ base_url = "http://localhost:11434"
95
+
96
+ # PydanticAI Agent Settings
97
+ [hexastack.ai.agent]
98
+ max_turns = 10
99
+ system_prompt = "You are a helpful AI assistant."
100
+ ```
101
+
102
+ ---
103
+
104
+ ## 5. Usage Examples
105
+
106
+ ### 1. Structured Output Extraction
107
+
108
+ ```python
109
+ from pydantic import BaseModel
110
+ from hexastack_core.ports.ai import LlmProviderPort
111
+
112
+
113
+ class InvoiceDTO(BaseModel):
114
+ customer_id: str
115
+ total_amount: float
116
+ items: list[str]
117
+
118
+
119
+ def extract_invoice(llm: LlmProviderPort, text: str) -> InvoiceDTO:
120
+ return llm.generate_structured(
121
+ prompt=f"Extract invoice details from: {text}",
122
+ response_schema=InvoiceDTO,
123
+ )
124
+ ```
125
+
126
+ ### 2. Auto-Reflecting CQRS Commands as Agent Tools
127
+
128
+ ```python
129
+ from hexastack_core.domain import Command, Query
130
+ from hexastack_ai.infra.tools import create_cqrs_agent
131
+
132
+
133
+ class CancelSubscriptionCommand(Command):
134
+ user_id: str
135
+ reason: str
136
+
137
+
138
+ class GetUserPlanQuery(Query[str]):
139
+ user_id: str
140
+
141
+
142
+ # Create PydanticAI agent with CQRS handlers as native tools:
143
+ agent = create_cqrs_agent(
144
+ pipeline=runtime.pipeline,
145
+ messages=[CancelSubscriptionCommand, GetUserPlanQuery],
146
+ model="anthropic/claude-3-5-sonnet",
147
+ system_prompt="You are a customer support agent.",
148
+ )
149
+
150
+ # Agent selects appropriate tools and executes them via Hexastack ExecutionPipeline:
151
+ result = agent.run_sync("Cancel subscription for user 123 due to pricing.")
152
+ ```
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "hexastack-ai"
3
+ version = "0.1.0"
4
+ description = "AI engine, LLM provider integration (LiteLLM, Instructor, PydanticAI), and CQRS agent reflection for Hexastack"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "hexastack-core",
10
+ "hexastack-cqrs",
11
+ "instructor>=1.7.0",
12
+ "litellm>=1.50.0",
13
+ "pydantic>=2.10.0",
14
+ "pydantic-ai>=0.0.24",
15
+ ]
16
+
17
+ [[project.authors]]
18
+ name = "Richard West"
19
+ email = "dopplereffect.us@gmail.com"
20
+
21
+ [project.entry-points."hexastack.bootstrappers"]
22
+ ai = "hexastack_ai.infra.bootstrap:AiBootstrapper"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.12.3,<0.13.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [tool.uv.sources.hexastack-core]
29
+ workspace = true
30
+
31
+ [tool.uv.sources.hexastack-cqrs]
32
+ workspace = true
33
+
34
+ [tool.importlinter]
35
+ root_packages = ["hexastack_ai"]
36
+
37
+ [[tool.importlinter.contracts]]
38
+ name = "Hexagonal architecture layer hierarchy"
39
+ type = "layers"
40
+ containers = ["hexastack_ai"]
41
+ layers = [
42
+ "adapters",
43
+ "domain",
44
+ ]
45
+
46
+ [[tool.importlinter.contracts]]
47
+ name = "Forbidden imports for domain"
48
+ type = "forbidden"
49
+ source_modules = ["hexastack_ai.domain"]
50
+ forbidden_modules = [
51
+ "hexastack_ai.adapters",
52
+ "hexastack_ai.infra",
53
+ ]
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "hexastack-ai"
3
+ version = "0.1.0"
4
+ description = "AI engine, LLM provider integration (LiteLLM, Instructor, PydanticAI), and CQRS agent reflection for Hexastack"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ authors = [
8
+ { name = "Richard West", email = "dopplereffect.us@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "hexastack-core",
13
+ "hexastack-cqrs",
14
+ "instructor>=1.7.0",
15
+ "litellm>=1.50.0",
16
+ "pydantic>=2.10.0",
17
+ "pydantic-ai>=0.0.24",
18
+ ]
19
+
20
+ [project.entry-points."hexastack.bootstrappers"]
21
+ ai = "hexastack_ai.infra.bootstrap:AiBootstrapper"
22
+
23
+ [build-system]
24
+ requires = ["uv_build>=0.12.3,<0.13.0"]
25
+ build-backend = "uv_build"
26
+
27
+ [tool.uv.sources]
28
+ hexastack-core = { workspace = true }
29
+ hexastack-cqrs = { workspace = true }
30
+
31
+ [tool.importlinter]
32
+ root_packages = ["hexastack_ai"]
33
+
34
+ [[tool.importlinter.contracts]]
35
+ name = "Hexagonal architecture layer hierarchy"
36
+ type = "layers"
37
+ containers = ["hexastack_ai"]
38
+ layers = [
39
+ "adapters",
40
+ "domain",
41
+ ]
42
+
43
+ [[tool.importlinter.contracts]]
44
+ name = "Forbidden imports for domain"
45
+ type = "forbidden"
46
+ source_modules = ["hexastack_ai.domain"]
47
+ forbidden_modules = [
48
+ "hexastack_ai.adapters",
49
+ "hexastack_ai.infra",
50
+ ]
@@ -0,0 +1,7 @@
1
+ from hexastack_ai import adapters, domain, infra
2
+
3
+ __all__ = [
4
+ "adapters",
5
+ "domain",
6
+ "infra",
7
+ ]
@@ -0,0 +1,7 @@
1
+ from hexastack_ai.adapters.litellm import LiteLlmAdapter
2
+ from hexastack_ai.adapters.pydantic_ai import PydanticAiAgentAdapter
3
+
4
+ __all__ = [
5
+ "LiteLlmAdapter",
6
+ "PydanticAiAgentAdapter",
7
+ ]
@@ -0,0 +1,226 @@
1
+ import inspect
2
+ from typing import Any
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from hexastack_ai.domain.exceptions import (
7
+ LlmProviderError,
8
+ StructuredOutputParsingError,
9
+ )
10
+ from hexastack_ai.infra.config import HexastackAiConfig
11
+ from hexastack_core.ports.ai import LlmProviderPort
12
+
13
+
14
+ class LiteLlmAdapter(LlmProviderPort):
15
+ """LiteLLM and Instructor adapter implementing LlmProviderPort.
16
+
17
+ Notes/Architectural Intent:
18
+ Wraps LiteLLM to provide vendor-agnostic LLM calls across 100+ providers
19
+ (OpenAI, Claude, Gemini, Ollama, Bedrock) while leveraging Instructor
20
+ for self-correcting structured output validation.
21
+ """
22
+
23
+ def __init__(self, config: HexastackAiConfig | None = None) -> None:
24
+ """Initialize LiteLlmAdapter with configuration.
25
+
26
+ Args:
27
+ config: HexastackAiConfig instance.
28
+ """
29
+ self._config = config or HexastackAiConfig()
30
+
31
+ @property
32
+ def _resolved_model(self) -> str:
33
+ """Resolve full model identifier ensuring provider prefix like 'gemini/' is handled."""
34
+ model = self._config.model
35
+ provider = self._config.provider.lower()
36
+ if provider == "gemini" and not model.startswith("gemini/"):
37
+ return f"gemini/{model}"
38
+ if provider == "anthropic" and not model.startswith("anthropic/"):
39
+ return f"anthropic/{model}"
40
+ if provider == "ollama" and not model.startswith("ollama/"):
41
+ return f"ollama/{model}"
42
+ return model
43
+
44
+ def generate_structured[T: BaseModel](
45
+ self, prompt: str, response_schema: type[T]
46
+ ) -> T:
47
+ """Generate structured Pydantic output using Instructor over LiteLLM.
48
+
49
+ Args:
50
+ prompt: The user prompt text.
51
+ response_schema: Target Pydantic model class.
52
+
53
+ Returns:
54
+ Validated instance of response_schema.
55
+
56
+ Raises:
57
+ StructuredOutputParsingError: If schema validation fails.
58
+ LlmProviderError: If API call fails.
59
+ """
60
+ import instructor
61
+ import litellm
62
+ from instructor.core import InstructorRetryException
63
+
64
+ client = instructor.from_litellm(litellm.completion)
65
+
66
+ kwargs: dict[str, Any] = {
67
+ "model": self._resolved_model,
68
+ "messages": [{"role": "user", "content": prompt}],
69
+ "response_model": response_schema,
70
+ "temperature": self._config.temperature,
71
+ "max_tokens": self._config.max_tokens,
72
+ }
73
+ if self._config.api_key:
74
+ kwargs["api_key"] = self._config.api_key
75
+ if self._config.litellm.api_base:
76
+ kwargs["api_base"] = self._config.litellm.api_base
77
+
78
+ try:
79
+ return client.chat.completions.create(**kwargs)
80
+ except InstructorRetryException as exc:
81
+ raise StructuredOutputParsingError(
82
+ f"Failed to generate structured {response_schema.__name__}: {exc}"
83
+ ) from exc
84
+ except Exception as exc:
85
+ raise LlmProviderError(
86
+ str(exc),
87
+ provider=self._config.provider,
88
+ model=self._config.model,
89
+ ) from exc
90
+
91
+ async def generate_structured_async[T: BaseModel](
92
+ self, prompt: str, response_schema: type[T]
93
+ ) -> T:
94
+ """Asynchronously generate structured Pydantic output using Instructor over LiteLLM."""
95
+ import instructor
96
+ import litellm
97
+ from instructor.core import InstructorRetryException
98
+
99
+ client = instructor.from_litellm(litellm.acompletion)
100
+
101
+ kwargs: dict[str, Any] = {
102
+ "model": self._resolved_model,
103
+ "messages": [{"role": "user", "content": prompt}],
104
+ "response_model": response_schema,
105
+ "temperature": self._config.temperature,
106
+ "max_tokens": self._config.max_tokens,
107
+ }
108
+ if self._config.api_key:
109
+ kwargs["api_key"] = self._config.api_key
110
+ if self._config.litellm.api_base:
111
+ kwargs["api_base"] = self._config.litellm.api_base
112
+
113
+ try:
114
+ raw_result: Any = client.chat.completions.create(**kwargs)
115
+ if inspect.isawaitable(raw_result):
116
+ return await raw_result
117
+ return raw_result
118
+ except InstructorRetryException as exc:
119
+ raise StructuredOutputParsingError(
120
+ f"Failed to generate structured {response_schema.__name__}: {exc}"
121
+ ) from exc
122
+ except Exception as exc:
123
+ raise LlmProviderError(
124
+ str(exc),
125
+ provider=self._config.provider,
126
+ model=self._config.model,
127
+ ) from exc
128
+
129
+ def generate_text(self, prompt: str, system_prompt: str | None = None) -> str:
130
+ """Generate unstructured text from a prompt via LiteLLM.
131
+
132
+ Args:
133
+ prompt: The user prompt text.
134
+ system_prompt: Optional system instruction prompt.
135
+
136
+ Returns:
137
+ Generated response text.
138
+
139
+ Raises:
140
+ LlmProviderError: If the upstream provider request fails.
141
+ """
142
+ import litellm
143
+
144
+ messages: list[dict[str, str]] = []
145
+ if system_prompt or self._config.agent.system_prompt:
146
+ sys = system_prompt or self._config.agent.system_prompt
147
+ if sys:
148
+ messages.append({"role": "system", "content": sys})
149
+ messages.append({"role": "user", "content": prompt})
150
+
151
+ kwargs: dict[str, Any] = {
152
+ "model": self._resolved_model,
153
+ "messages": messages,
154
+ "temperature": self._config.temperature,
155
+ "max_tokens": self._config.max_tokens,
156
+ "drop_params": self._config.litellm.drop_params,
157
+ "num_retries": self._config.litellm.num_retries,
158
+ "timeout": self._config.litellm.timeout,
159
+ }
160
+ if self._config.api_key:
161
+ kwargs["api_key"] = self._config.api_key
162
+ if self._config.litellm.api_base:
163
+ kwargs["api_base"] = self._config.litellm.api_base
164
+
165
+ try:
166
+ response = litellm.completion(**kwargs)
167
+ return response.choices[0].message.content or ""
168
+ except Exception as exc:
169
+ raise LlmProviderError(
170
+ str(exc),
171
+ provider=self._config.provider,
172
+ model=self._config.model,
173
+ ) from exc
174
+
175
+ async def generate_text_async(
176
+ self, prompt: str, system_prompt: str | None = None
177
+ ) -> str:
178
+ """Asynchronously generate unstructured text from a prompt via LiteLLM.
179
+
180
+ Args:
181
+ prompt: The user prompt text.
182
+ system_prompt: Optional system instruction prompt.
183
+
184
+ Returns:
185
+ Generated response text.
186
+
187
+ Raises:
188
+ LlmProviderError: If the upstream provider request fails.
189
+ """
190
+ import litellm
191
+
192
+ messages: list[dict[str, str]] = []
193
+ if system_prompt or self._config.agent.system_prompt:
194
+ sys = system_prompt or self._config.agent.system_prompt
195
+ if sys:
196
+ messages.append({"role": "system", "content": sys})
197
+ messages.append({"role": "user", "content": prompt})
198
+
199
+ kwargs: dict[str, Any] = {
200
+ "model": self._resolved_model,
201
+ "messages": messages,
202
+ "temperature": self._config.temperature,
203
+ "max_tokens": self._config.max_tokens,
204
+ "drop_params": self._config.litellm.drop_params,
205
+ "num_retries": self._config.litellm.num_retries,
206
+ "timeout": self._config.litellm.timeout,
207
+ }
208
+ if self._config.api_key:
209
+ kwargs["api_key"] = self._config.api_key
210
+ if self._config.litellm.api_base:
211
+ kwargs["api_base"] = self._config.litellm.api_base
212
+
213
+ try:
214
+ response = await litellm.acompletion(**kwargs)
215
+ return response.choices[0].message.content or ""
216
+ except Exception as exc:
217
+ raise LlmProviderError(
218
+ str(exc),
219
+ provider=self._config.provider,
220
+ model=self._config.model,
221
+ ) from exc
222
+
223
+
224
+ __all__ = [
225
+ "LiteLlmAdapter",
226
+ ]
@@ -0,0 +1,66 @@
1
+ from typing import Any
2
+
3
+ from pydantic_ai import Agent
4
+
5
+ from hexastack_ai.domain.exceptions import AgentExecutionError
6
+
7
+
8
+ class PydanticAiAgentAdapter:
9
+ """Adapter wrapping a PydanticAI Agent instance.
10
+
11
+ Notes/Architectural Intent:
12
+ Encapsulates agent execution lifecycle, converting low-level exceptions
13
+ into Hexastack domain AgentExecutionError.
14
+ """
15
+
16
+ def __init__(self, agent: Agent[Any, Any]) -> None:
17
+ """Initialize adapter with a configured PydanticAI Agent instance."""
18
+ self._agent = agent
19
+
20
+ @property
21
+ def agent(self) -> Agent[Any, Any]:
22
+ """Access the underlying PydanticAI Agent instance."""
23
+ return self._agent
24
+
25
+ async def run(self, prompt: str, **kwargs: Any) -> Any:
26
+ """Asynchronously run the agent against a user prompt.
27
+
28
+ Args:
29
+ prompt: User request prompt.
30
+ **kwargs: Extra parameters passed to agent.run().
31
+
32
+ Returns:
33
+ The agent result data.
34
+
35
+ Raises:
36
+ AgentExecutionError: If agent execution or tool call fails.
37
+ """
38
+ try:
39
+ result = await self._agent.run(prompt, **kwargs)
40
+ return getattr(result, "output", getattr(result, "data", result))
41
+ except Exception as exc:
42
+ raise AgentExecutionError(str(exc)) from exc
43
+
44
+ def run_sync(self, prompt: str, **kwargs: Any) -> Any:
45
+ """Synchronously run the agent against a user prompt.
46
+
47
+ Args:
48
+ prompt: User request prompt.
49
+ **kwargs: Extra parameters passed to agent.run_sync().
50
+
51
+ Returns:
52
+ The agent result data.
53
+
54
+ Raises:
55
+ AgentExecutionError: If agent execution fails.
56
+ """
57
+ try:
58
+ result = self._agent.run_sync(prompt, **kwargs)
59
+ return getattr(result, "output", getattr(result, "data", result))
60
+ except Exception as exc:
61
+ raise AgentExecutionError(str(exc)) from exc
62
+
63
+
64
+ __all__ = [
65
+ "PydanticAiAgentAdapter",
66
+ ]
@@ -0,0 +1,13 @@
1
+ from hexastack_ai.domain.exceptions import (
2
+ AgentExecutionError,
3
+ AiError,
4
+ LlmProviderError,
5
+ StructuredOutputParsingError,
6
+ )
7
+
8
+ __all__ = [
9
+ "AgentExecutionError",
10
+ "AiError",
11
+ "LlmProviderError",
12
+ "StructuredOutputParsingError",
13
+ ]
@@ -0,0 +1,53 @@
1
+ from hexastack_core.domain import HexastackError
2
+
3
+
4
+ class AiError(HexastackError):
5
+ """Base exception for all AI, LLM, and agent operations in Hexastack."""
6
+
7
+
8
+ class LlmProviderError(AiError):
9
+ """Exception raised when an upstream LLM API call fails."""
10
+
11
+ def __init__(
12
+ self,
13
+ message: str,
14
+ provider: str | None = None,
15
+ model: str | None = None,
16
+ ) -> None:
17
+ """Initialize LlmProviderError with model and provider context.
18
+
19
+ Args:
20
+ message: Error description.
21
+ provider: Name of the LLM provider (e.g. 'openai', 'anthropic').
22
+ model: Name of the target model (e.g. 'gpt-4o', 'claude-3-5-sonnet').
23
+ """
24
+ self.provider = provider
25
+ self.model = model
26
+ suffix = f" [provider={provider}, model={model}]" if provider or model else ""
27
+ super().__init__(f"{message}{suffix}")
28
+
29
+
30
+ class StructuredOutputParsingError(AiError):
31
+ """Exception raised when LLM output fails schema validation."""
32
+
33
+ def __init__(self, message: str, raw_response: str | None = None) -> None:
34
+ """Initialize exception with raw output text.
35
+
36
+ Args:
37
+ message: Error description.
38
+ raw_response: Raw response string from the model.
39
+ """
40
+ self.raw_response = raw_response
41
+ super().__init__(message)
42
+
43
+
44
+ class AgentExecutionError(AiError):
45
+ """Exception raised when an agent loop or tool execution fails."""
46
+
47
+
48
+ __all__ = [
49
+ "AgentExecutionError",
50
+ "AiError",
51
+ "LlmProviderError",
52
+ "StructuredOutputParsingError",
53
+ ]
@@ -0,0 +1,27 @@
1
+ from hexastack_ai.infra.bootstrap import (
2
+ AiBootstrapper,
3
+ AiBootstrapResult,
4
+ )
5
+ from hexastack_ai.infra.config import (
6
+ HexastackAiConfig,
7
+ LiteLlmDialectConfig,
8
+ OllamaDialectConfig,
9
+ PydanticAiDialectConfig,
10
+ register_ai_config,
11
+ )
12
+ from hexastack_ai.infra.tools import (
13
+ create_cqrs_agent,
14
+ create_tool_for_message,
15
+ )
16
+
17
+ __all__ = [
18
+ "AiBootstrapper",
19
+ "AiBootstrapResult",
20
+ "create_cqrs_agent",
21
+ "create_tool_for_message",
22
+ "HexastackAiConfig",
23
+ "LiteLlmDialectConfig",
24
+ "OllamaDialectConfig",
25
+ "PydanticAiDialectConfig",
26
+ "register_ai_config",
27
+ ]
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+
3
+ from hexastack_ai.infra.config import HexastackAiConfig, register_ai_config
4
+ from hexastack_core.adapters.ai import InMemoryLlmProvider
5
+ from hexastack_core.infra.bootstrap import BootstrapContext
6
+ from hexastack_core.infra.registries.config import ConfigRegistry
7
+ from hexastack_core.ports.ai import LlmProviderPort
8
+ from hexastack_core.ports.bootstrap import BootstrapperPort
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AiBootstrapResult:
13
+ """Dataclass holding initialized AI provider and configuration."""
14
+
15
+ config: HexastackAiConfig
16
+ llm_provider: LlmProviderPort
17
+
18
+
19
+ class AiBootstrapper(BootstrapperPort):
20
+ """Bootstrap extension initializing LLM provider and agent integration.
21
+
22
+ Notes/Architectural Intent:
23
+ Implements BootstrapperPort at order=18, registering [hexastack.ai] config
24
+ and binding LlmProviderPort into the DI container. Automatically defaults
25
+ to InMemoryLlmProvider when provider='memory' for zero-infrastructure test isolation.
26
+ """
27
+
28
+ name: str = "ai"
29
+ order: int = 18
30
+
31
+ def configure(self, context: BootstrapContext) -> None:
32
+ """Phase 2: Assemble LLM provider adapter in DI container.
33
+
34
+ Args:
35
+ context: BootstrapContext containing DI container and config.
36
+ """
37
+ di = context.container
38
+
39
+ # 1. Read AI Configuration
40
+ if HexastackAiConfig in di:
41
+ ai_config = di.resolve(HexastackAiConfig)
42
+ else:
43
+ ai_config = context.get_config("ai", HexastackAiConfig)
44
+
45
+ # 2. Build Provider based on config
46
+ llm_provider: LlmProviderPort
47
+ if ai_config.provider == "memory":
48
+ llm_provider = InMemoryLlmProvider()
49
+ di.add_instance(llm_provider, declared_class=InMemoryLlmProvider)
50
+ else:
51
+ from hexastack_ai.adapters.litellm import LiteLlmAdapter
52
+
53
+ llm_provider = LiteLlmAdapter(config=ai_config)
54
+ di.add_instance(llm_provider, declared_class=LiteLlmAdapter)
55
+
56
+ # 3. Register in DI container
57
+ if LlmProviderPort not in di:
58
+ di.add_instance(llm_provider, declared_class=LlmProviderPort)
59
+
60
+ # 4. Store result in context properties
61
+ ai_result = AiBootstrapResult(
62
+ config=ai_config,
63
+ llm_provider=llm_provider,
64
+ )
65
+ context.properties["ai_result"] = ai_result
66
+ context.properties["ai_provider"] = llm_provider
67
+
68
+ def register_config(self, registry: ConfigRegistry) -> None:
69
+ """Phase 1: Register AI configuration schema under 'ai'.
70
+
71
+ Args:
72
+ registry: Target ConfigRegistry instance.
73
+ """
74
+ register_ai_config(registry)
75
+
76
+
77
+ __all__ = [
78
+ "AiBootstrapper",
79
+ "AiBootstrapResult",
80
+ ]
@@ -0,0 +1,110 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from hexastack_core.infra.decorators import config_section
4
+ from hexastack_core.infra.registries.config import ConfigRegistry
5
+
6
+
7
+ class LiteLlmDialectConfig(BaseModel):
8
+ """Dialect configuration options specific to LiteLLM."""
9
+
10
+ drop_params: bool = Field(
11
+ default=True,
12
+ description="Automatically drop unmapped provider parameters to prevent errors.",
13
+ )
14
+ num_retries: int = Field(
15
+ default=3,
16
+ description="Number of retry attempts on rate limits or transient errors.",
17
+ )
18
+ timeout: float = Field(
19
+ default=60.0,
20
+ description="Request timeout in seconds.",
21
+ )
22
+ api_base: str | None = Field(
23
+ default=None,
24
+ description="Custom API base URL (e.g. for self-hosted LiteLLM proxy or local endpoint).",
25
+ )
26
+
27
+
28
+ class OllamaDialectConfig(BaseModel):
29
+ """Dialect configuration options for local Ollama instances."""
30
+
31
+ base_url: str = Field(
32
+ default="http://localhost:11434",
33
+ description="Base URL for the local Ollama daemon.",
34
+ )
35
+
36
+
37
+ class PydanticAiDialectConfig(BaseModel):
38
+ """Configuration options for PydanticAI agents."""
39
+
40
+ max_turns: int = Field(
41
+ default=10,
42
+ description="Maximum turn limit for agent tool-calling loops.",
43
+ )
44
+ system_prompt: str | None = Field(
45
+ default=None,
46
+ description="Default system prompt for agent personas.",
47
+ )
48
+
49
+
50
+ @config_section("ai")
51
+ class HexastackAiConfig(BaseModel):
52
+ """Configuration schema for Hexastack AI engine under [hexastack.ai].
53
+
54
+ Notes/Architectural Intent:
55
+ Partitions global model settings (model, temperature, tokens) from
56
+ provider/dialect-specific subsections (LiteLLM, Ollama, PydanticAI).
57
+ """
58
+
59
+ provider: str = Field(
60
+ default="memory",
61
+ description="Target LLM provider ('memory', 'litellm', 'openai', 'anthropic', 'gemini', 'ollama').",
62
+ )
63
+ model: str = Field(
64
+ default="gpt-4o-mini",
65
+ description="Default model identifier (e.g. 'gpt-4o', 'claude-3-5-sonnet-20241022', 'gemini/gemini-1.5-pro').",
66
+ )
67
+ temperature: float = Field(
68
+ default=0.2,
69
+ description="Sampling temperature between 0.0 and 2.0.",
70
+ )
71
+ max_tokens: int = Field(
72
+ default=2048,
73
+ description="Maximum tokens for text generation.",
74
+ )
75
+ api_key: str | None = Field(
76
+ default=None,
77
+ description="Optional explicit API key override (prefers environment variables by default).",
78
+ )
79
+
80
+ # Dialect-specific sections
81
+ litellm: LiteLlmDialectConfig = Field(
82
+ default_factory=LiteLlmDialectConfig,
83
+ description="LiteLLM proxy and retry configuration.",
84
+ )
85
+ ollama: OllamaDialectConfig = Field(
86
+ default_factory=OllamaDialectConfig,
87
+ description="Ollama local model configuration.",
88
+ )
89
+ agent: PydanticAiDialectConfig = Field(
90
+ default_factory=PydanticAiDialectConfig,
91
+ description="PydanticAI agent configuration.",
92
+ )
93
+
94
+
95
+ __all__ = [
96
+ "HexastackAiConfig",
97
+ "LiteLlmDialectConfig",
98
+ "OllamaDialectConfig",
99
+ "PydanticAiDialectConfig",
100
+ "register_ai_config",
101
+ ]
102
+
103
+
104
+ def register_ai_config(registry: ConfigRegistry) -> None:
105
+ """Register AI configuration schema under 'ai' ([hexastack.ai]).
106
+
107
+ Args:
108
+ registry: Target ConfigRegistry instance.
109
+ """
110
+ registry.register_config_section("ai", HexastackAiConfig)
@@ -0,0 +1,111 @@
1
+ import inspect
2
+ from collections.abc import Sequence
3
+ from typing import Any
4
+
5
+ from pydantic_ai import Agent
6
+ from pydantic_ai.models import Model
7
+ from pydantic_core import PydanticUndefined
8
+
9
+ from hexastack_core.domain import Command, Generic, Query
10
+ from hexastack_cqrs.infra.pipeline import ExecutionPipeline
11
+
12
+ __all__ = [
13
+ "create_cqrs_agent",
14
+ "create_tool_for_message",
15
+ ]
16
+
17
+
18
+ def create_cqrs_agent(
19
+ pipeline: ExecutionPipeline,
20
+ messages: Sequence[type[Command | Query[Any]]],
21
+ model: str | Model = "test",
22
+ system_prompt: str | None = None,
23
+ ) -> Agent[Any, Any]:
24
+ """Assemble a PydanticAI Agent with CQRS message handlers reflected as tools.
25
+
26
+ Notes/Architectural Intent:
27
+ Bridges the CQRS message bus with AI agent capabilities. The agent can
28
+ reason, select appropriate Commands/Queries, and invoke domain logic
29
+ through the standard Hexastack execution pipeline.
30
+
31
+ Args:
32
+ pipeline: Target ExecutionPipeline.
33
+ messages: Sequence of Command/Query classes to expose as tools.
34
+ model: Target model string ('test', 'openai:gpt-4o', 'anthropic:claude-3-5-sonnet') or Model instance.
35
+ system_prompt: Optional initial persona instructions.
36
+
37
+ Returns:
38
+ Configured PydanticAI Agent instance.
39
+ """
40
+ sys_prompt = system_prompt or (
41
+ "You are an AI assistant capable of executing domain operations "
42
+ "using the provided tools."
43
+ )
44
+ agent: Agent[Any, Any] = Agent(model=model, system_prompt=sys_prompt)
45
+
46
+ for msg_cls in messages:
47
+ tool_fn = create_tool_for_message(msg_cls, pipeline)
48
+ agent.tool_plain(tool_fn)
49
+
50
+ return agent
51
+
52
+
53
+ def create_tool_for_message(
54
+ msg_cls: type[Generic],
55
+ pipeline: ExecutionPipeline,
56
+ ) -> Any:
57
+ """Create a typed tool function that constructs a CQRS message and executes it.
58
+
59
+ Notes/Architectural Intent:
60
+ Reflects Pydantic model fields dynamically onto the generated tool function's
61
+ `__signature__` and `__annotations__`. This allows PydanticAI to generate
62
+ accurate function-calling schemas while dispatching directly through
63
+ the Hexastack ExecutionPipeline.
64
+
65
+ Args:
66
+ msg_cls: Domain Generic, Command, or Query class.
67
+ pipeline: Target ExecutionPipeline instance.
68
+
69
+ Returns:
70
+ Callable tool function with dynamic signature and execution dispatcher.
71
+ """
72
+
73
+ async def tool_executor(**kwargs: Any) -> Any:
74
+ msg = msg_cls.model_validate(kwargs)
75
+ result = pipeline.execute(msg)
76
+ if inspect.isawaitable(result):
77
+ return await result
78
+ return result
79
+
80
+ # Reflect Pydantic model fields into parameter signature and annotations
81
+ parameters = [
82
+ inspect.Parameter(
83
+ name=field_name,
84
+ kind=inspect.Parameter.KEYWORD_ONLY,
85
+ annotation=field_info.annotation or Any,
86
+ default=(
87
+ field_info.default
88
+ if field_info.default is not PydanticUndefined
89
+ else inspect.Parameter.empty
90
+ ),
91
+ )
92
+ for field_name, field_info in msg_cls.model_fields.items()
93
+ ]
94
+ setattr( # noqa: B010
95
+ tool_executor,
96
+ "__signature__",
97
+ inspect.Signature(parameters=parameters),
98
+ )
99
+ setattr( # noqa: B010
100
+ tool_executor,
101
+ "__annotations__",
102
+ {
103
+ name: field_info.annotation or Any
104
+ for name, field_info in msg_cls.model_fields.items()
105
+ },
106
+ )
107
+ tool_executor.__name__ = msg_cls.__name__
108
+ tool_executor.__doc__ = (
109
+ msg_cls.__doc__ or f"Execute the {msg_cls.__name__} domain operation."
110
+ )
111
+ return tool_executor