aeko 1.0.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.
- aeko-1.0.0/.gitignore +40 -0
- aeko-1.0.0/LICENSE +21 -0
- aeko-1.0.0/PKG-INFO +568 -0
- aeko-1.0.0/README.md +545 -0
- aeko-1.0.0/aeko/__init__.py +34 -0
- aeko-1.0.0/aeko/config/__init__.py +31 -0
- aeko-1.0.0/aeko/config/_text.py +21 -0
- aeko-1.0.0/aeko/config/aeko.py +63 -0
- aeko-1.0.0/aeko/config/dto.py +117 -0
- aeko-1.0.0/aeko/config/exceptions.py +21 -0
- aeko-1.0.0/aeko/config/inventory.py +75 -0
- aeko-1.0.0/aeko/config/messenger.py +186 -0
- aeko-1.0.0/aeko/engine/__init__.py +0 -0
- aeko-1.0.0/aeko/engine/agents/agents.py +112 -0
- aeko-1.0.0/aeko/engine/agents/llms.py +58 -0
- aeko-1.0.0/aeko/engine/graph/builder.py +221 -0
- aeko-1.0.0/aeko/engine/graph/nodes.py +397 -0
- aeko-1.0.0/aeko/engine/graph/state.py +124 -0
- aeko-1.0.0/aeko/engine/prompts/__init__.py +55 -0
- aeko-1.0.0/aeko/engine/prompts/builder.py +132 -0
- aeko-1.0.0/aeko/engine/prompts/continuous_improvement_coordinator.py +47 -0
- aeko-1.0.0/aeko/engine/prompts/faq.py +50 -0
- aeko-1.0.0/aeko/engine/prompts/green_gases_analyst.py +45 -0
- aeko-1.0.0/aeko/engine/prompts/orchestrator.py +49 -0
- aeko-1.0.0/aeko/engine/prompts/output_guardrail.py +50 -0
- aeko-1.0.0/aeko/engine/prompts/pollutants_analyst.py +50 -0
- aeko-1.0.0/aeko/engine/prompts/report_analyst.py +48 -0
- aeko-1.0.0/aeko/engine/prompts/router.py +54 -0
- aeko-1.0.0/aeko/engine/runtime.py +115 -0
- aeko-1.0.0/aeko/py.typed +0 -0
- aeko-1.0.0/pyproject.toml +45 -0
- aeko-1.0.0/tests/conftest.py +172 -0
- aeko-1.0.0/tests/test_agents.py +142 -0
- aeko-1.0.0/tests/test_config.py +439 -0
- aeko-1.0.0/tests/test_graph_builder.py +110 -0
- aeko-1.0.0/tests/test_graph_e2e.py +184 -0
- aeko-1.0.0/tests/test_llms.py +126 -0
- aeko-1.0.0/tests/test_nodes.py +407 -0
- aeko-1.0.0/tests/test_prompt_builder.py +113 -0
aeko-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
### AL ###
|
|
2
|
+
#Template for AL projects for Dynamics 365 Business Central
|
|
3
|
+
#launch.json folder
|
|
4
|
+
.vscode/
|
|
5
|
+
#Cache folder
|
|
6
|
+
.alcache/
|
|
7
|
+
#Symbols folder
|
|
8
|
+
.alpackages/
|
|
9
|
+
#Snapshots folder
|
|
10
|
+
.snapshots/
|
|
11
|
+
#Testing Output folder
|
|
12
|
+
.output/
|
|
13
|
+
#Extension App-file
|
|
14
|
+
*.app
|
|
15
|
+
#Rapid Application Development File
|
|
16
|
+
rad.json
|
|
17
|
+
#Translation Base-file
|
|
18
|
+
*.g.xlf
|
|
19
|
+
#License-files
|
|
20
|
+
*.bclicense
|
|
21
|
+
*.flf
|
|
22
|
+
#Test results file
|
|
23
|
+
TestResults.xml
|
|
24
|
+
*__pycache__/
|
|
25
|
+
*.pyc
|
|
26
|
+
*.pyo
|
|
27
|
+
*.pyd
|
|
28
|
+
*vscode/
|
|
29
|
+
*AGENT_DOCS/
|
|
30
|
+
|
|
31
|
+
agent.md
|
|
32
|
+
claude.md
|
|
33
|
+
.claude/
|
|
34
|
+
|
|
35
|
+
.env
|
|
36
|
+
result.txt
|
|
37
|
+
# Python build artifacts
|
|
38
|
+
dist/
|
|
39
|
+
build/
|
|
40
|
+
*.egg-info/
|
aeko-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aether
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
aeko-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aeko
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Intelligent core of the Aether ecosystem: multi-agent system for GHG inventory analysis, improvement plans and conversational support.
|
|
5
|
+
Project-URL: Homepage, https://github.com/AetherGases/aeko-sdk
|
|
6
|
+
Project-URL: Repository, https://github.com/AetherGases/aeko-sdk
|
|
7
|
+
Author: Eduardo Costa Amex Macal
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,esg,ghg,langchain,langgraph,sustainability
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: <3.12,>=3.11
|
|
18
|
+
Requires-Dist: langchain-classic<2,>=1.0.8
|
|
19
|
+
Requires-Dist: langchain-core<2,>=1.5.5
|
|
20
|
+
Requires-Dist: langchain-google-genai<3,>=2.1.12
|
|
21
|
+
Requires-Dist: langgraph<2,>=1.2.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# aeko
|
|
25
|
+
|
|
26
|
+
**The intelligent core of the Aether ecosystem** — a multi-agent system that reads a
|
|
27
|
+
company's greenhouse gas (GHG) inventory, explains it, and turns it into a concrete
|
|
28
|
+
improvement plan.
|
|
29
|
+
|
|
30
|
+
Aether helps industrial companies cut their greenhouse gas emissions. Their employees
|
|
31
|
+
fill in the company's GHG inventory; Aeko is what reads that inventory and answers with
|
|
32
|
+
pollutant analysis, green gas alternatives and a prioritized improvement plan — plus a
|
|
33
|
+
chatbot for the day-to-day ESG questions that come up along the way.
|
|
34
|
+
|
|
35
|
+
This package is the reusable engine behind that product. It is a **library, not a
|
|
36
|
+
service**: it exposes a small facade over a LangChain/LangGraph agent graph so any
|
|
37
|
+
backend — an API, a worker, a notebook — can embed it.
|
|
38
|
+
|
|
39
|
+
> 🇧🇷 Um resumo em português está no final deste documento: [Resumo em português](#resumo-em-português).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Table of contents
|
|
44
|
+
|
|
45
|
+
- [What the SDK does](#what-the-sdk-does)
|
|
46
|
+
- [The agent system](#the-agent-system)
|
|
47
|
+
- [Requirements](#requirements)
|
|
48
|
+
- [Installation](#installation)
|
|
49
|
+
- [Quickstart](#quickstart)
|
|
50
|
+
- [Integrating into other systems](#integrating-into-other-systems)
|
|
51
|
+
- [Design rules that shape the integration](#design-rules-that-shape-the-integration)
|
|
52
|
+
- [1. Configure once, at startup](#1-configure-once-at-startup)
|
|
53
|
+
- [2. Register your tools](#2-register-your-tools)
|
|
54
|
+
- [3. The conversational flow](#3-the-conversational-flow)
|
|
55
|
+
- [4. The inventory flow](#4-the-inventory-flow)
|
|
56
|
+
- [5. Full example: a stateless FastAPI service](#5-full-example-a-stateless-fastapi-service)
|
|
57
|
+
- [6. Error handling](#6-error-handling)
|
|
58
|
+
- [API reference](#api-reference)
|
|
59
|
+
- [Development](#development)
|
|
60
|
+
- [Resumo em português](#resumo-em-português)
|
|
61
|
+
- [License](#license)
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## What the SDK does
|
|
66
|
+
|
|
67
|
+
Aeko exposes two independent entry points into the same agent graph:
|
|
68
|
+
|
|
69
|
+
| Entry point | Class | Use it for |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| **Conversation** | `AekoMessenger` | The ESG chatbot: a user message in, a reviewed answer out, with session memory. |
|
|
72
|
+
| **Inventory report** | `AekoInventoryAnalyzer` | A GHG inventory in, a full improvement plan out. |
|
|
73
|
+
|
|
74
|
+
Both share the same agents, the same registered tools and the same configuration. The
|
|
75
|
+
difference is where the run enters the graph and how it ends.
|
|
76
|
+
|
|
77
|
+
## The agent system
|
|
78
|
+
|
|
79
|
+
Eight agents, each with its own prompt, persona and tools. Names are in Portuguese
|
|
80
|
+
because they are also the routing keys the graph and `set_tools()` use — pass them
|
|
81
|
+
exactly as written.
|
|
82
|
+
|
|
83
|
+
| Agent | Role | Model |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `Roteador` | First-touch triage; classifies intent and hands off. | fast |
|
|
86
|
+
| `FAQ` | Answers institutional/conceptual questions directly. | fast |
|
|
87
|
+
| `Orquestrador` | Consolidates the specialists' output and replies to the user. | fast |
|
|
88
|
+
| `Guardrail de Saída` | Reviews the consolidated answer before it can leave. | fast |
|
|
89
|
+
| `Análista de inventários` | Reads the GHG inventory itself. | slow |
|
|
90
|
+
| `Analista de Poluentes` | Pollutant analysis. | slow |
|
|
91
|
+
| `Analista de Gases Verdes` | Green gas alternatives. | slow |
|
|
92
|
+
| `Coordenador de Melhoria Contínua` | Writes the improvement plan. | slow |
|
|
93
|
+
|
|
94
|
+
The four cheap agents (classify, consolidate, review) run on `fast_model`; the four
|
|
95
|
+
specialist analysts run on `slow_model`. Both are configurable.
|
|
96
|
+
|
|
97
|
+
**Conversational flow** — enters at the router, always passes the guardrail:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
send_message()
|
|
101
|
+
│
|
|
102
|
+
▼
|
|
103
|
+
Roteador ──────────────────────────▶ FAQ ─────────────────────────▶ answer
|
|
104
|
+
│
|
|
105
|
+
├──▶ Analista de Poluentes ─────┐
|
|
106
|
+
├──▶ Analista de Gases Verdes ──┼──▶ Orquestrador ──▶ Guardrail de Saída
|
|
107
|
+
│ │ │
|
|
108
|
+
└──▶ Coordenador de Melhoria ───┴───────────▶ answer approved? ──▶ answer
|
|
109
|
+
│
|
|
110
|
+
rejected (up to 3x)
|
|
111
|
+
│
|
|
112
|
+
└──▶ back to Roteador
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the guardrail keeps rejecting past the retry cap, the run ends with **no** answer:
|
|
116
|
+
`MessageResponse.answer` is `""` and `approved` is `False`. Always check it.
|
|
117
|
+
|
|
118
|
+
**Inventory flow** — enters at the inventory analyst, ends at a terminal node, so it
|
|
119
|
+
never passes the guardrail:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
analyze()
|
|
123
|
+
│
|
|
124
|
+
▼
|
|
125
|
+
Análista de inventários ──▶ Analista de Poluentes ⇄ Analista de Gases Verdes
|
|
126
|
+
│
|
|
127
|
+
▼
|
|
128
|
+
Coordenador de Melhoria Contínua ──▶ plan
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- **Python 3.11** (`>=3.11,<3.12`)
|
|
134
|
+
- A **Google Gemini API key**
|
|
135
|
+
|
|
136
|
+
Runtime dependencies (`langchain-core`, `langchain-classic`, `langgraph`,
|
|
137
|
+
`langchain-google-genai`) are installed with the package.
|
|
138
|
+
|
|
139
|
+
## Installation
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pip install aeko
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Distribution and import name are the same: `aeko`. (The GitHub repository is `aeko-sdk`.)
|
|
146
|
+
|
|
147
|
+
## Quickstart
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from aeko import Aeko, AekoMessenger, AekoInventoryAnalyzer
|
|
151
|
+
|
|
152
|
+
# 1. Configure the SDK once, for the whole process.
|
|
153
|
+
Aeko.config("YOUR_GEMINI_API_KEY")
|
|
154
|
+
|
|
155
|
+
# 2. Chat.
|
|
156
|
+
messenger = AekoMessenger()
|
|
157
|
+
messenger.prepare(session_id="session-42", user_info="Ana, ESG analyst at ACME Chemicals")
|
|
158
|
+
|
|
159
|
+
reply = messenger.send_message("What is the difference between scope 1 and scope 2?")
|
|
160
|
+
print(reply.answer)
|
|
161
|
+
print(reply.agents_called) # e.g. ['FAQ']
|
|
162
|
+
print(reply.approved) # guardrail verdict
|
|
163
|
+
|
|
164
|
+
# 3. Analyze an inventory.
|
|
165
|
+
analyzer = AekoInventoryAnalyzer()
|
|
166
|
+
analyzer.set_context("Last report: 12,400 tCO2e, scope 1 dominated by the boiler fleet.")
|
|
167
|
+
|
|
168
|
+
report = analyzer.analyze(inventory_markdown)
|
|
169
|
+
print(report.answer) # the improvement plan
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Integrating into other systems
|
|
175
|
+
|
|
176
|
+
### Design rules that shape the integration
|
|
177
|
+
|
|
178
|
+
Three deliberate design choices decide how you wire Aeko into your backend. Read these
|
|
179
|
+
before writing the integration — they explain every recommendation below.
|
|
180
|
+
|
|
181
|
+
1. **The SDK never reads the environment.** There is no `GEMINI_API_KEY` fallback, no
|
|
182
|
+
`.env` loading. Your application owns its configuration and passes it in through
|
|
183
|
+
`Aeko.config()`. Nothing works until it does.
|
|
184
|
+
2. **Configuration and tools are process-wide.** `Aeko.config()` and
|
|
185
|
+
`AekoMessenger.set_tools()` mutate a single process-wide runtime and rebuild every
|
|
186
|
+
agent. Call them **at startup**, not per request. (`set_tools` is a `classmethod` for
|
|
187
|
+
exactly this reason: registering tools on one instance would silently rebuild the
|
|
188
|
+
agents behind every other instance's back.)
|
|
189
|
+
3. **Session memory lives in the process.** `AekoMessenger` keeps conversation history in
|
|
190
|
+
a process-wide dict keyed by `session_id`. That is fine for a single worker, and *not*
|
|
191
|
+
enough for anything else — see the stateless pattern below.
|
|
192
|
+
|
|
193
|
+
### 1. Configure once, at startup
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
from aeko import Aeko
|
|
197
|
+
|
|
198
|
+
Aeko.config(
|
|
199
|
+
api_key=settings.GEMINI_API_KEY,
|
|
200
|
+
fast_model="gemini-3.1-flash-lite", # router, FAQ, orchestrator, guardrail
|
|
201
|
+
slow_model="gemini-3.5-flash", # the four specialist analysts
|
|
202
|
+
max_tokens=1024, # output cap for chat turns
|
|
203
|
+
report_max_tokens=8192, # output cap for the inventory report
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Every keyword argument is optional and falls back to the default shown. Calling
|
|
208
|
+
`Aeko.config()` again applies the change immediately — the agents and the compiled graph
|
|
209
|
+
are rebuilt on the next run. `Aeko.is_configured()` tells you whether a key was supplied
|
|
210
|
+
(useful in a health check); `Aeko.reset()` restores every default and clears registered
|
|
211
|
+
tools.
|
|
212
|
+
|
|
213
|
+
### 2. Register your tools
|
|
214
|
+
|
|
215
|
+
Tools are **your** application's code — a price lookup, an emission-factor table, a search
|
|
216
|
+
over your own documents. Aeko binds them to the agents you choose.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from langchain_core.tools import tool
|
|
220
|
+
from aeko import AekoMessenger, AekoTool
|
|
221
|
+
|
|
222
|
+
@tool
|
|
223
|
+
def consulta_precos(equipamento: str) -> str:
|
|
224
|
+
"""Return the average market price for a piece of equipment."""
|
|
225
|
+
return price_service.lookup(equipamento)
|
|
226
|
+
|
|
227
|
+
AekoMessenger.set_tools({
|
|
228
|
+
"Coordenador de Melhoria Contínua": [
|
|
229
|
+
AekoTool(
|
|
230
|
+
tool=consulta_precos,
|
|
231
|
+
description="Use to price the equipment you recommend, in BRL.",
|
|
232
|
+
),
|
|
233
|
+
],
|
|
234
|
+
# A bare LangChain tool works too — its own docstring becomes the description.
|
|
235
|
+
"FAQ": [buscar_norma],
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Each tool is used **twice, from a single declaration**: its description is rendered into
|
|
240
|
+
that agent's `# Ferramentas Disponiveis` prompt section, and the tool object itself is
|
|
241
|
+
bound to that agent's executor. The prompt therefore can never advertise a tool the agent
|
|
242
|
+
is unable to call.
|
|
243
|
+
|
|
244
|
+
Keys must be agent names from the table above; anything else raises `UnknownAgentError`,
|
|
245
|
+
which carries the valid names in `.known_agents`. `AGENT_NAMES` is exported for
|
|
246
|
+
validating input before you get there:
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from aeko import AGENT_NAMES # ('Roteador', 'FAQ', 'Orquestrador', ...)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 3. The conversational flow
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
messenger = AekoMessenger()
|
|
256
|
+
|
|
257
|
+
session = messenger.prepare(
|
|
258
|
+
session_id="session-42",
|
|
259
|
+
user_info="Ana, ESG analyst at ACME Chemicals, 380 employees, resin manufacturing",
|
|
260
|
+
)
|
|
261
|
+
# session.turns -> how many prior turns the session holds
|
|
262
|
+
|
|
263
|
+
response = messenger.send_message("Our scope 1 jumped 12% this quarter. Where do I look?")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`user_info` is free-form and is forwarded to every agent as company context — the richer
|
|
267
|
+
it is, the more grounded the answers.
|
|
268
|
+
|
|
269
|
+
`send_message()` returns a `MessageResponse`:
|
|
270
|
+
|
|
271
|
+
| Field | Meaning |
|
|
272
|
+
| --- | --- |
|
|
273
|
+
| `session_id` | The session the answer belongs to. |
|
|
274
|
+
| `answer` | The final user-facing text, already stripped of the agents' internal routing markers. **Empty when the guardrail never approved a draft.** |
|
|
275
|
+
| `agents_called` | Names of the agents that contributed, in call order. |
|
|
276
|
+
| `approved` | Whether the output guardrail approved the answer. |
|
|
277
|
+
| `guardrail_retries` | How many times the guardrail sent the draft back. |
|
|
278
|
+
|
|
279
|
+
Calling `send_message()` before `prepare()` raises `SessionNotPreparedError`.
|
|
280
|
+
|
|
281
|
+
**Resuming a session on another process.** Pass the prior turns to `prepare()` and the
|
|
282
|
+
session is rehydrated from your own storage. History is accepted either as
|
|
283
|
+
`{"role", "content"}` dicts or as LangChain message objects, oldest first:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
messenger.prepare(
|
|
287
|
+
session_id="session-42",
|
|
288
|
+
user_info=user_info,
|
|
289
|
+
history=[
|
|
290
|
+
{"role": "user", "content": "What is scope 3?"},
|
|
291
|
+
{"role": "assistant", "content": "Scope 3 covers..."},
|
|
292
|
+
],
|
|
293
|
+
)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Passing `history` **replaces** whatever this process held for that id. Omitting it keeps
|
|
297
|
+
(or starts) the in-process session.
|
|
298
|
+
|
|
299
|
+
### 4. The inventory flow
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from aeko import AekoInventoryAnalyzer
|
|
303
|
+
|
|
304
|
+
analyzer = AekoInventoryAnalyzer()
|
|
305
|
+
|
|
306
|
+
# Optional: a company's first report legitimately has no previous one.
|
|
307
|
+
analyzer.set_context("2025 report: 12,400 tCO2e, scope 1 dominated by the boiler fleet.")
|
|
308
|
+
|
|
309
|
+
report = analyzer.analyze(inventory_markdown)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`analyze()` expects the inventory **rendered as Markdown** — a table is the natural shape.
|
|
313
|
+
It runs with `report_max_tokens` instead of the chat cap, since this flow writes a full
|
|
314
|
+
report that the chat-sized cap would truncate.
|
|
315
|
+
|
|
316
|
+
It returns an `InventoryAnalysisResponse`:
|
|
317
|
+
|
|
318
|
+
| Field | Meaning |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| `answer` | The final improvement plan. |
|
|
321
|
+
| `agents_called` | Names of the agents that contributed. |
|
|
322
|
+
| `context_used` | Whether a previous-report context was set beforehand. |
|
|
323
|
+
|
|
324
|
+
There is no `approved` field: this flow ends at the continuous improvement coordinator, a
|
|
325
|
+
terminal node, and never reaches the output guardrail.
|
|
326
|
+
|
|
327
|
+
### 5. Full example: a stateless FastAPI service
|
|
328
|
+
|
|
329
|
+
This is the shape Aeko was designed for: an HTTP API with more than one worker, where
|
|
330
|
+
conversation history lives in **your** database and is handed back to the SDK on every
|
|
331
|
+
request.
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
from contextlib import asynccontextmanager
|
|
335
|
+
|
|
336
|
+
from fastapi import FastAPI, HTTPException
|
|
337
|
+
from pydantic import BaseModel
|
|
338
|
+
|
|
339
|
+
from aeko import (
|
|
340
|
+
Aeko,
|
|
341
|
+
AekoInventoryAnalyzer,
|
|
342
|
+
AekoMessenger,
|
|
343
|
+
AekoNotConfiguredError,
|
|
344
|
+
AekoTool,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
from .settings import settings
|
|
348
|
+
from .tools import buscar_norma, consulta_precos
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
@asynccontextmanager
|
|
352
|
+
async def lifespan(app: FastAPI):
|
|
353
|
+
# Process-wide setup: exactly once, before the first request.
|
|
354
|
+
Aeko.config(settings.GEMINI_API_KEY, report_max_tokens=12288)
|
|
355
|
+
AekoMessenger.set_tools({
|
|
356
|
+
"FAQ": [buscar_norma],
|
|
357
|
+
"Coordenador de Melhoria Contínua": [
|
|
358
|
+
AekoTool(tool=consulta_precos, description="Price the equipment you recommend."),
|
|
359
|
+
],
|
|
360
|
+
})
|
|
361
|
+
yield
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
app = FastAPI(lifespan=lifespan)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ChatRequest(BaseModel):
|
|
368
|
+
session_id: str
|
|
369
|
+
user_info: str
|
|
370
|
+
message: str
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@app.post("/chat")
|
|
374
|
+
def chat(body: ChatRequest):
|
|
375
|
+
# 1. Rehydrate the session from your own storage — this worker may never
|
|
376
|
+
# have seen this session_id before.
|
|
377
|
+
history = db.load_turns(body.session_id)
|
|
378
|
+
|
|
379
|
+
messenger = AekoMessenger()
|
|
380
|
+
messenger.prepare(body.session_id, body.user_info, history=history)
|
|
381
|
+
|
|
382
|
+
# 2. Run it. Blocking call: keep it off the event loop in production
|
|
383
|
+
# (`run_in_threadpool`, a task queue, or a sync worker).
|
|
384
|
+
try:
|
|
385
|
+
response = messenger.send_message(body.message)
|
|
386
|
+
except AekoNotConfiguredError as exc:
|
|
387
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
388
|
+
|
|
389
|
+
# 3. The guardrail can refuse every draft — there is no answer to persist.
|
|
390
|
+
if not response.answer:
|
|
391
|
+
raise HTTPException(
|
|
392
|
+
status_code=502,
|
|
393
|
+
detail="The output guardrail rejected every draft. Please rephrase.",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# 4. Persist the turn so the next request (on any worker) can replay it.
|
|
397
|
+
db.save_turn(body.session_id, body.message, response.answer)
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
"answer": response.answer,
|
|
401
|
+
"agents": response.agents_called,
|
|
402
|
+
"approved": response.approved,
|
|
403
|
+
"guardrail_retries": response.guardrail_retries,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class InventoryRequest(BaseModel):
|
|
408
|
+
inventory_markdown: str
|
|
409
|
+
previous_report: str | None = None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@app.post("/inventory")
|
|
413
|
+
def inventory(body: InventoryRequest):
|
|
414
|
+
analyzer = AekoInventoryAnalyzer()
|
|
415
|
+
|
|
416
|
+
if body.previous_report:
|
|
417
|
+
analyzer.set_context(body.previous_report)
|
|
418
|
+
|
|
419
|
+
report = analyzer.analyze(body.inventory_markdown)
|
|
420
|
+
|
|
421
|
+
return {"plan": report.answer, "agents": report.agents_called}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
Notes for production:
|
|
425
|
+
|
|
426
|
+
- **`send_message()` and `analyze()` are synchronous and slow** (several model calls per
|
|
427
|
+
run, more when the guardrail retries). Don't block an async event loop with them — use
|
|
428
|
+
a threadpool, a background worker, or a task queue.
|
|
429
|
+
- **Never call `Aeko.config()` or `set_tools()` per request.** Both rebuild every agent
|
|
430
|
+
process-wide; doing it under load throws away warm agents for every concurrent run.
|
|
431
|
+
- **Don't rely on in-process sessions across workers.** Always pass `history` — a session
|
|
432
|
+
that was only ever built in worker A does not exist in worker B.
|
|
433
|
+
- Configuration and sessions are shared mutable process state, so treat startup as the
|
|
434
|
+
only place that writes them.
|
|
435
|
+
|
|
436
|
+
### 6. Error handling
|
|
437
|
+
|
|
438
|
+
Every error the SDK raises inherits from `AekoError`, so one `except` covers the SDK:
|
|
439
|
+
|
|
440
|
+
| Exception | Raised when | Typical response |
|
|
441
|
+
| --- | --- | --- |
|
|
442
|
+
| `AekoNotConfiguredError` | `Aeko.config()` was never called, or the key is empty/not a string. | `503` — a deployment problem, not a user one. |
|
|
443
|
+
| `SessionNotPreparedError` | `send_message()` ran before `prepare()`. | `500` — an integration bug. |
|
|
444
|
+
| `UnknownAgentError` | `set_tools()` got a key that is not an agent name. Carries `.agent` and `.known_agents`. | Fail at startup. |
|
|
445
|
+
| `AekoError` | Base class for all of the above. | Catch-all. |
|
|
446
|
+
|
|
447
|
+
```python
|
|
448
|
+
from aeko import AekoError
|
|
449
|
+
|
|
450
|
+
try:
|
|
451
|
+
response = messenger.send_message(text)
|
|
452
|
+
except AekoError as exc:
|
|
453
|
+
logger.exception("Aeko failed")
|
|
454
|
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Remember that a *rejected* answer is not an exception — it is a successful run with an
|
|
458
|
+
empty `answer` and `approved=False`.
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## API reference
|
|
463
|
+
|
|
464
|
+
Everything below is importable directly from `aeko`.
|
|
465
|
+
|
|
466
|
+
**`Aeko`** — configuration facade.
|
|
467
|
+
|
|
468
|
+
| Member | Signature |
|
|
469
|
+
| --- | --- |
|
|
470
|
+
| `config` | `config(api_key: str, *, fast_model: str \| None = None, slow_model: str \| None = None, max_tokens: int \| None = None, report_max_tokens: int \| None = None) -> None` |
|
|
471
|
+
| `is_configured` | `is_configured() -> bool` |
|
|
472
|
+
| `reset` | `reset() -> None` |
|
|
473
|
+
|
|
474
|
+
**`AekoMessenger`** — conversational entry point.
|
|
475
|
+
|
|
476
|
+
| Member | Signature |
|
|
477
|
+
| --- | --- |
|
|
478
|
+
| `set_tools` *(classmethod)* | `set_tools(tools: dict[str, list[AekoTool \| Any]]) -> None` |
|
|
479
|
+
| `prepare` | `prepare(session_id: str, user_info: str, history: Sequence[Any] \| None = None) -> SessionInfo` |
|
|
480
|
+
| `send_message` | `send_message(message: str) -> MessageResponse` |
|
|
481
|
+
|
|
482
|
+
**`AekoInventoryAnalyzer`** — report entry point.
|
|
483
|
+
|
|
484
|
+
| Member | Signature |
|
|
485
|
+
| --- | --- |
|
|
486
|
+
| `set_context` | `set_context(context: str) -> None` |
|
|
487
|
+
| `analyze` | `analyze(inventory: str) -> InventoryAnalysisResponse` |
|
|
488
|
+
|
|
489
|
+
**Data objects** (frozen dataclasses): `AekoTool`, `SessionInfo`, `MessageResponse`,
|
|
490
|
+
`InventoryAnalysisResponse`.
|
|
491
|
+
**Exceptions**: `AekoError`, `AekoNotConfiguredError`, `SessionNotPreparedError`,
|
|
492
|
+
`UnknownAgentError`.
|
|
493
|
+
**Constants**: `AGENT_NAMES`, `__version__`.
|
|
494
|
+
|
|
495
|
+
**Defaults**
|
|
496
|
+
|
|
497
|
+
| Setting | Default |
|
|
498
|
+
| --- | --- |
|
|
499
|
+
| `fast_model` | `gemini-3.1-flash-lite` |
|
|
500
|
+
| `slow_model` | `gemini-3.5-flash` |
|
|
501
|
+
| `max_tokens` | `1024` |
|
|
502
|
+
| `report_max_tokens` | `8192` |
|
|
503
|
+
| Guardrail retry cap | `3` |
|
|
504
|
+
|
|
505
|
+
## Development
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
509
|
+
pip install -r requirements-dev.txt
|
|
510
|
+
pytest
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The test suite runs against a scripted fake chat model and never calls Gemini, so no API
|
|
514
|
+
key is needed to run it.
|
|
515
|
+
|
|
516
|
+
Releases are published to PyPI by the GitHub Actions workflow in
|
|
517
|
+
`.github/workflows/publish.yml`, triggered when a GitHub Release is published. The release
|
|
518
|
+
tag must match `aeko.__version__` or the workflow fails before building.
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
## Resumo em português
|
|
523
|
+
|
|
524
|
+
**aeko** é o núcleo inteligente do ecossistema Aether: um sistema multiagente
|
|
525
|
+
(LangChain + LangGraph) que lê o inventário de gases de efeito estufa de uma empresa,
|
|
526
|
+
explica os números e devolve um plano de melhoria priorizado — além do chatbot que
|
|
527
|
+
responde às dúvidas de ESG do dia a dia. É uma **biblioteca**, não um serviço: qualquer
|
|
528
|
+
backend Python pode embuti-la.
|
|
529
|
+
|
|
530
|
+
```bash
|
|
531
|
+
pip install aeko
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
```python
|
|
535
|
+
from aeko import Aeko, AekoMessenger, AekoInventoryAnalyzer
|
|
536
|
+
|
|
537
|
+
Aeko.config("SUA_CHAVE_GEMINI") # obrigatório: o SDK não lê variáveis de ambiente
|
|
538
|
+
|
|
539
|
+
messenger = AekoMessenger()
|
|
540
|
+
messenger.prepare(session_id="sessao-42", user_info="Ana, analista de ESG na ACME")
|
|
541
|
+
resposta = messenger.send_message("Como reduzo o escopo 1 da nossa caldeira?")
|
|
542
|
+
print(resposta.answer)
|
|
543
|
+
|
|
544
|
+
analisador = AekoInventoryAnalyzer()
|
|
545
|
+
relatorio = analisador.analyze(inventario_em_markdown)
|
|
546
|
+
print(relatorio.answer)
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Três pontos que definem a integração:
|
|
550
|
+
|
|
551
|
+
1. **Configure no startup, nunca por requisição.** `Aeko.config()` e
|
|
552
|
+
`AekoMessenger.set_tools()` alteram um runtime único do processo e reconstroem todos os
|
|
553
|
+
agentes.
|
|
554
|
+
2. **A memória de sessão vive no processo.** Com mais de um worker, carregue o histórico
|
|
555
|
+
do seu banco e passe em `prepare(session_id, user_info, history=...)`.
|
|
556
|
+
3. **Resposta vazia não é exceção.** Se o `Guardrail de Saída` reprovar todas as
|
|
557
|
+
tentativas (limite de 3), o run termina com `answer == ""` e `approved is False` —
|
|
558
|
+
verifique sempre antes de persistir.
|
|
559
|
+
|
|
560
|
+
Os nomes dos agentes (`Roteador`, `FAQ`, `Orquestrador`, `Guardrail de Saída`,
|
|
561
|
+
`Análista de inventários`, `Analista de Poluentes`, `Analista de Gases Verdes`,
|
|
562
|
+
`Coordenador de Melhoria Contínua`) são também as chaves de roteamento — use-os
|
|
563
|
+
exatamente como escritos, inclusive acentuação. A seção em inglês acima tem o detalhamento
|
|
564
|
+
completo, incluindo um exemplo de serviço FastAPI stateless.
|
|
565
|
+
|
|
566
|
+
## License
|
|
567
|
+
|
|
568
|
+
MIT — see [LICENSE](LICENSE).
|