cortex-agent-sdk 0.0.1__py3-none-any.whl
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/__init__.py +7 -0
- cortex_agent_sdk/agent.py +321 -0
- cortex_agent_sdk/engine.py +150 -0
- cortex_agent_sdk/errores/__init__.py +2 -0
- cortex_agent_sdk/errores/catalogo.py +35 -0
- cortex_agent_sdk/errores/excepcion.py +19 -0
- cortex_agent_sdk/gateway.py +23 -0
- cortex_agent_sdk/google/__init__.py +1 -0
- cortex_agent_sdk/history/__init__.py +2 -0
- cortex_agent_sdk/history/models.py +145 -0
- cortex_agent_sdk/history/pipeline.py +53 -0
- cortex_agent_sdk/history/transform.py +17 -0
- cortex_agent_sdk/hooks.py +102 -0
- cortex_agent_sdk/immutable.py +54 -0
- cortex_agent_sdk/lifecycle.py +67 -0
- cortex_agent_sdk/openai/__init__.py +2 -0
- cortex_agent_sdk/openai/engine.py +304 -0
- cortex_agent_sdk/openai/options.py +19 -0
- cortex_agent_sdk/postgres/__init__.py +1 -0
- cortex_agent_sdk/postgres/store.py +468 -0
- cortex_agent_sdk/py.typed +1 -0
- cortex_agent_sdk/redis/__init__.py +1 -0
- cortex_agent_sdk/redis/scripts.py +65 -0
- cortex_agent_sdk/redis/store.py +293 -0
- cortex_agent_sdk/results.py +29 -0
- cortex_agent_sdk/runtime.py +39 -0
- cortex_agent_sdk/sessions/__init__.py +4 -0
- cortex_agent_sdk/sessions/codec.py +27 -0
- cortex_agent_sdk/sessions/lease.py +85 -0
- cortex_agent_sdk/sessions/memory.py +186 -0
- cortex_agent_sdk/sessions/models.py +70 -0
- cortex_agent_sdk/sessions/store.py +38 -0
- cortex_agent_sdk/tools/__init__.py +1 -0
- cortex_agent_sdk/tools/contracts.py +152 -0
- cortex_agent_sdk/tools/decorators.py +15 -0
- cortex_agent_sdk/tools/execution.py +181 -0
- cortex_agent_sdk/tools/models.py +57 -0
- cortex_agent_sdk-0.0.1.dist-info/METADATA +192 -0
- cortex_agent_sdk-0.0.1.dist-info/RECORD +41 -0
- cortex_agent_sdk-0.0.1.dist-info/WHEEL +4 -0
- cortex_agent_sdk-0.0.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from importlib.metadata import version as _package_version
|
|
2
|
+
|
|
3
|
+
from cortex_agent_sdk.agent import Agent
|
|
4
|
+
from cortex_agent_sdk.results import AgentResult
|
|
5
|
+
from cortex_agent_sdk.tools.decorators import final_answer, tool
|
|
6
|
+
|
|
7
|
+
__version__ = _package_version("cortex-agent-sdk")
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Awaitable, Callable, Iterable
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Self
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from cortex_agent_sdk.engine import EngineRequest, EngineResult, ModelEngine, ToolCall
|
|
10
|
+
from cortex_agent_sdk.errores import AppError, CodigoError
|
|
11
|
+
from cortex_agent_sdk.history.models import Turn
|
|
12
|
+
from cortex_agent_sdk.history.pipeline import HistoryPipeline
|
|
13
|
+
from cortex_agent_sdk.history.transform import HistoryTransform
|
|
14
|
+
from cortex_agent_sdk.hooks import AgentHooks, HookChain
|
|
15
|
+
from cortex_agent_sdk.lifecycle import AgentLifecycle
|
|
16
|
+
from cortex_agent_sdk.results import AgentExitReason, AgentResult
|
|
17
|
+
from cortex_agent_sdk.runtime import AgentOptions, RunState
|
|
18
|
+
from cortex_agent_sdk.sessions import MemorySessionStore, SessionRecord, SessionStore
|
|
19
|
+
from cortex_agent_sdk.sessions.lease import LeaseKeeper
|
|
20
|
+
from cortex_agent_sdk.tools.execution import ToolExecutor, ToolOutcome, ToolSet
|
|
21
|
+
from cortex_agent_sdk.tools.models import ToolBinding, ToolFunction
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Agent:
|
|
25
|
+
"""Fachada async y dueño único del loop de tools."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
engine: ModelEngine,
|
|
30
|
+
*,
|
|
31
|
+
instructions: str = "",
|
|
32
|
+
tools: Iterable[ToolFunction | ToolBinding] = (),
|
|
33
|
+
hooks: AgentHooks | None = None,
|
|
34
|
+
history_transform: HistoryTransform | None = None,
|
|
35
|
+
session_store: SessionStore | None = None,
|
|
36
|
+
options: AgentOptions | None = None,
|
|
37
|
+
own_engine: bool = True,
|
|
38
|
+
own_session_store: bool = False,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._engine = engine
|
|
41
|
+
self._instructions = instructions
|
|
42
|
+
self._static_tools = tuple(tools)
|
|
43
|
+
self._options = options or AgentOptions()
|
|
44
|
+
self._history = HistoryPipeline(history_transform, self._options.max_history_turns)
|
|
45
|
+
self._hooks = HookChain(hooks or AgentHooks(), self._options.hook_timeout_seconds)
|
|
46
|
+
self._session_store = session_store or MemorySessionStore()
|
|
47
|
+
self._own_engine = own_engine
|
|
48
|
+
self._own_session_store = session_store is None or own_session_store
|
|
49
|
+
self._lifecycle = AgentLifecycle()
|
|
50
|
+
|
|
51
|
+
async def __aenter__(self) -> Self:
|
|
52
|
+
await self._lifecycle.ensure_open()
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(self, *_: object) -> None:
|
|
56
|
+
await self.aclose()
|
|
57
|
+
|
|
58
|
+
async def run(
|
|
59
|
+
self,
|
|
60
|
+
text: str,
|
|
61
|
+
*,
|
|
62
|
+
session_id: str | None = None,
|
|
63
|
+
tools: Iterable[ToolFunction | ToolBinding] = (),
|
|
64
|
+
response_model: type[BaseModel] | None = None,
|
|
65
|
+
instructions: str | None = None,
|
|
66
|
+
) -> AgentResult:
|
|
67
|
+
async with self._lifecycle.run():
|
|
68
|
+
if not text:
|
|
69
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "text no puede estar vacío")
|
|
70
|
+
|
|
71
|
+
tool_set = ToolSet.build((*self._static_tools, *tuple(tools)))
|
|
72
|
+
run_instructions = self._instructions if instructions is None else instructions
|
|
73
|
+
if session_id is None:
|
|
74
|
+
state = RunState(
|
|
75
|
+
history=[Turn.user(text)],
|
|
76
|
+
record=None,
|
|
77
|
+
lease_keeper=None,
|
|
78
|
+
instructions=run_instructions,
|
|
79
|
+
)
|
|
80
|
+
return await self._run_loop(state, tool_set, response_model, None)
|
|
81
|
+
|
|
82
|
+
timeout = self._options.session_lock_timeout_seconds
|
|
83
|
+
async with self._session_store.acquire(session_id, timeout) as lease:
|
|
84
|
+
lease_keeper = LeaseKeeper(lease)
|
|
85
|
+
await lease_keeper.start()
|
|
86
|
+
try:
|
|
87
|
+
record = await lease_keeper.load()
|
|
88
|
+
if record is None:
|
|
89
|
+
record = SessionRecord.create(
|
|
90
|
+
session_id, self._engine.provider, self._engine.model
|
|
91
|
+
)
|
|
92
|
+
self._validate_record(record, session_id)
|
|
93
|
+
history = [*record.history, Turn.user(text)]
|
|
94
|
+
state = RunState(
|
|
95
|
+
history=history,
|
|
96
|
+
record=record,
|
|
97
|
+
lease_keeper=lease_keeper,
|
|
98
|
+
instructions=run_instructions,
|
|
99
|
+
)
|
|
100
|
+
return await self._run_loop(state, tool_set, response_model, session_id)
|
|
101
|
+
finally:
|
|
102
|
+
await lease_keeper.aclose()
|
|
103
|
+
|
|
104
|
+
async def reset_session(self, session_id: str) -> None:
|
|
105
|
+
async with self._lifecycle.run():
|
|
106
|
+
if not session_id:
|
|
107
|
+
raise AppError(CodigoError.CONFIG_INVALIDA, "session_id no puede estar vacío")
|
|
108
|
+
await self._session_store.reset(
|
|
109
|
+
session_id,
|
|
110
|
+
self._options.session_lock_timeout_seconds,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def aclose(self) -> None:
|
|
114
|
+
await self._lifecycle.close(
|
|
115
|
+
self._close_resources,
|
|
116
|
+
self._options.shutdown_timeout_seconds,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
async def _close_resources(self) -> None:
|
|
120
|
+
try:
|
|
121
|
+
if self._own_engine:
|
|
122
|
+
await self._engine.aclose()
|
|
123
|
+
finally:
|
|
124
|
+
if self._own_session_store:
|
|
125
|
+
await self._session_store.aclose()
|
|
126
|
+
|
|
127
|
+
async def _run_loop(
|
|
128
|
+
self,
|
|
129
|
+
state: RunState,
|
|
130
|
+
tool_set: ToolSet,
|
|
131
|
+
response_model: type[BaseModel] | None,
|
|
132
|
+
session_id: str | None,
|
|
133
|
+
) -> AgentResult:
|
|
134
|
+
executor = ToolExecutor(tool_set, self._options.tool_timeout_seconds)
|
|
135
|
+
for step in range(1, self._options.max_steps + 1):
|
|
136
|
+
engine_result = await self._generate(state, tool_set, response_model, session_id)
|
|
137
|
+
self._record_engine_result(state, engine_result)
|
|
138
|
+
|
|
139
|
+
if not engine_result.tool_calls:
|
|
140
|
+
return await self._finish(state, AgentExitReason.COMPLETED, step)
|
|
141
|
+
|
|
142
|
+
self._validate_tool_batch(tool_set, engine_result.tool_calls)
|
|
143
|
+
await self._start_tool_calls(state, engine_result.tool_calls)
|
|
144
|
+
outcomes = await self._execute_tool_calls(state, executor, engine_result.tool_calls)
|
|
145
|
+
await self._finish_tool_calls(state)
|
|
146
|
+
|
|
147
|
+
if state.consecutive_tool_failures >= self._options.max_consecutive_tool_failures:
|
|
148
|
+
return await self._finish(state, AgentExitReason.TOOL_FAILURE_LIMIT, step)
|
|
149
|
+
all_final = outcomes and all(
|
|
150
|
+
outcome.final_answer and not outcome.failed for outcome in outcomes
|
|
151
|
+
)
|
|
152
|
+
if all_final:
|
|
153
|
+
state.last_text = "\n\n".join(outcome.output for outcome in outcomes)
|
|
154
|
+
return await self._finish(state, AgentExitReason.FINAL_ANSWER, step)
|
|
155
|
+
|
|
156
|
+
return await self._finish(state, AgentExitReason.MAX_STEPS, self._options.max_steps)
|
|
157
|
+
|
|
158
|
+
async def _generate(
|
|
159
|
+
self,
|
|
160
|
+
state: RunState,
|
|
161
|
+
tool_set: ToolSet,
|
|
162
|
+
response_model: type[BaseModel] | None,
|
|
163
|
+
session_id: str | None,
|
|
164
|
+
) -> EngineResult:
|
|
165
|
+
history = await self._history.prepare(tuple(state.history), session_id)
|
|
166
|
+
request = EngineRequest(
|
|
167
|
+
instructions=state.instructions,
|
|
168
|
+
history=history,
|
|
169
|
+
tools=tool_set.specs,
|
|
170
|
+
response_model=response_model,
|
|
171
|
+
)
|
|
172
|
+
request = await self._hooks.run_before_model(request)
|
|
173
|
+
try:
|
|
174
|
+
async with asyncio.timeout(self._options.provider_timeout_seconds):
|
|
175
|
+
result = await self._with_lease(
|
|
176
|
+
state,
|
|
177
|
+
lambda: self._engine.generate(request),
|
|
178
|
+
)
|
|
179
|
+
except TimeoutError as error:
|
|
180
|
+
raise AppError(CodigoError.PROVIDER_TIMEOUT, "provider agotó el timeout") from error
|
|
181
|
+
result = await self._hooks.run_after_model(result)
|
|
182
|
+
if result.provider != self._engine.provider:
|
|
183
|
+
raise AppError(CodigoError.PROVIDER_RESPUESTA_INVALIDA, "provider efectivo inválido")
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
def _record_engine_result(self, state: RunState, result: EngineResult) -> None:
|
|
187
|
+
state.history.append(result.turn)
|
|
188
|
+
state.usage = state.usage + result.usage
|
|
189
|
+
state.raw_responses.append(result.raw)
|
|
190
|
+
state.last_text = result.turn.text or state.last_text
|
|
191
|
+
state.structured = result.structured or state.structured
|
|
192
|
+
state.effective_model = result.model
|
|
193
|
+
|
|
194
|
+
async def _start_tool_calls(
|
|
195
|
+
self,
|
|
196
|
+
state: RunState,
|
|
197
|
+
calls: tuple[ToolCall, ...],
|
|
198
|
+
) -> None:
|
|
199
|
+
if state.record is None:
|
|
200
|
+
return
|
|
201
|
+
call_ids = tuple(call.call_id for call in calls)
|
|
202
|
+
state.record = state.record.start_turn(uuid4().hex, call_ids, tuple(state.history))
|
|
203
|
+
await self._save_record(state)
|
|
204
|
+
|
|
205
|
+
async def _execute_tool_calls(
|
|
206
|
+
self,
|
|
207
|
+
state: RunState,
|
|
208
|
+
executor: ToolExecutor,
|
|
209
|
+
calls: tuple[ToolCall, ...],
|
|
210
|
+
) -> tuple[ToolOutcome, ...]:
|
|
211
|
+
outcomes: list[ToolOutcome] = []
|
|
212
|
+
for original_call in calls:
|
|
213
|
+
if state.consecutive_tool_failures >= self._options.max_consecutive_tool_failures:
|
|
214
|
+
outcome = ToolOutcome.skipped(original_call)
|
|
215
|
+
await self._checkpoint_outcome(state, outcome)
|
|
216
|
+
outcomes.append(outcome)
|
|
217
|
+
continue
|
|
218
|
+
call = await self._hooks.run_before_tool(original_call)
|
|
219
|
+
if call.call_id != original_call.call_id or call.name != original_call.name:
|
|
220
|
+
raise AppError(CodigoError.HOOK_FALLO, "before_tool cambió identidad de la call")
|
|
221
|
+
original_outcome = await self._with_lease(
|
|
222
|
+
state,
|
|
223
|
+
partial(executor.execute, call),
|
|
224
|
+
)
|
|
225
|
+
try:
|
|
226
|
+
outcome = await self._hooks.run_after_tool(original_outcome)
|
|
227
|
+
except AppError:
|
|
228
|
+
await self._checkpoint_outcome(state, original_outcome)
|
|
229
|
+
raise
|
|
230
|
+
if outcome.call != call:
|
|
231
|
+
await self._checkpoint_outcome(state, original_outcome)
|
|
232
|
+
raise AppError(CodigoError.HOOK_FALLO, "after_tool cambió identidad de la call")
|
|
233
|
+
await self._checkpoint_outcome(state, outcome)
|
|
234
|
+
outcomes.append(outcome)
|
|
235
|
+
state.tool_calls += 1
|
|
236
|
+
if outcome.failed:
|
|
237
|
+
state.consecutive_tool_failures += 1
|
|
238
|
+
else:
|
|
239
|
+
state.consecutive_tool_failures = 0
|
|
240
|
+
return tuple(outcomes)
|
|
241
|
+
|
|
242
|
+
async def _checkpoint_outcome(self, state: RunState, outcome: ToolOutcome) -> None:
|
|
243
|
+
turn = Turn(role="tool", parts=(outcome.as_history_part(),))
|
|
244
|
+
state.history.append(turn)
|
|
245
|
+
if state.record is None:
|
|
246
|
+
return
|
|
247
|
+
history = tuple(state.history)
|
|
248
|
+
state.record = state.record.complete_call(outcome.call.call_id, history)
|
|
249
|
+
await self._save_record(state)
|
|
250
|
+
|
|
251
|
+
async def _finish_tool_calls(self, state: RunState) -> None:
|
|
252
|
+
if state.record is None:
|
|
253
|
+
return
|
|
254
|
+
history = self._history.window(tuple(state.history))
|
|
255
|
+
state.history = list(history)
|
|
256
|
+
state.record = state.record.finish_turn(history)
|
|
257
|
+
await self._save_record(state)
|
|
258
|
+
|
|
259
|
+
async def _finish(
|
|
260
|
+
self,
|
|
261
|
+
state: RunState,
|
|
262
|
+
reason: AgentExitReason,
|
|
263
|
+
steps: int,
|
|
264
|
+
) -> AgentResult:
|
|
265
|
+
state.history = list(self._history.window(tuple(state.history)))
|
|
266
|
+
result = AgentResult(
|
|
267
|
+
text=state.last_text,
|
|
268
|
+
reason=reason,
|
|
269
|
+
usage=state.usage,
|
|
270
|
+
steps=steps,
|
|
271
|
+
tool_calls=state.tool_calls,
|
|
272
|
+
provider=self._engine.provider,
|
|
273
|
+
model=state.effective_model or self._engine.model,
|
|
274
|
+
history=tuple(state.history),
|
|
275
|
+
raw_responses=tuple(state.raw_responses),
|
|
276
|
+
structured=state.structured,
|
|
277
|
+
)
|
|
278
|
+
await self._hooks.run_turn_finished(result)
|
|
279
|
+
if state.record is not None:
|
|
280
|
+
state.record = state.record.finish_turn(tuple(state.history))
|
|
281
|
+
await self._save_record(state)
|
|
282
|
+
return result
|
|
283
|
+
|
|
284
|
+
def _validate_tool_batch(self, tool_set: ToolSet, calls: tuple[ToolCall, ...]) -> None:
|
|
285
|
+
final_calls = [call for call in calls if tool_set.is_final_answer(call.name)]
|
|
286
|
+
if final_calls and len(calls) > 1:
|
|
287
|
+
raise AppError(
|
|
288
|
+
CodigoError.PROVIDER_RESPUESTA_INVALIDA,
|
|
289
|
+
"provider mezcló una tool terminal con otras calls en el mismo paso",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def _save_record(self, state: RunState) -> None:
|
|
293
|
+
if state.record is None or state.lease_keeper is None:
|
|
294
|
+
return
|
|
295
|
+
state.record = await state.lease_keeper.save(state.record)
|
|
296
|
+
|
|
297
|
+
async def _with_lease[T](
|
|
298
|
+
self,
|
|
299
|
+
state: RunState,
|
|
300
|
+
operation: Callable[[], Awaitable[T]],
|
|
301
|
+
) -> T:
|
|
302
|
+
if state.lease_keeper is None:
|
|
303
|
+
return await operation()
|
|
304
|
+
return await state.lease_keeper.run(operation)
|
|
305
|
+
|
|
306
|
+
def _validate_record(self, record: SessionRecord, session_id: str) -> None:
|
|
307
|
+
if record.session_id != session_id:
|
|
308
|
+
raise AppError(
|
|
309
|
+
CodigoError.SESION_INVALIDA,
|
|
310
|
+
"el store devolvió una sesión con identidad distinta",
|
|
311
|
+
)
|
|
312
|
+
if record.active_turn is not None:
|
|
313
|
+
raise AppError(
|
|
314
|
+
CodigoError.SESION_RECUPERACION_REQUERIDA,
|
|
315
|
+
f"turno incompleto en sesión {record.session_id}",
|
|
316
|
+
)
|
|
317
|
+
if record.provider != self._engine.provider or record.model != self._engine.model:
|
|
318
|
+
raise AppError(
|
|
319
|
+
CodigoError.SESION_ENGINE_DISTINTO,
|
|
320
|
+
f"sesión ligada a {record.provider}/{record.model}",
|
|
321
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
from typing import Protocol, cast, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, JsonValue, field_serializer, field_validator
|
|
7
|
+
|
|
8
|
+
from cortex_agent_sdk.history.models import ToolCallPart, Turn
|
|
9
|
+
from cortex_agent_sdk.immutable import FrozenJsonValue, freeze_json, thaw_json
|
|
10
|
+
from cortex_agent_sdk.tools.models import ToolSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ToolCall(BaseModel):
|
|
14
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
15
|
+
|
|
16
|
+
call_id: str
|
|
17
|
+
name: str
|
|
18
|
+
arguments: Mapping[str, JsonValue]
|
|
19
|
+
|
|
20
|
+
@field_validator("arguments", mode="after")
|
|
21
|
+
@classmethod
|
|
22
|
+
def freeze_arguments(
|
|
23
|
+
cls,
|
|
24
|
+
value: Mapping[str, JsonValue],
|
|
25
|
+
) -> Mapping[str, JsonValue]:
|
|
26
|
+
frozen = freeze_json(value)
|
|
27
|
+
if not isinstance(frozen, Mapping):
|
|
28
|
+
raise TypeError("arguments debe ser un objeto")
|
|
29
|
+
return cast(Mapping[str, JsonValue], frozen)
|
|
30
|
+
|
|
31
|
+
@field_serializer("arguments", when_used="json")
|
|
32
|
+
def serialize_arguments(
|
|
33
|
+
self,
|
|
34
|
+
value: Mapping[str, JsonValue],
|
|
35
|
+
) -> dict[str, object]:
|
|
36
|
+
return {key: thaw_json(cast(FrozenJsonValue, item)) for key, item in value.items()}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Usage:
|
|
41
|
+
input_tokens: int = 0
|
|
42
|
+
output_tokens: int = 0
|
|
43
|
+
reasoning_tokens: int = 0
|
|
44
|
+
cached_tokens: int = 0
|
|
45
|
+
cache_write_tokens: int = 0
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if (
|
|
49
|
+
min(
|
|
50
|
+
self.input_tokens,
|
|
51
|
+
self.output_tokens,
|
|
52
|
+
self.reasoning_tokens,
|
|
53
|
+
self.cached_tokens,
|
|
54
|
+
self.cache_write_tokens,
|
|
55
|
+
)
|
|
56
|
+
< 0
|
|
57
|
+
):
|
|
58
|
+
raise ValueError("usage no admite valores negativos")
|
|
59
|
+
|
|
60
|
+
def __add__(self, other: "Usage") -> "Usage":
|
|
61
|
+
return Usage(
|
|
62
|
+
input_tokens=self.input_tokens + other.input_tokens,
|
|
63
|
+
output_tokens=self.output_tokens + other.output_tokens,
|
|
64
|
+
reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
|
|
65
|
+
cached_tokens=self.cached_tokens + other.cached_tokens,
|
|
66
|
+
cache_write_tokens=self.cache_write_tokens + other.cache_write_tokens,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True, slots=True)
|
|
71
|
+
class EngineRequest:
|
|
72
|
+
instructions: str
|
|
73
|
+
history: tuple[Turn, ...]
|
|
74
|
+
tools: tuple[ToolSpec, ...]
|
|
75
|
+
response_model: type[BaseModel] | None = None
|
|
76
|
+
metadata: Mapping[str, str] | None = None
|
|
77
|
+
|
|
78
|
+
def __post_init__(self) -> None:
|
|
79
|
+
valid_history = isinstance(self.history, tuple) and all(
|
|
80
|
+
isinstance(turn, Turn) for turn in self.history
|
|
81
|
+
)
|
|
82
|
+
valid_tools = isinstance(self.tools, tuple) and all(
|
|
83
|
+
isinstance(spec, ToolSpec) for spec in self.tools
|
|
84
|
+
)
|
|
85
|
+
valid_model = self.response_model is None or (
|
|
86
|
+
isinstance(self.response_model, type) and issubclass(self.response_model, BaseModel)
|
|
87
|
+
)
|
|
88
|
+
if (
|
|
89
|
+
not isinstance(self.instructions, str)
|
|
90
|
+
or not valid_history
|
|
91
|
+
or not valid_tools
|
|
92
|
+
or not valid_model
|
|
93
|
+
):
|
|
94
|
+
raise TypeError("EngineRequest inválido")
|
|
95
|
+
if self.metadata is not None:
|
|
96
|
+
if not all(
|
|
97
|
+
isinstance(key, str) and isinstance(value, str)
|
|
98
|
+
for key, value in self.metadata.items()
|
|
99
|
+
):
|
|
100
|
+
raise TypeError("metadata inválida")
|
|
101
|
+
object.__setattr__(self, "metadata", MappingProxyType(dict(self.metadata)))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True, slots=True)
|
|
105
|
+
class EngineResult:
|
|
106
|
+
turn: Turn
|
|
107
|
+
tool_calls: tuple[ToolCall, ...]
|
|
108
|
+
usage: Usage
|
|
109
|
+
provider: str
|
|
110
|
+
model: str
|
|
111
|
+
stop_reason: str | None
|
|
112
|
+
raw: object
|
|
113
|
+
structured: BaseModel | None = None
|
|
114
|
+
|
|
115
|
+
def __post_init__(self) -> None:
|
|
116
|
+
valid_calls = isinstance(self.tool_calls, tuple) and all(
|
|
117
|
+
isinstance(call, ToolCall) for call in self.tool_calls
|
|
118
|
+
)
|
|
119
|
+
valid_structured = self.structured is None or isinstance(self.structured, BaseModel)
|
|
120
|
+
if not isinstance(self.turn, Turn) or not valid_calls:
|
|
121
|
+
raise TypeError("EngineResult inválido")
|
|
122
|
+
turn_calls = tuple(part for part in self.turn.parts if isinstance(part, ToolCallPart))
|
|
123
|
+
aligned_calls = len(turn_calls) == len(self.tool_calls) and all(
|
|
124
|
+
part.call_id == call.call_id
|
|
125
|
+
and part.name == call.name
|
|
126
|
+
and part.arguments == call.arguments
|
|
127
|
+
for part, call in zip(turn_calls, self.tool_calls, strict=True)
|
|
128
|
+
)
|
|
129
|
+
if not aligned_calls:
|
|
130
|
+
raise TypeError("tool_calls no coincide con el turno")
|
|
131
|
+
if not isinstance(self.usage, Usage) or not self.provider or not self.model:
|
|
132
|
+
raise TypeError("EngineResult inválido")
|
|
133
|
+
if self.stop_reason is not None and not isinstance(self.stop_reason, str):
|
|
134
|
+
raise TypeError("stop_reason inválido")
|
|
135
|
+
if not valid_structured:
|
|
136
|
+
raise TypeError("structured inválido")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@runtime_checkable
|
|
140
|
+
class ModelEngine(Protocol):
|
|
141
|
+
@property
|
|
142
|
+
def provider(self) -> str: ...
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def model(self) -> str: ...
|
|
146
|
+
|
|
147
|
+
async def generate(self, request: EngineRequest) -> EngineResult: ...
|
|
148
|
+
|
|
149
|
+
async def aclose(self) -> None: ...
|
|
150
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Severidad(StrEnum):
|
|
5
|
+
INFO = "INFO"
|
|
6
|
+
WARNING = "WARNING"
|
|
7
|
+
ERROR = "ERROR"
|
|
8
|
+
CRITICAL = "CRITICAL"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodigoError(StrEnum):
|
|
12
|
+
UNKNOWN = "UNKNOWN"
|
|
13
|
+
CONFIG_INVALIDA = "CONFIG_INVALIDA"
|
|
14
|
+
RECURSO_CERRADO = "RECURSO_CERRADO"
|
|
15
|
+
RUNTIME_CIERRE_TIMEOUT = "RUNTIME_CIERRE_TIMEOUT"
|
|
16
|
+
PROVIDER_FALLO = "PROVIDER_FALLO"
|
|
17
|
+
PROVIDER_TIMEOUT = "PROVIDER_TIMEOUT"
|
|
18
|
+
PROVIDER_RESPUESTA_INVALIDA = "PROVIDER_RESPUESTA_INVALIDA"
|
|
19
|
+
TOOL_NO_ENCONTRADA = "TOOL_NO_ENCONTRADA"
|
|
20
|
+
TOOL_ARGUMENTOS_INVALIDOS = "TOOL_ARGUMENTOS_INVALIDOS"
|
|
21
|
+
TOOL_TIMEOUT = "TOOL_TIMEOUT"
|
|
22
|
+
TOOL_FALLO = "TOOL_FALLO"
|
|
23
|
+
TOOL_OMITIDA_POR_LIMITE = "TOOL_OMITIDA_POR_LIMITE"
|
|
24
|
+
TOOL_RESULTADO_INVALIDO = "TOOL_RESULTADO_INVALIDO"
|
|
25
|
+
FINAL_ANSWER_INVALIDA = "FINAL_ANSWER_INVALIDA"
|
|
26
|
+
HOOK_FALLO = "HOOK_FALLO"
|
|
27
|
+
HISTORIAL_TRANSFORM_INVALIDO = "HISTORIAL_TRANSFORM_INVALIDO"
|
|
28
|
+
SESION_OCUPADA = "SESION_OCUPADA"
|
|
29
|
+
SESION_LEASE_PERDIDO = "SESION_LEASE_PERDIDO"
|
|
30
|
+
SESION_CONFLICTO = "SESION_CONFLICTO"
|
|
31
|
+
SESION_ENGINE_DISTINTO = "SESION_ENGINE_DISTINTO"
|
|
32
|
+
SESION_RECUPERACION_REQUERIDA = "SESION_RECUPERACION_REQUERIDA"
|
|
33
|
+
SESION_INVALIDA = "SESION_INVALIDA"
|
|
34
|
+
SESION_STORE_FALLO = "SESION_STORE_FALLO"
|
|
35
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from cortex_agent_sdk.errores.catalogo import CodigoError, Severidad
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AppError(Exception):
|
|
5
|
+
"""Error del SDK con código estable y mensaje seguro opcional."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
codigo: CodigoError,
|
|
10
|
+
detalle: str,
|
|
11
|
+
severidad: Severidad = Severidad.ERROR,
|
|
12
|
+
mensaje_seguro: str | None = None,
|
|
13
|
+
) -> None:
|
|
14
|
+
self.codigo = codigo
|
|
15
|
+
self.detalle = detalle
|
|
16
|
+
self.severidad = severidad
|
|
17
|
+
self.mensaje_seguro = mensaje_seguro
|
|
18
|
+
super().__init__(f"[{codigo.value}] {detalle}")
|
|
19
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, SecretStr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TalosGateway(BaseModel):
|
|
5
|
+
"""Conexión interna a Talos para un engine nativo."""
|
|
6
|
+
|
|
7
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
8
|
+
|
|
9
|
+
url: AnyHttpUrl
|
|
10
|
+
project_token: SecretStr = Field(min_length=32)
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
url: str | AnyHttpUrl,
|
|
16
|
+
project_token: str | SecretStr,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(url=url, project_token=project_token)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def openai_base_url(self) -> str:
|
|
22
|
+
return f"{str(self.url).rstrip('/')}/v1"
|
|
23
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Espacio reservado para el engine de Google posterior a 0.0.1."""
|