cortex-agent-sdk 0.0.2__tar.gz → 0.0.4__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.
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/PKG-INFO +72 -14
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/README.md +71 -13
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/pyproject.toml +1 -1
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/agent.py +5 -2
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/engine.py +19 -15
- cortex_agent_sdk-0.0.4/src/cortex_agent_sdk/google/__init__.py +1 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/history/models.py +46 -37
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/history/transform.py +2 -3
- cortex_agent_sdk-0.0.4/src/cortex_agent_sdk/immutable.py +75 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/openai/__init__.py +1 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/openai/engine.py +25 -17
- cortex_agent_sdk-0.0.4/src/cortex_agent_sdk/openai/models.py +57 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/redis/store.py +32 -17
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/tools/contracts.py +139 -27
- cortex_agent_sdk-0.0.4/src/cortex_agent_sdk/tools/decorators.py +65 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/tools/execution.py +8 -7
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/tools/models.py +14 -13
- cortex_agent_sdk-0.0.2/src/cortex_agent_sdk/google/__init__.py +0 -1
- cortex_agent_sdk-0.0.2/src/cortex_agent_sdk/immutable.py +0 -54
- cortex_agent_sdk-0.0.2/src/cortex_agent_sdk/tools/decorators.py +0 -15
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/.gitignore +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/LICENSE +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/errores/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/errores/catalogo.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/errores/excepcion.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/gateway.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/history/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/history/pipeline.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/hooks.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/lifecycle.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/openai/options.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/postgres/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/postgres/store.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/py.typed +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/redis/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/redis/scripts.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/results.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/runtime.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/__init__.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/codec.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/lease.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/memory.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/models.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/sessions/store.py +0 -0
- {cortex_agent_sdk-0.0.2 → cortex_agent_sdk-0.0.4}/src/cortex_agent_sdk/tools/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cortex-agent-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: SDK async y multiproveedor para construir agentes con control explícito
|
|
5
5
|
Project-URL: Repository, https://github.com/epok200/cortex_agent_sdk
|
|
6
6
|
Project-URL: Issues, https://github.com/epok200/cortex_agent_sdk/issues
|
|
@@ -137,10 +137,51 @@ async def main() -> None:
|
|
|
137
137
|
asyncio.run(main())
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
Los type hints son la vía recomendada para tools sencillas. Cortex infiere el contrato visible y
|
|
141
|
+
valida los argumentos antes de ejecutar la función; no hace falta escribir JSON Schema ni
|
|
142
|
+
`ToolSpec` manualmente.
|
|
143
|
+
|
|
144
|
+
### Contratos Pydantic
|
|
145
|
+
|
|
146
|
+
Cuando los argumentos forman un contrato reutilizable o tienen validaciones entre campos, puede
|
|
147
|
+
usarse un `BaseModel` como fuente de verdad:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from datetime import datetime
|
|
151
|
+
from typing import Self
|
|
152
|
+
|
|
153
|
+
from pydantic import BaseModel, Field, model_validator
|
|
154
|
+
|
|
155
|
+
from cortex_agent_sdk import final_answer
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ReminderArgs(BaseModel):
|
|
159
|
+
message: str = Field(min_length=1, description="Texto del recordatorio.")
|
|
160
|
+
when: datetime | None = None
|
|
161
|
+
cron: str | None = None
|
|
162
|
+
|
|
163
|
+
@model_validator(mode="after")
|
|
164
|
+
def validate_mode(self) -> Self:
|
|
165
|
+
if (self.when is None) == (self.cron is None):
|
|
166
|
+
raise ValueError("se requiere exactamente when o cron")
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@final_answer(input_model=ReminderArgs)
|
|
171
|
+
async def schedule_reminder(args: ReminderArgs) -> str:
|
|
172
|
+
"""Programa un recordatorio."""
|
|
173
|
+
return f"Recordatorio: {args.message}"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Cortex genera el `ToolSpec` neutral a partir del modelo, rechaza propiedades adicionales y entrega a
|
|
177
|
+
la función una instancia ya validada. `ToolBinding(input_model=...)` ofrece la misma capacidad para
|
|
178
|
+
tools construidas en runtime. El `ToolSpec` manual continúa disponible como escape hatch cuando el
|
|
179
|
+
schema visible necesita construirse dinámicamente o requiere control de bajo nivel.
|
|
180
|
+
|
|
140
181
|
## Capacidades del alfa
|
|
141
182
|
|
|
142
183
|
- Loop async acotado.
|
|
143
|
-
- Tools async con schema inferido o
|
|
184
|
+
- Tools async con schema inferido, Pydantic explícito o `ToolSpec` manual.
|
|
144
185
|
- Historial y sesiones en memoria, Redis o PostgreSQL.
|
|
145
186
|
- Hooks locales.
|
|
146
187
|
- Timeouts para providers y tools.
|
|
@@ -159,6 +200,7 @@ renovable, fencing token y compare-and-swap.
|
|
|
159
200
|
Redis no requiere inicialización de schema:
|
|
160
201
|
|
|
161
202
|
```python
|
|
203
|
+
import asyncio
|
|
162
204
|
import os
|
|
163
205
|
|
|
164
206
|
from cortex_agent_sdk import Agent
|
|
@@ -166,26 +208,38 @@ from cortex_agent_sdk.openai import OpenAIEngine
|
|
|
166
208
|
from cortex_agent_sdk.redis import RedisSessionStore
|
|
167
209
|
|
|
168
210
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await agent.
|
|
211
|
+
async def main() -> None:
|
|
212
|
+
store = RedisSessionStore(os.environ["REDIS_URL"])
|
|
213
|
+
async with Agent(
|
|
214
|
+
OpenAIEngine("gpt-5.6-luna"),
|
|
215
|
+
session_store=store,
|
|
216
|
+
own_session_store=True,
|
|
217
|
+
) as agent:
|
|
218
|
+
result = await agent.run("Hola", session_id="producto:tenant:usuario")
|
|
219
|
+
print(result.text)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
asyncio.run(main())
|
|
177
223
|
```
|
|
178
224
|
|
|
179
225
|
PostgreSQL exige crear su tabla de forma explícita una vez:
|
|
180
226
|
|
|
181
227
|
```python
|
|
228
|
+
import asyncio
|
|
182
229
|
import os
|
|
183
230
|
|
|
184
231
|
from cortex_agent_sdk.postgres import PostgresSessionStore
|
|
185
232
|
|
|
186
233
|
|
|
187
|
-
|
|
188
|
-
|
|
234
|
+
async def main() -> None:
|
|
235
|
+
store = PostgresSessionStore(os.environ["POSTGRES_URL"])
|
|
236
|
+
try:
|
|
237
|
+
await store.setup()
|
|
238
|
+
finally:
|
|
239
|
+
await store.aclose()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
asyncio.run(main())
|
|
189
243
|
```
|
|
190
244
|
|
|
191
245
|
Una tarea periódica puede ejecutar `await store.cleanup_expired()` para vaciar historiales vencidos
|
|
@@ -193,8 +247,12 @@ que nunca volvieron a solicitarse. El row mínimo permanece para conservar el fe
|
|
|
193
247
|
|
|
194
248
|
`Agent.aclose()` hace un cierre ordenado: deja de aceptar turnos nuevos, espera los turnos activos y
|
|
195
249
|
después cierra los recursos que posee. El límite se configura con
|
|
196
|
-
`AgentOptions.shutdown_timeout_seconds
|
|
197
|
-
|
|
250
|
+
`AgentOptions.shutdown_timeout_seconds` y cubre tanto el drenado como el cierre físico. Si vence
|
|
251
|
+
mientras hay un turno activo, el SDK no lo cancela ni empieza a cerrar recursos. Si vence durante el
|
|
252
|
+
cierre físico, algunos recursos podrían haberse cerrado ya. El timeout es un presupuesto de cierre,
|
|
253
|
+
no una garantía estricta de tiempo de pared: un finalizador que resista la cancelación puede retrasar
|
|
254
|
+
el retorno para no abandonar recursos a medias. Al excederlo regresa `RUNTIME_CIERRE_TIMEOUT` y una
|
|
255
|
+
segunda llamada a `aclose()` reintenta lo pendiente.
|
|
198
256
|
|
|
199
257
|
Una sesión dañada o deliberadamente descartada se elimina mediante
|
|
200
258
|
`await agent.reset_session(session_id)`.
|
|
@@ -103,10 +103,51 @@ async def main() -> None:
|
|
|
103
103
|
asyncio.run(main())
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Los type hints son la vía recomendada para tools sencillas. Cortex infiere el contrato visible y
|
|
107
|
+
valida los argumentos antes de ejecutar la función; no hace falta escribir JSON Schema ni
|
|
108
|
+
`ToolSpec` manualmente.
|
|
109
|
+
|
|
110
|
+
### Contratos Pydantic
|
|
111
|
+
|
|
112
|
+
Cuando los argumentos forman un contrato reutilizable o tienen validaciones entre campos, puede
|
|
113
|
+
usarse un `BaseModel` como fuente de verdad:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from datetime import datetime
|
|
117
|
+
from typing import Self
|
|
118
|
+
|
|
119
|
+
from pydantic import BaseModel, Field, model_validator
|
|
120
|
+
|
|
121
|
+
from cortex_agent_sdk import final_answer
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ReminderArgs(BaseModel):
|
|
125
|
+
message: str = Field(min_length=1, description="Texto del recordatorio.")
|
|
126
|
+
when: datetime | None = None
|
|
127
|
+
cron: str | None = None
|
|
128
|
+
|
|
129
|
+
@model_validator(mode="after")
|
|
130
|
+
def validate_mode(self) -> Self:
|
|
131
|
+
if (self.when is None) == (self.cron is None):
|
|
132
|
+
raise ValueError("se requiere exactamente when o cron")
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@final_answer(input_model=ReminderArgs)
|
|
137
|
+
async def schedule_reminder(args: ReminderArgs) -> str:
|
|
138
|
+
"""Programa un recordatorio."""
|
|
139
|
+
return f"Recordatorio: {args.message}"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Cortex genera el `ToolSpec` neutral a partir del modelo, rechaza propiedades adicionales y entrega a
|
|
143
|
+
la función una instancia ya validada. `ToolBinding(input_model=...)` ofrece la misma capacidad para
|
|
144
|
+
tools construidas en runtime. El `ToolSpec` manual continúa disponible como escape hatch cuando el
|
|
145
|
+
schema visible necesita construirse dinámicamente o requiere control de bajo nivel.
|
|
146
|
+
|
|
106
147
|
## Capacidades del alfa
|
|
107
148
|
|
|
108
149
|
- Loop async acotado.
|
|
109
|
-
- Tools async con schema inferido o
|
|
150
|
+
- Tools async con schema inferido, Pydantic explícito o `ToolSpec` manual.
|
|
110
151
|
- Historial y sesiones en memoria, Redis o PostgreSQL.
|
|
111
152
|
- Hooks locales.
|
|
112
153
|
- Timeouts para providers y tools.
|
|
@@ -125,6 +166,7 @@ renovable, fencing token y compare-and-swap.
|
|
|
125
166
|
Redis no requiere inicialización de schema:
|
|
126
167
|
|
|
127
168
|
```python
|
|
169
|
+
import asyncio
|
|
128
170
|
import os
|
|
129
171
|
|
|
130
172
|
from cortex_agent_sdk import Agent
|
|
@@ -132,26 +174,38 @@ from cortex_agent_sdk.openai import OpenAIEngine
|
|
|
132
174
|
from cortex_agent_sdk.redis import RedisSessionStore
|
|
133
175
|
|
|
134
176
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
await agent.
|
|
177
|
+
async def main() -> None:
|
|
178
|
+
store = RedisSessionStore(os.environ["REDIS_URL"])
|
|
179
|
+
async with Agent(
|
|
180
|
+
OpenAIEngine("gpt-5.6-luna"),
|
|
181
|
+
session_store=store,
|
|
182
|
+
own_session_store=True,
|
|
183
|
+
) as agent:
|
|
184
|
+
result = await agent.run("Hola", session_id="producto:tenant:usuario")
|
|
185
|
+
print(result.text)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
asyncio.run(main())
|
|
143
189
|
```
|
|
144
190
|
|
|
145
191
|
PostgreSQL exige crear su tabla de forma explícita una vez:
|
|
146
192
|
|
|
147
193
|
```python
|
|
194
|
+
import asyncio
|
|
148
195
|
import os
|
|
149
196
|
|
|
150
197
|
from cortex_agent_sdk.postgres import PostgresSessionStore
|
|
151
198
|
|
|
152
199
|
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
async def main() -> None:
|
|
201
|
+
store = PostgresSessionStore(os.environ["POSTGRES_URL"])
|
|
202
|
+
try:
|
|
203
|
+
await store.setup()
|
|
204
|
+
finally:
|
|
205
|
+
await store.aclose()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
asyncio.run(main())
|
|
155
209
|
```
|
|
156
210
|
|
|
157
211
|
Una tarea periódica puede ejecutar `await store.cleanup_expired()` para vaciar historiales vencidos
|
|
@@ -159,8 +213,12 @@ que nunca volvieron a solicitarse. El row mínimo permanece para conservar el fe
|
|
|
159
213
|
|
|
160
214
|
`Agent.aclose()` hace un cierre ordenado: deja de aceptar turnos nuevos, espera los turnos activos y
|
|
161
215
|
después cierra los recursos que posee. El límite se configura con
|
|
162
|
-
`AgentOptions.shutdown_timeout_seconds
|
|
163
|
-
|
|
216
|
+
`AgentOptions.shutdown_timeout_seconds` y cubre tanto el drenado como el cierre físico. Si vence
|
|
217
|
+
mientras hay un turno activo, el SDK no lo cancela ni empieza a cerrar recursos. Si vence durante el
|
|
218
|
+
cierre físico, algunos recursos podrían haberse cerrado ya. El timeout es un presupuesto de cierre,
|
|
219
|
+
no una garantía estricta de tiempo de pared: un finalizador que resista la cancelación puede retrasar
|
|
220
|
+
el retorno para no abandonar recursos a medias. Al excederlo regresa `RUNTIME_CIERRE_TIMEOUT` y una
|
|
221
|
+
segunda llamada a `aclose()` reintenta lo pendiente.
|
|
164
222
|
|
|
165
223
|
Una sesión dañada o deliberadamente descartada se elimina mediante
|
|
166
224
|
`await agent.reset_session(session_id)`.
|
|
@@ -43,7 +43,10 @@ class Agent:
|
|
|
43
43
|
self._options = options or AgentOptions()
|
|
44
44
|
self._history = HistoryPipeline(history_transform, self._options.max_history_turns)
|
|
45
45
|
self._hooks = HookChain(hooks or AgentHooks(), self._options.hook_timeout_seconds)
|
|
46
|
-
|
|
46
|
+
if session_store is None:
|
|
47
|
+
self._session_store = MemorySessionStore()
|
|
48
|
+
else:
|
|
49
|
+
self._session_store = session_store
|
|
47
50
|
self._own_engine = own_engine
|
|
48
51
|
self._own_session_store = session_store is None or own_session_store
|
|
49
52
|
self._lifecycle = AgentLifecycle()
|
|
@@ -65,7 +68,7 @@ class Agent:
|
|
|
65
68
|
instructions: str | None = None,
|
|
66
69
|
) -> AgentResult:
|
|
67
70
|
async with self._lifecycle.run():
|
|
68
|
-
if not text:
|
|
71
|
+
if not text.strip():
|
|
69
72
|
raise AppError(CodigoError.CONFIG_INVALIDA, "text no puede estar vacío")
|
|
70
73
|
|
|
71
74
|
tool_set = ToolSet.build((*self._static_tools, *tuple(tools)))
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from types import MappingProxyType
|
|
4
|
-
from typing import Protocol,
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, JsonValue, field_serializer, field_validator
|
|
7
7
|
|
|
8
8
|
from cortex_agent_sdk.history.models import ToolCallPart, Turn
|
|
9
|
-
from cortex_agent_sdk.immutable import
|
|
9
|
+
from cortex_agent_sdk.immutable import JsonObject, freeze_json_object, thaw_json_object
|
|
10
10
|
from cortex_agent_sdk.tools.models import ToolSpec
|
|
11
11
|
|
|
12
12
|
|
|
@@ -15,25 +15,22 @@ class ToolCall(BaseModel):
|
|
|
15
15
|
|
|
16
16
|
call_id: str
|
|
17
17
|
name: str
|
|
18
|
-
arguments:
|
|
18
|
+
arguments: JsonObject
|
|
19
19
|
|
|
20
20
|
@field_validator("arguments", mode="after")
|
|
21
21
|
@classmethod
|
|
22
22
|
def freeze_arguments(
|
|
23
23
|
cls,
|
|
24
|
-
value:
|
|
25
|
-
) ->
|
|
26
|
-
|
|
27
|
-
if not isinstance(frozen, Mapping):
|
|
28
|
-
raise TypeError("arguments debe ser un objeto")
|
|
29
|
-
return cast(Mapping[str, JsonValue], frozen)
|
|
24
|
+
value: JsonObject,
|
|
25
|
+
) -> JsonObject:
|
|
26
|
+
return freeze_json_object(value)
|
|
30
27
|
|
|
31
28
|
@field_serializer("arguments", when_used="json")
|
|
32
29
|
def serialize_arguments(
|
|
33
30
|
self,
|
|
34
|
-
value:
|
|
35
|
-
) -> dict[str,
|
|
36
|
-
return
|
|
31
|
+
value: JsonObject,
|
|
32
|
+
) -> dict[str, JsonValue]:
|
|
33
|
+
return thaw_json_object(value)
|
|
37
34
|
|
|
38
35
|
|
|
39
36
|
@dataclass(frozen=True, slots=True)
|
|
@@ -117,7 +114,7 @@ class EngineResult:
|
|
|
117
114
|
isinstance(call, ToolCall) for call in self.tool_calls
|
|
118
115
|
)
|
|
119
116
|
valid_structured = self.structured is None or isinstance(self.structured, BaseModel)
|
|
120
|
-
if not isinstance(self.turn, Turn) or not valid_calls:
|
|
117
|
+
if not isinstance(self.turn, Turn) or self.turn.role != "assistant" or not valid_calls:
|
|
121
118
|
raise TypeError("EngineResult inválido")
|
|
122
119
|
turn_calls = tuple(part for part in self.turn.parts if isinstance(part, ToolCallPart))
|
|
123
120
|
aligned_calls = len(turn_calls) == len(self.tool_calls) and all(
|
|
@@ -128,7 +125,15 @@ class EngineResult:
|
|
|
128
125
|
)
|
|
129
126
|
if not aligned_calls:
|
|
130
127
|
raise TypeError("tool_calls no coincide con el turno")
|
|
131
|
-
|
|
128
|
+
valid_identity = (
|
|
129
|
+
isinstance(self.provider, str)
|
|
130
|
+
and bool(self.provider)
|
|
131
|
+
and isinstance(self.model, str)
|
|
132
|
+
and bool(self.model)
|
|
133
|
+
)
|
|
134
|
+
provider_state = self.turn.provider_state
|
|
135
|
+
state_matches = provider_state is None or provider_state.provider == self.provider
|
|
136
|
+
if not isinstance(self.usage, Usage) or not valid_identity or not state_matches:
|
|
132
137
|
raise TypeError("EngineResult inválido")
|
|
133
138
|
if self.stop_reason is not None and not isinstance(self.stop_reason, str):
|
|
134
139
|
raise TypeError("stop_reason inválido")
|
|
@@ -147,4 +152,3 @@ class ModelEngine(Protocol):
|
|
|
147
152
|
async def generate(self, request: EngineRequest) -> EngineResult: ...
|
|
148
153
|
|
|
149
154
|
async def aclose(self) -> None: ...
|
|
150
|
-
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Espacio reservado para el futuro engine de Google."""
|
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
from collections.abc import Mapping
|
|
3
|
-
from typing import Annotated, Literal,
|
|
4
|
-
|
|
5
|
-
from pydantic import
|
|
3
|
+
from typing import Annotated, Literal, Self
|
|
4
|
+
|
|
5
|
+
from pydantic import (
|
|
6
|
+
BaseModel,
|
|
7
|
+
ConfigDict,
|
|
8
|
+
Field,
|
|
9
|
+
JsonValue,
|
|
10
|
+
field_serializer,
|
|
11
|
+
field_validator,
|
|
12
|
+
model_validator,
|
|
13
|
+
)
|
|
6
14
|
|
|
7
15
|
from cortex_agent_sdk.immutable import (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
JsonObject,
|
|
17
|
+
ProviderItem,
|
|
18
|
+
ProviderValue,
|
|
19
|
+
freeze_json_object,
|
|
20
|
+
freeze_provider_item,
|
|
21
|
+
thaw_json_object,
|
|
13
22
|
)
|
|
14
23
|
|
|
15
24
|
_BYTES_TAG = "$cortex.bytes"
|
|
16
25
|
|
|
17
|
-
ProviderValue = FrozenProviderValue
|
|
18
|
-
|
|
19
26
|
|
|
20
27
|
class ProviderState(BaseModel):
|
|
21
28
|
"""Items nativos necesarios para continuar sin pérdida con un provider."""
|
|
@@ -23,7 +30,7 @@ class ProviderState(BaseModel):
|
|
|
23
30
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
24
31
|
|
|
25
32
|
provider: str
|
|
26
|
-
items: tuple[
|
|
33
|
+
items: tuple[ProviderItem, ...]
|
|
27
34
|
|
|
28
35
|
@field_validator("items", mode="before")
|
|
29
36
|
@classmethod
|
|
@@ -34,14 +41,14 @@ class ProviderState(BaseModel):
|
|
|
34
41
|
@classmethod
|
|
35
42
|
def freeze_items(
|
|
36
43
|
cls,
|
|
37
|
-
value: tuple[
|
|
38
|
-
) -> tuple[
|
|
39
|
-
return tuple(
|
|
44
|
+
value: tuple[ProviderItem, ...],
|
|
45
|
+
) -> tuple[ProviderItem, ...]:
|
|
46
|
+
return tuple(freeze_provider_item(item) for item in value)
|
|
40
47
|
|
|
41
48
|
@field_serializer("items", when_used="json")
|
|
42
49
|
def encode_bytes(
|
|
43
50
|
self,
|
|
44
|
-
items: tuple[
|
|
51
|
+
items: tuple[ProviderItem, ...],
|
|
45
52
|
) -> tuple[dict[str, object], ...]:
|
|
46
53
|
return tuple(
|
|
47
54
|
{key: _encode_provider_bytes(value) for key, value in item.items()} for item in items
|
|
@@ -61,25 +68,22 @@ class ToolCallPart(BaseModel):
|
|
|
61
68
|
type: Literal["tool_call"] = "tool_call"
|
|
62
69
|
call_id: str
|
|
63
70
|
name: str
|
|
64
|
-
arguments:
|
|
71
|
+
arguments: JsonObject
|
|
65
72
|
|
|
66
73
|
@field_validator("arguments", mode="after")
|
|
67
74
|
@classmethod
|
|
68
75
|
def freeze_arguments(
|
|
69
76
|
cls,
|
|
70
|
-
value:
|
|
71
|
-
) ->
|
|
72
|
-
|
|
73
|
-
if not isinstance(frozen, Mapping):
|
|
74
|
-
raise TypeError("arguments debe ser un objeto")
|
|
75
|
-
return cast(Mapping[str, JsonValue], frozen)
|
|
77
|
+
value: JsonObject,
|
|
78
|
+
) -> JsonObject:
|
|
79
|
+
return freeze_json_object(value)
|
|
76
80
|
|
|
77
81
|
@field_serializer("arguments", when_used="json")
|
|
78
82
|
def serialize_arguments(
|
|
79
83
|
self,
|
|
80
|
-
value:
|
|
81
|
-
) -> dict[str,
|
|
82
|
-
return
|
|
84
|
+
value: JsonObject,
|
|
85
|
+
) -> dict[str, JsonValue]:
|
|
86
|
+
return thaw_json_object(value)
|
|
83
87
|
|
|
84
88
|
|
|
85
89
|
class ToolResultPart(BaseModel):
|
|
@@ -93,7 +97,7 @@ class ToolResultPart(BaseModel):
|
|
|
93
97
|
error_code: str | None = None
|
|
94
98
|
|
|
95
99
|
|
|
96
|
-
|
|
100
|
+
HistoryPart = Annotated[TextPart | ToolCallPart | ToolResultPart, Field(discriminator="type")]
|
|
97
101
|
|
|
98
102
|
|
|
99
103
|
class Turn(BaseModel):
|
|
@@ -102,9 +106,24 @@ class Turn(BaseModel):
|
|
|
102
106
|
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
103
107
|
|
|
104
108
|
role: Literal["user", "assistant", "tool"]
|
|
105
|
-
parts: tuple[
|
|
109
|
+
parts: tuple[HistoryPart, ...]
|
|
106
110
|
provider_state: ProviderState | None = None
|
|
107
111
|
|
|
112
|
+
@model_validator(mode="after")
|
|
113
|
+
def validate_role_contract(self) -> Self:
|
|
114
|
+
if self.role == "user":
|
|
115
|
+
valid_parts = all(isinstance(part, TextPart) for part in self.parts)
|
|
116
|
+
elif self.role == "assistant":
|
|
117
|
+
valid_parts = all(isinstance(part, TextPart | ToolCallPart) for part in self.parts)
|
|
118
|
+
else:
|
|
119
|
+
valid_parts = all(isinstance(part, ToolResultPart) for part in self.parts)
|
|
120
|
+
|
|
121
|
+
if not valid_parts:
|
|
122
|
+
raise ValueError(f"parts incompatibles con role={self.role}")
|
|
123
|
+
if self.role != "assistant" and self.provider_state is not None:
|
|
124
|
+
raise ValueError("provider_state requiere role=assistant")
|
|
125
|
+
return self
|
|
126
|
+
|
|
108
127
|
@classmethod
|
|
109
128
|
def user(cls, text: str) -> "Turn":
|
|
110
129
|
return cls(role="user", parts=(TextPart(text=text),))
|
|
@@ -133,13 +152,3 @@ def _decode_provider_bytes(value: object) -> object:
|
|
|
133
152
|
if set(value) == {_BYTES_TAG} and isinstance(value[_BYTES_TAG], str):
|
|
134
153
|
return base64.b64decode(value[_BYTES_TAG], validate=True)
|
|
135
154
|
return {key: _decode_provider_bytes(item) for key, item in value.items()}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _freeze_provider_item(
|
|
139
|
-
value: Mapping[str, ProviderValue],
|
|
140
|
-
) -> Mapping[str, ProviderValue]:
|
|
141
|
-
frozen = freeze_provider(value)
|
|
142
|
-
if not isinstance(frozen, Mapping):
|
|
143
|
-
raise TypeError("provider item debe ser un objeto")
|
|
144
|
-
return frozen
|
|
145
|
-
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from collections.abc import Awaitable, Callable
|
|
1
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
|
|
@@ -13,5 +13,4 @@ class TransformContext:
|
|
|
13
13
|
session_id: str | None
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
HistoryTransform = Callable[[TransformContext], Awaitable[
|
|
17
|
-
|
|
16
|
+
HistoryTransform = Callable[[TransformContext], Awaitable[Sequence[Turn]]]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from pydantic import JsonValue
|
|
6
|
+
|
|
7
|
+
type FrozenJsonValue = (
|
|
8
|
+
bool | int | float | str | tuple[FrozenJsonValue, ...] | Mapping[str, FrozenJsonValue] | None
|
|
9
|
+
)
|
|
10
|
+
type JsonInputObject = Mapping[str, JsonValue]
|
|
11
|
+
type FrozenJsonObject = Mapping[str, FrozenJsonValue]
|
|
12
|
+
type JsonObject = JsonInputObject | FrozenJsonObject
|
|
13
|
+
type ProviderValue = (
|
|
14
|
+
bool
|
|
15
|
+
| int
|
|
16
|
+
| float
|
|
17
|
+
| str
|
|
18
|
+
| bytes
|
|
19
|
+
| tuple[ProviderValue, ...]
|
|
20
|
+
| Mapping[str, ProviderValue]
|
|
21
|
+
| None
|
|
22
|
+
)
|
|
23
|
+
type ProviderItem = Mapping[str, ProviderValue]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def freeze_json_object(value: JsonObject) -> FrozenJsonObject:
|
|
27
|
+
frozen = {key: _freeze_json(item) for key, item in value.items()}
|
|
28
|
+
return MappingProxyType(frozen)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def thaw_json_object(value: JsonObject) -> dict[str, JsonValue]:
|
|
32
|
+
return {key: _thaw_json(item) for key, item in value.items()}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def freeze_provider_item(value: ProviderItem) -> ProviderItem:
|
|
36
|
+
frozen = {key: _freeze_provider(item) for key, item in value.items()}
|
|
37
|
+
return MappingProxyType(frozen)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def thaw_provider_item(value: ProviderItem) -> dict[str, object]:
|
|
41
|
+
return {key: _thaw_provider(item) for key, item in value.items()}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _freeze_json(value: JsonValue | FrozenJsonValue) -> FrozenJsonValue:
|
|
45
|
+
if isinstance(value, list | tuple):
|
|
46
|
+
return tuple(_freeze_json(item) for item in value)
|
|
47
|
+
if isinstance(value, Mapping):
|
|
48
|
+
frozen = {key: _freeze_json(item) for key, item in value.items()}
|
|
49
|
+
return MappingProxyType(frozen)
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _thaw_json(value: JsonValue | FrozenJsonValue) -> JsonValue:
|
|
54
|
+
if isinstance(value, list | tuple):
|
|
55
|
+
return [_thaw_json(item) for item in value]
|
|
56
|
+
if isinstance(value, Mapping):
|
|
57
|
+
return {key: _thaw_json(item) for key, item in value.items()}
|
|
58
|
+
return cast(JsonValue, value)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _freeze_provider(value: ProviderValue) -> ProviderValue:
|
|
62
|
+
if isinstance(value, tuple):
|
|
63
|
+
return tuple(_freeze_provider(item) for item in value)
|
|
64
|
+
if isinstance(value, Mapping):
|
|
65
|
+
frozen = {key: _freeze_provider(item) for key, item in value.items()}
|
|
66
|
+
return MappingProxyType(frozen)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _thaw_provider(value: ProviderValue) -> object:
|
|
71
|
+
if isinstance(value, tuple):
|
|
72
|
+
return [_thaw_provider(item) for item in value]
|
|
73
|
+
if isinstance(value, Mapping):
|
|
74
|
+
return {key: _thaw_provider(item) for key, item in value.items()}
|
|
75
|
+
return value
|