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.
Files changed (39) hide show
  1. aeko-1.0.0/.gitignore +40 -0
  2. aeko-1.0.0/LICENSE +21 -0
  3. aeko-1.0.0/PKG-INFO +568 -0
  4. aeko-1.0.0/README.md +545 -0
  5. aeko-1.0.0/aeko/__init__.py +34 -0
  6. aeko-1.0.0/aeko/config/__init__.py +31 -0
  7. aeko-1.0.0/aeko/config/_text.py +21 -0
  8. aeko-1.0.0/aeko/config/aeko.py +63 -0
  9. aeko-1.0.0/aeko/config/dto.py +117 -0
  10. aeko-1.0.0/aeko/config/exceptions.py +21 -0
  11. aeko-1.0.0/aeko/config/inventory.py +75 -0
  12. aeko-1.0.0/aeko/config/messenger.py +186 -0
  13. aeko-1.0.0/aeko/engine/__init__.py +0 -0
  14. aeko-1.0.0/aeko/engine/agents/agents.py +112 -0
  15. aeko-1.0.0/aeko/engine/agents/llms.py +58 -0
  16. aeko-1.0.0/aeko/engine/graph/builder.py +221 -0
  17. aeko-1.0.0/aeko/engine/graph/nodes.py +397 -0
  18. aeko-1.0.0/aeko/engine/graph/state.py +124 -0
  19. aeko-1.0.0/aeko/engine/prompts/__init__.py +55 -0
  20. aeko-1.0.0/aeko/engine/prompts/builder.py +132 -0
  21. aeko-1.0.0/aeko/engine/prompts/continuous_improvement_coordinator.py +47 -0
  22. aeko-1.0.0/aeko/engine/prompts/faq.py +50 -0
  23. aeko-1.0.0/aeko/engine/prompts/green_gases_analyst.py +45 -0
  24. aeko-1.0.0/aeko/engine/prompts/orchestrator.py +49 -0
  25. aeko-1.0.0/aeko/engine/prompts/output_guardrail.py +50 -0
  26. aeko-1.0.0/aeko/engine/prompts/pollutants_analyst.py +50 -0
  27. aeko-1.0.0/aeko/engine/prompts/report_analyst.py +48 -0
  28. aeko-1.0.0/aeko/engine/prompts/router.py +54 -0
  29. aeko-1.0.0/aeko/engine/runtime.py +115 -0
  30. aeko-1.0.0/aeko/py.typed +0 -0
  31. aeko-1.0.0/pyproject.toml +45 -0
  32. aeko-1.0.0/tests/conftest.py +172 -0
  33. aeko-1.0.0/tests/test_agents.py +142 -0
  34. aeko-1.0.0/tests/test_config.py +439 -0
  35. aeko-1.0.0/tests/test_graph_builder.py +110 -0
  36. aeko-1.0.0/tests/test_graph_e2e.py +184 -0
  37. aeko-1.0.0/tests/test_llms.py +126 -0
  38. aeko-1.0.0/tests/test_nodes.py +407 -0
  39. 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).