pi-python-core 0.10.1__py3-none-any.whl → 0.11.0__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.
- pi_python/__init__.py +2 -0
- pi_python/_version.py +1 -1
- pi_python/agent.py +88 -4
- pi_python/events.py +3 -4
- pi_python/features.py +2 -0
- pi_python/loop.py +44 -6
- pi_python/messages.py +57 -0
- pi_python/provider.py +18 -0
- pi_python/providers/openai.py +23 -11
- pi_python/run.py +91 -11
- pi_python/tools.py +39 -2
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.11.0.dist-info}/METADATA +2 -2
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.11.0.dist-info}/RECORD +16 -16
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.11.0.dist-info}/WHEEL +0 -0
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {pi_python_core-0.10.1.dist-info → pi_python_core-0.11.0.dist-info}/licenses/NOTICE +0 -0
pi_python/__init__.py
CHANGED
|
@@ -64,6 +64,7 @@ from .recovery import (
|
|
|
64
64
|
)
|
|
65
65
|
from .tools import (
|
|
66
66
|
Tool,
|
|
67
|
+
ToolInvocation,
|
|
67
68
|
ToolContext,
|
|
68
69
|
ToolExecutor,
|
|
69
70
|
ToolResult,
|
|
@@ -151,6 +152,7 @@ __all__ = [
|
|
|
151
152
|
"is_recoverable_length",
|
|
152
153
|
"is_retryable_error",
|
|
153
154
|
"retry_delay",
|
|
155
|
+
"ToolInvocation",
|
|
154
156
|
"ToolContext",
|
|
155
157
|
"ToolExecutor",
|
|
156
158
|
"ToolResult",
|
pi_python/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.11.0"
|
pi_python/agent.py
CHANGED
|
@@ -9,6 +9,7 @@ import asyncio
|
|
|
9
9
|
from copy import copy, deepcopy
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from typing import Any, Callable
|
|
12
|
+
from uuid import uuid4
|
|
12
13
|
|
|
13
14
|
from .cancellation import CancelToken
|
|
14
15
|
from .errors import (
|
|
@@ -29,6 +30,7 @@ from .messages import (
|
|
|
29
30
|
TextContent,
|
|
30
31
|
Message,
|
|
31
32
|
SystemMessage,
|
|
33
|
+
ToolCall,
|
|
32
34
|
UserMessage,
|
|
33
35
|
validate_history,
|
|
34
36
|
validate_json,
|
|
@@ -37,7 +39,7 @@ from .provider import Provider, DefaultProvider, FunctionProvider, has_default_s
|
|
|
37
39
|
from .queues import MessageQueues
|
|
38
40
|
from .run import CALLER_CANCELLED, Run, RunResult, merge
|
|
39
41
|
from .sync import run_sync
|
|
40
|
-
from .tools import Tool
|
|
42
|
+
from .tools import Tool, ToolInvocation
|
|
41
43
|
from .transcript import current_system_message
|
|
42
44
|
|
|
43
45
|
|
|
@@ -67,6 +69,8 @@ class Agent:
|
|
|
67
69
|
thinking_budgets: dict[str, int] | None = None,
|
|
68
70
|
transport: str | None = None,
|
|
69
71
|
session_id: str | None = None,
|
|
72
|
+
auto_session_id: bool = True,
|
|
73
|
+
cache_session_id: str | None = None,
|
|
70
74
|
get_api_key: Callable | None = None,
|
|
71
75
|
on_payload: Callable | None = None,
|
|
72
76
|
on_response: Callable | None = None,
|
|
@@ -78,9 +82,28 @@ class Agent:
|
|
|
78
82
|
execution_mode: str = "parallel",
|
|
79
83
|
steering_mode: str = "one_at_a_time",
|
|
80
84
|
follow_up_mode: str = "one_at_a_time",
|
|
85
|
+
include_tool_origin: bool = False,
|
|
81
86
|
):
|
|
82
87
|
if execution_mode not in {"parallel", "sequential"}:
|
|
83
88
|
raise ConfigurationError("Invalid execution mode")
|
|
89
|
+
if type(include_tool_origin) is not bool:
|
|
90
|
+
raise ConfigurationError("include_tool_origin must be a boolean")
|
|
91
|
+
self.include_tool_origin = include_tool_origin
|
|
92
|
+
if type(auto_session_id) is not bool:
|
|
93
|
+
raise ConfigurationError("auto_session_id must be a boolean")
|
|
94
|
+
if cache_session_id is not None:
|
|
95
|
+
if (
|
|
96
|
+
not isinstance(cache_session_id, str)
|
|
97
|
+
or not 1 <= len(cache_session_id) <= 64
|
|
98
|
+
or any(not "!" <= char <= "~" for char in cache_session_id)
|
|
99
|
+
):
|
|
100
|
+
raise ConfigurationError(
|
|
101
|
+
"cache_session_id must contain 1–64 printable ASCII characters without spaces"
|
|
102
|
+
)
|
|
103
|
+
if not auto_session_id:
|
|
104
|
+
raise ConfigurationError("cache_session_id requires auto_session_id=True")
|
|
105
|
+
self._auto_session_id = auto_session_id
|
|
106
|
+
self._cache_session_id = cache_session_id
|
|
84
107
|
self._queues = MessageQueues(steering_mode, follow_up_mode)
|
|
85
108
|
if provider is not None and stream_fn is not None:
|
|
86
109
|
raise ConfigurationError("Pass provider or stream_fn, not both")
|
|
@@ -142,6 +165,23 @@ class Agent:
|
|
|
142
165
|
self._settled = asyncio.Event()
|
|
143
166
|
self._settled.set()
|
|
144
167
|
|
|
168
|
+
@property
|
|
169
|
+
def cache_session_id(self) -> str | None:
|
|
170
|
+
"""Agent-managed Codex cache fallback, independent of explicit session_id.
|
|
171
|
+
|
|
172
|
+
Save this alongside history and restore it with cache_session_id=... .
|
|
173
|
+
Reading it does not enable connection reuse. Explicit session_id takes
|
|
174
|
+
precedence on requests; cache_retention='none' suppresses both on the wire.
|
|
175
|
+
"""
|
|
176
|
+
if (
|
|
177
|
+
not self._auto_session_id
|
|
178
|
+
or getattr(self.provider, "supports_agent_cache_session", False) is not True
|
|
179
|
+
):
|
|
180
|
+
return None
|
|
181
|
+
if self._cache_session_id is None:
|
|
182
|
+
self._cache_session_id = str(uuid4())
|
|
183
|
+
return self._cache_session_id
|
|
184
|
+
|
|
145
185
|
@property
|
|
146
186
|
def steering_mode(self) -> str:
|
|
147
187
|
return self._queues.steering_mode
|
|
@@ -204,6 +244,10 @@ class Agent:
|
|
|
204
244
|
if self._running:
|
|
205
245
|
self._updates.append(update)
|
|
206
246
|
else:
|
|
247
|
+
# Preserve submission order after an application batch deferred updates.
|
|
248
|
+
for pending in self._updates:
|
|
249
|
+
merge(self._defaults, pending)
|
|
250
|
+
self._updates.clear()
|
|
207
251
|
merge(self._defaults, update)
|
|
208
252
|
|
|
209
253
|
@staticmethod
|
|
@@ -255,6 +299,7 @@ class Agent:
|
|
|
255
299
|
raise AgentBusyError("Cannot reset while running")
|
|
256
300
|
baseline = current_system_message(self._messages)
|
|
257
301
|
self._messages = [baseline] if baseline else []
|
|
302
|
+
self._cache_session_id = None
|
|
258
303
|
self._last_error = None
|
|
259
304
|
self._run = None
|
|
260
305
|
self.clear_all_queues()
|
|
@@ -332,6 +377,31 @@ class Agent:
|
|
|
332
377
|
raise AgentBusyError("Agent already running")
|
|
333
378
|
return await self._start(self._input(message))
|
|
334
379
|
|
|
380
|
+
async def execute_tools(
|
|
381
|
+
self, invocations: list[ToolInvocation], *, source: str | None = None
|
|
382
|
+
) -> RunResult:
|
|
383
|
+
"""Execute a nonempty batch without a model request or consuming queued input.
|
|
384
|
+
|
|
385
|
+
Shares the model tool path, limits and cancellation ownership. Results are
|
|
386
|
+
recorded as application feedback, never as assistant/tool-result pairs.
|
|
387
|
+
``source`` is caller-supplied diagnostic metadata, not an identity claim.
|
|
388
|
+
"""
|
|
389
|
+
self._usable()
|
|
390
|
+
if self._running:
|
|
391
|
+
raise AgentBusyError("Agent already running")
|
|
392
|
+
validate_history(self._messages)
|
|
393
|
+
if not isinstance(invocations, list) or not invocations:
|
|
394
|
+
raise ConfigurationError("execute_tools requires a nonempty list of ToolInvocation")
|
|
395
|
+
if source is not None and not isinstance(source, str):
|
|
396
|
+
raise ConfigurationError("source must be a string or None")
|
|
397
|
+
calls = []
|
|
398
|
+
for invocation in invocations:
|
|
399
|
+
if not isinstance(invocation, ToolInvocation):
|
|
400
|
+
raise ConfigurationError("Expected ToolInvocation")
|
|
401
|
+
invocation.__post_init__()
|
|
402
|
+
calls.append(ToolCall(str(uuid4()), invocation.name, deepcopy(invocation.arguments)))
|
|
403
|
+
return await self._start([], application_calls=calls, source=source)
|
|
404
|
+
|
|
335
405
|
def prompt_sync(
|
|
336
406
|
self, message: str | Message | list[Message], images: list[ImageContent] | None = None
|
|
337
407
|
) -> RunResult:
|
|
@@ -369,18 +439,32 @@ class Agent:
|
|
|
369
439
|
return await self._start([])
|
|
370
440
|
|
|
371
441
|
async def _start(
|
|
372
|
-
self,
|
|
442
|
+
self,
|
|
443
|
+
pending: list[Message],
|
|
444
|
+
skip_initial_steering: bool = False,
|
|
445
|
+
*,
|
|
446
|
+
application_calls: list[ToolCall] | None = None,
|
|
447
|
+
source: str | None = None,
|
|
373
448
|
) -> RunResult:
|
|
374
|
-
if
|
|
449
|
+
if (
|
|
450
|
+
application_calls is None
|
|
451
|
+
and isinstance(self.provider, DefaultProvider)
|
|
452
|
+
and not has_default_stream()
|
|
453
|
+
):
|
|
375
454
|
raise ConfigurationError(
|
|
376
455
|
"No model provider: pass Agent(provider=...) or call set_default_stream_fn(...)"
|
|
377
456
|
)
|
|
457
|
+
for update in self._updates:
|
|
458
|
+
merge(self._defaults, update)
|
|
459
|
+
self._updates.clear()
|
|
378
460
|
self._running = True
|
|
379
461
|
self._idle.clear()
|
|
380
462
|
self._settled.clear()
|
|
381
463
|
self._last_error = None
|
|
382
464
|
self._events.failed = False
|
|
383
|
-
run = self._run = Run(
|
|
465
|
+
run = self._run = Run(
|
|
466
|
+
self, skip_initial_steering, application_calls=application_calls, source=source
|
|
467
|
+
)
|
|
384
468
|
run.loop = asyncio.get_running_loop()
|
|
385
469
|
# Driver starts with a checkpoint so cancellation before scheduling still finalizes.
|
|
386
470
|
run.driver = asyncio.create_task(self._drive(run, pending))
|
pi_python/events.py
CHANGED
|
@@ -16,7 +16,7 @@ from .messages import validate_json
|
|
|
16
16
|
class Event:
|
|
17
17
|
type: str
|
|
18
18
|
run_id: str
|
|
19
|
-
turn_id: int
|
|
19
|
+
turn_id: int | None
|
|
20
20
|
sequence: int
|
|
21
21
|
call_id: str | None = None
|
|
22
22
|
data: dict[str, Any] = field(default_factory=dict)
|
|
@@ -50,8 +50,7 @@ def decode_event(value: str) -> Event:
|
|
|
50
50
|
if (
|
|
51
51
|
type(e.sequence) is not int
|
|
52
52
|
or e.sequence < 1
|
|
53
|
-
or type(e.turn_id) is not int
|
|
54
|
-
or e.turn_id < 0
|
|
53
|
+
or (e.turn_id is not None and (type(e.turn_id) is not int or e.turn_id < 0))
|
|
55
54
|
):
|
|
56
55
|
raise MessageValidationError("Invalid event sequence or turn")
|
|
57
56
|
if e.call_id is not None and not isinstance(e.call_id, str):
|
|
@@ -85,7 +84,7 @@ class EventDispatcher:
|
|
|
85
84
|
self,
|
|
86
85
|
kind: str,
|
|
87
86
|
run_id: str,
|
|
88
|
-
turn_id: int,
|
|
87
|
+
turn_id: int | None,
|
|
89
88
|
data: dict[str, Any],
|
|
90
89
|
call_id: str | None,
|
|
91
90
|
await_owned: Callable,
|
pi_python/features.py
CHANGED
pi_python/loop.py
CHANGED
|
@@ -19,6 +19,7 @@ from .messages import (
|
|
|
19
19
|
message_from_dict,
|
|
20
20
|
message_to_dict,
|
|
21
21
|
validate_history,
|
|
22
|
+
convert_application_tool_batch,
|
|
22
23
|
)
|
|
23
24
|
from .models import ModelInfo
|
|
24
25
|
from .provider import ModelRequest
|
|
@@ -82,6 +83,12 @@ async def stream_response(run: Run) -> tuple[AssistantMessage, bool]:
|
|
|
82
83
|
messages = await run.hook("transform_context", messages, run.token)
|
|
83
84
|
if run.hooks.convert_to_llm:
|
|
84
85
|
messages = await run.hook("convert_to_llm", deepcopy(messages))
|
|
86
|
+
messages = [
|
|
87
|
+
convert_application_tool_batch(m)
|
|
88
|
+
if isinstance(m, CustomMessage) and m.custom_type == "application_tool_batch"
|
|
89
|
+
else m
|
|
90
|
+
for m in messages
|
|
91
|
+
]
|
|
85
92
|
if any(isinstance(m, CustomMessage) for m in messages):
|
|
86
93
|
raise UnsupportedCapabilityError("Custom messages require convert_to_llm")
|
|
87
94
|
validate_history(messages)
|
|
@@ -98,6 +105,14 @@ async def stream_response(run: Run) -> tuple[AssistantMessage, bool]:
|
|
|
98
105
|
deepcopy(context.options),
|
|
99
106
|
model_info=deepcopy(model) if isinstance(model, ModelInfo) else None,
|
|
100
107
|
)
|
|
108
|
+
if (
|
|
109
|
+
getattr(run.provider, "supports_agent_cache_session", False) is True
|
|
110
|
+
and not request.options.get("session_id")
|
|
111
|
+
and request.options.get("cache_retention") != "none"
|
|
112
|
+
):
|
|
113
|
+
cache_session_id = run.agent.cache_session_id
|
|
114
|
+
if cache_session_id is not None:
|
|
115
|
+
request._set_cache_session_id(cache_session_id)
|
|
101
116
|
if run.hooks.get_api_key:
|
|
102
117
|
request.api_key = await run.hook(
|
|
103
118
|
"get_api_key", getattr(run.provider, "name", request.model)
|
|
@@ -212,6 +227,8 @@ async def execute_batch(run: Run, outcomes: list[ToolOutcome]) -> None:
|
|
|
212
227
|
agent_context=snapshot["context"],
|
|
213
228
|
_on_reconciliation_required=lambda: run.require_reconciliation(),
|
|
214
229
|
)
|
|
230
|
+
if run.application_batch:
|
|
231
|
+
contexts[id(outcome)]._set_origin(outcome.origin, outcome.source)
|
|
215
232
|
return contexts[id(outcome)]
|
|
216
233
|
|
|
217
234
|
def abort_before_start(outcome: ToolOutcome) -> bool:
|
|
@@ -300,6 +317,32 @@ async def execute_batch(run: Run, outcomes: list[ToolOutcome]) -> None:
|
|
|
300
317
|
await run.commit_outcome(outcome)
|
|
301
318
|
|
|
302
319
|
|
|
320
|
+
def exceeds_tool_budget(run: Run, outcomes: list[ToolOutcome], used: int = 0) -> bool:
|
|
321
|
+
maximum = run.limits.max_tool_calls
|
|
322
|
+
if maximum is None or used + len(outcomes) <= maximum:
|
|
323
|
+
return False
|
|
324
|
+
for outcome in outcomes:
|
|
325
|
+
outcome.result = error_result("limit", "Entire tool batch exceeds remaining budget")
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
async def run_application_batch(run: Run) -> tuple[str, str]:
|
|
330
|
+
run.checkpoint()
|
|
331
|
+
await run.emit("agent_start")
|
|
332
|
+
if exceeds_tool_budget(run, run.active_batch):
|
|
333
|
+
return "limit_reached", "max_tool_calls"
|
|
334
|
+
await execute_batch(run, run.active_batch)
|
|
335
|
+
if run.agent._unknown:
|
|
336
|
+
return "failed", "outcome_unknown"
|
|
337
|
+
if run.stuck():
|
|
338
|
+
run.fail("Tool did not stop after cancellation")
|
|
339
|
+
return "cancelled" if run.token.cancelled else "failed", "tool_not_stopped"
|
|
340
|
+
if run.token.cancelled:
|
|
341
|
+
status, reason, _ = run.cancelled_outcome()
|
|
342
|
+
return status, reason
|
|
343
|
+
return "completed", "stop"
|
|
344
|
+
|
|
345
|
+
|
|
303
346
|
async def run_loop(run: Run, initial: list[Message]) -> tuple[str, str]:
|
|
304
347
|
context = run.context
|
|
305
348
|
assert context is not None
|
|
@@ -366,13 +409,8 @@ async def run_loop(run: Run, initial: list[Message]) -> tuple[str, str]:
|
|
|
366
409
|
else:
|
|
367
410
|
status, reason = "cancelled", run.token.reason or "aborted"
|
|
368
411
|
elif outcomes:
|
|
369
|
-
|
|
370
|
-
if max_calls is not None and tool_count + len(outcomes) > max_calls:
|
|
412
|
+
if exceeds_tool_budget(run, outcomes, tool_count):
|
|
371
413
|
status, reason = "limit_reached", "max_tool_calls"
|
|
372
|
-
for outcome in outcomes:
|
|
373
|
-
outcome.result = error_result(
|
|
374
|
-
"limit", "Entire tool batch exceeds remaining budget"
|
|
375
|
-
)
|
|
376
414
|
elif message.stop_reason == "length":
|
|
377
415
|
for outcome in outcomes:
|
|
378
416
|
await run.emit(
|
pi_python/messages.py
CHANGED
|
@@ -361,6 +361,63 @@ def message_from_dict(data: dict[str, Any]) -> Message:
|
|
|
361
361
|
raise MessageValidationError(str(exc)) from exc
|
|
362
362
|
|
|
363
363
|
|
|
364
|
+
def convert_application_tool_batch(message: CustomMessage) -> UserMessage:
|
|
365
|
+
"""Project built-in application feedback, never local details or raw results.
|
|
366
|
+
|
|
367
|
+
An allowlist also protects the default visibility boundary for imported records
|
|
368
|
+
with extra fields. Applications can override conversion with convert_to_llm.
|
|
369
|
+
"""
|
|
370
|
+
data = message.data
|
|
371
|
+
if not isinstance(data, dict) or not isinstance(data.get("calls"), list):
|
|
372
|
+
raise MessageValidationError("Invalid application tool batch")
|
|
373
|
+
calls = []
|
|
374
|
+
for call in data["calls"]:
|
|
375
|
+
if not isinstance(call, dict):
|
|
376
|
+
raise MessageValidationError("Invalid application tool call record")
|
|
377
|
+
keys = (
|
|
378
|
+
"id",
|
|
379
|
+
"name",
|
|
380
|
+
"arguments",
|
|
381
|
+
"prepared_arguments",
|
|
382
|
+
"origin",
|
|
383
|
+
"source",
|
|
384
|
+
"execution_status",
|
|
385
|
+
"is_error",
|
|
386
|
+
"error_code",
|
|
387
|
+
"content",
|
|
388
|
+
)
|
|
389
|
+
try:
|
|
390
|
+
visible = {key: call[key] for key in keys}
|
|
391
|
+
except KeyError as exc:
|
|
392
|
+
raise MessageValidationError("Incomplete application tool call record") from exc
|
|
393
|
+
validate_json(visible)
|
|
394
|
+
if (
|
|
395
|
+
visible["origin"] != "application"
|
|
396
|
+
or not isinstance(visible["id"], str)
|
|
397
|
+
or not visible["id"]
|
|
398
|
+
or not isinstance(visible["name"], str)
|
|
399
|
+
or not visible["name"]
|
|
400
|
+
or not isinstance(visible["arguments"], dict)
|
|
401
|
+
or (
|
|
402
|
+
visible["prepared_arguments"] is not None
|
|
403
|
+
and not isinstance(visible["prepared_arguments"], dict)
|
|
404
|
+
)
|
|
405
|
+
or (visible["source"] is not None and not isinstance(visible["source"], str))
|
|
406
|
+
or type(visible["is_error"]) is not bool
|
|
407
|
+
or visible["execution_status"]
|
|
408
|
+
not in ("not_started", "running", "succeeded", "failed", "cancelled", "unknown")
|
|
409
|
+
or (visible["error_code"] is not None and not isinstance(visible["error_code"], str))
|
|
410
|
+
):
|
|
411
|
+
raise MessageValidationError("Invalid application tool call record")
|
|
412
|
+
_blocks(visible["content"], images=True)
|
|
413
|
+
calls.append(visible)
|
|
414
|
+
return UserMessage(
|
|
415
|
+
"Application tool execution results (application-selected, not model tool calls):\n"
|
|
416
|
+
+ json.dumps({"calls": calls}, ensure_ascii=False, allow_nan=False),
|
|
417
|
+
timestamp=message.timestamp,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
364
421
|
def validate_history(messages: list[Message]) -> None:
|
|
365
422
|
pending: dict[str, str] = {}
|
|
366
423
|
for message in messages:
|
pi_python/provider.py
CHANGED
|
@@ -28,6 +28,20 @@ class ModelRequest:
|
|
|
28
28
|
on_response: Callable | None = field(default=None, repr=False)
|
|
29
29
|
on_provider_stream_event: Callable | None = field(default=None, repr=False)
|
|
30
30
|
|
|
31
|
+
def _set_cache_session_id(self, value: str) -> None:
|
|
32
|
+
# Per-request metadata, deliberately outside the public dataclass fields
|
|
33
|
+
# and options. It must never be interpreted as a transport session_id.
|
|
34
|
+
self._agent_cache_session_id = value
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def cache_session_id(self) -> str | None:
|
|
38
|
+
"""Optional Agent-owned cache affinity, never a transport session.
|
|
39
|
+
|
|
40
|
+
Providers opt in with supports_agent_cache_session=True. This property
|
|
41
|
+
is not a dataclass field and does not change serialized request options.
|
|
42
|
+
"""
|
|
43
|
+
return getattr(self, "_agent_cache_session_id", None)
|
|
44
|
+
|
|
31
45
|
|
|
32
46
|
@dataclass
|
|
33
47
|
class ModelEvent:
|
|
@@ -109,6 +123,10 @@ def has_default_stream() -> bool:
|
|
|
109
123
|
|
|
110
124
|
|
|
111
125
|
class DefaultProvider:
|
|
126
|
+
@property
|
|
127
|
+
def supports_agent_cache_session(self) -> bool:
|
|
128
|
+
return getattr(_default_stream, "supports_agent_cache_session", False) is True
|
|
129
|
+
|
|
112
130
|
@property
|
|
113
131
|
def name(self) -> str:
|
|
114
132
|
return getattr(_default_stream, "name", "custom")
|
pi_python/providers/openai.py
CHANGED
|
@@ -318,6 +318,14 @@ class OpenAIProvider(RemoteProvider):
|
|
|
318
318
|
position += 1
|
|
319
319
|
return items, transcript
|
|
320
320
|
|
|
321
|
+
def _cache_key(self, request: ModelRequest) -> str | None:
|
|
322
|
+
if request.options.get("cache_retention", "short") == "none":
|
|
323
|
+
return None
|
|
324
|
+
key = request.options.get("session_id")
|
|
325
|
+
if not key and getattr(self, "supports_agent_cache_session", False) is True:
|
|
326
|
+
key = request.cache_session_id
|
|
327
|
+
return key[:64] if isinstance(key, str) else key
|
|
328
|
+
|
|
321
329
|
def build_request(self, request: ModelRequest, chatgpt_sign_in: bool = False) -> dict[str, Any]:
|
|
322
330
|
model = self.model_info(request)
|
|
323
331
|
compat = model.compat
|
|
@@ -334,9 +342,7 @@ class OpenAIProvider(RemoteProvider):
|
|
|
334
342
|
retention = options.get("cache_retention", "short")
|
|
335
343
|
if retention not in {"none", "short", "long"}:
|
|
336
344
|
raise ConfigurationError("Invalid cache_retention")
|
|
337
|
-
key =
|
|
338
|
-
if isinstance(key, str) and len(key) > 64:
|
|
339
|
-
key = key[:64]
|
|
345
|
+
key = self._cache_key(request)
|
|
340
346
|
body: dict[str, Any] = {
|
|
341
347
|
"model": request.model,
|
|
342
348
|
"input": items,
|
|
@@ -436,13 +442,17 @@ class OpenAIProvider(RemoteProvider):
|
|
|
436
442
|
"OpenAI-Beta": "responses=experimental",
|
|
437
443
|
}
|
|
438
444
|
)
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
445
|
+
cache_key = self._cache_key(request)
|
|
446
|
+
if cache_key:
|
|
447
|
+
if request.options.get("session_id"):
|
|
448
|
+
# Preserve the existing precedence of explicit session_id.
|
|
449
|
+
headers["session-id"] = headers["x-client-request-id"] = cache_key
|
|
450
|
+
else:
|
|
451
|
+
# Automatic defaults must not overwrite application headers.
|
|
452
|
+
supplied = {name.lower() for name in headers}
|
|
453
|
+
for name in ("session-id", "x-client-request-id"):
|
|
454
|
+
if name not in supplied:
|
|
455
|
+
headers[name] = cache_key
|
|
446
456
|
if (
|
|
447
457
|
self.name == "openai"
|
|
448
458
|
and request.options.get("session_id")
|
|
@@ -458,7 +468,8 @@ class OpenAIProvider(RemoteProvider):
|
|
|
458
468
|
body = await self.payload(request, self.build_request(request, chatgpt))
|
|
459
469
|
if transport == "websocket-cached" and not request.options.get("session_id"):
|
|
460
470
|
raise ConfigurationError("websocket-cached requires session_id")
|
|
461
|
-
#
|
|
471
|
+
# Only an application-supplied session_id can opt into connection reuse
|
|
472
|
+
# and response continuation. The Agent's cache fallback never reaches here.
|
|
462
473
|
cached = (
|
|
463
474
|
request.options.get("session_id")
|
|
464
475
|
if request.options.get("cache_retention") != "none"
|
|
@@ -645,6 +656,7 @@ class OpenAIProvider(RemoteProvider):
|
|
|
645
656
|
class OpenAICodexProvider(OpenAIProvider):
|
|
646
657
|
name = "openai-codex"
|
|
647
658
|
api = "openai-codex-responses"
|
|
659
|
+
supports_agent_cache_session = True
|
|
648
660
|
|
|
649
661
|
def __init__(
|
|
650
662
|
self, *, base_url: str = "https://chatgpt.com/backend-api/codex", **kwargs: Any
|
pi_python/run.py
CHANGED
|
@@ -15,7 +15,9 @@ from .errors import ConfigurationError
|
|
|
15
15
|
from .hooks import AgentConfigUpdate, Hooks, RunContext, TurnUpdate
|
|
16
16
|
from .messages import (
|
|
17
17
|
AssistantMessage,
|
|
18
|
+
CustomMessage,
|
|
18
19
|
Message,
|
|
20
|
+
ToolCall,
|
|
19
21
|
ToolResultMessage,
|
|
20
22
|
message_to_dict,
|
|
21
23
|
validate_history,
|
|
@@ -23,7 +25,7 @@ from .messages import (
|
|
|
23
25
|
from .errors import SubscriptionError
|
|
24
26
|
from .tools import ToolOutcome, error_result, invoke
|
|
25
27
|
from .transcript import declare_tool_changes
|
|
26
|
-
from .loop import abort_error, failure_message, run_loop
|
|
28
|
+
from .loop import abort_error, failure_message, run_loop, run_application_batch
|
|
27
29
|
|
|
28
30
|
if TYPE_CHECKING:
|
|
29
31
|
from .agent import Agent
|
|
@@ -58,11 +60,24 @@ def merge(target: Any, update: AgentConfigUpdate) -> None:
|
|
|
58
60
|
class Run:
|
|
59
61
|
"""State that lives for one run. The Agent keeps everything that outlives it."""
|
|
60
62
|
|
|
61
|
-
def __init__(
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
agent: Agent,
|
|
66
|
+
skip_initial_steering: bool = False,
|
|
67
|
+
*,
|
|
68
|
+
application_calls: list[ToolCall] | None = None,
|
|
69
|
+
source: str | None = None,
|
|
70
|
+
):
|
|
62
71
|
self.agent = agent
|
|
63
72
|
self.token = CancelToken()
|
|
64
73
|
self.id = str(uuid4())
|
|
65
74
|
self.turn = 0
|
|
75
|
+
self.application_batch = application_calls is not None
|
|
76
|
+
self.origin = "application" if self.application_batch else "model"
|
|
77
|
+
self.source = source
|
|
78
|
+
self.include_tool_origin = agent.include_tool_origin
|
|
79
|
+
self._limits = agent.limits
|
|
80
|
+
self._execution_mode = agent.execution_mode
|
|
66
81
|
defaults = agent._defaults
|
|
67
82
|
self.context = RunContext(
|
|
68
83
|
deepcopy(agent._messages),
|
|
@@ -74,8 +89,16 @@ class Run:
|
|
|
74
89
|
self.skip_initial_steering = skip_initial_steering
|
|
75
90
|
self.partial: Any = None
|
|
76
91
|
self.started = False # message_start was published for the current response
|
|
77
|
-
self.outcomes: list[ToolOutcome] = [
|
|
78
|
-
|
|
92
|
+
self.outcomes: list[ToolOutcome] = [
|
|
93
|
+
ToolOutcome(
|
|
94
|
+
deepcopy(call),
|
|
95
|
+
original_arguments=deepcopy(call.arguments),
|
|
96
|
+
)
|
|
97
|
+
for call in application_calls or []
|
|
98
|
+
]
|
|
99
|
+
for outcome in self.outcomes:
|
|
100
|
+
outcome._set_origin("application", source)
|
|
101
|
+
self.active_batch: list[ToolOutcome] = list(self.outcomes)
|
|
79
102
|
self._committed: set[int] = set()
|
|
80
103
|
self._ended: set[int] = set()
|
|
81
104
|
self._owned: set[asyncio.Future] = set()
|
|
@@ -95,11 +118,11 @@ class Run:
|
|
|
95
118
|
|
|
96
119
|
@property
|
|
97
120
|
def limits(self) -> RunLimits:
|
|
98
|
-
return self.agent.limits
|
|
121
|
+
return self._limits if self.application_batch else self.agent.limits
|
|
99
122
|
|
|
100
123
|
@property
|
|
101
124
|
def execution_mode(self) -> str:
|
|
102
|
-
return self.agent.execution_mode
|
|
125
|
+
return self._execution_mode if self.application_batch else self.agent.execution_mode
|
|
103
126
|
|
|
104
127
|
@property
|
|
105
128
|
def pending_calls(self) -> tuple[str, ...]:
|
|
@@ -199,7 +222,16 @@ class Run:
|
|
|
199
222
|
await asyncio.gather(cancel_wait, return_exceptions=True)
|
|
200
223
|
|
|
201
224
|
async def emit(self, kind: str, call_id: str | None = None, **data: Any) -> None:
|
|
202
|
-
|
|
225
|
+
if self.include_tool_origin and kind.startswith("tool_execution_"):
|
|
226
|
+
data.update(origin=self.origin, source=self.source)
|
|
227
|
+
await self.agent._events.emit(
|
|
228
|
+
kind,
|
|
229
|
+
self.id,
|
|
230
|
+
None if self.application_batch else self.turn,
|
|
231
|
+
data,
|
|
232
|
+
call_id,
|
|
233
|
+
self.await_owned,
|
|
234
|
+
)
|
|
203
235
|
|
|
204
236
|
async def hook(self, name: str, *args: Any) -> Any:
|
|
205
237
|
callback = getattr(self.hooks, name)
|
|
@@ -307,6 +339,8 @@ class Run:
|
|
|
307
339
|
result = outcome.result
|
|
308
340
|
outcome._settled = True
|
|
309
341
|
self._committed.add(id(outcome))
|
|
342
|
+
if self.application_batch:
|
|
343
|
+
return # One custom batch record is appended after all accepted calls settle.
|
|
310
344
|
message = ToolResultMessage(
|
|
311
345
|
outcome.call.id,
|
|
312
346
|
outcome.call.name,
|
|
@@ -330,6 +364,37 @@ class Run:
|
|
|
330
364
|
execution_status=outcome.execution_status,
|
|
331
365
|
)
|
|
332
366
|
|
|
367
|
+
async def append_application_batch(self) -> None:
|
|
368
|
+
calls = []
|
|
369
|
+
for outcome in self.outcomes:
|
|
370
|
+
result = outcome.result
|
|
371
|
+
assert result is not None # commit_outcome fills unresolved calls during cleanup.
|
|
372
|
+
calls.append(
|
|
373
|
+
{
|
|
374
|
+
"id": outcome.call.id,
|
|
375
|
+
"name": outcome.call.name,
|
|
376
|
+
"arguments": deepcopy(outcome.original_arguments),
|
|
377
|
+
"prepared_arguments": deepcopy(outcome.prepared_arguments),
|
|
378
|
+
"origin": outcome.origin,
|
|
379
|
+
"source": outcome.source,
|
|
380
|
+
"execution_status": outcome.execution_status,
|
|
381
|
+
"is_error": result.is_error,
|
|
382
|
+
"error_code": result.error_code,
|
|
383
|
+
"content": [asdict(block) for block in result.content],
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
await self.append(
|
|
387
|
+
CustomMessage(
|
|
388
|
+
"application_tool_batch",
|
|
389
|
+
{
|
|
390
|
+
"run_id": self.id,
|
|
391
|
+
"origin": self.origin,
|
|
392
|
+
"source": self.source,
|
|
393
|
+
"calls": calls,
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
|
|
333
398
|
async def drive(self, pending: list[Message]) -> RunResult:
|
|
334
399
|
agent = self.agent
|
|
335
400
|
self.driver_started = True
|
|
@@ -339,7 +404,11 @@ class Run:
|
|
|
339
404
|
# or aborted assistant message, followed by turn_end.
|
|
340
405
|
failure: AssistantMessage | None = None
|
|
341
406
|
try:
|
|
342
|
-
status, reason =
|
|
407
|
+
status, reason = (
|
|
408
|
+
await run_application_batch(self)
|
|
409
|
+
if self.application_batch
|
|
410
|
+
else await run_loop(self, pending)
|
|
411
|
+
)
|
|
343
412
|
except asyncio.CancelledError:
|
|
344
413
|
status, reason, failure = self.cancelled_outcome()
|
|
345
414
|
except Exception as exc:
|
|
@@ -366,6 +435,16 @@ class Run:
|
|
|
366
435
|
except Exception as exc:
|
|
367
436
|
status = "failed"
|
|
368
437
|
errors.append(f"{type(exc).__name__}: {exc}")
|
|
438
|
+
if self.application_batch:
|
|
439
|
+
failure = None # No model turn occurred, including on abort or failure.
|
|
440
|
+
try:
|
|
441
|
+
await self.append_application_batch()
|
|
442
|
+
except asyncio.CancelledError:
|
|
443
|
+
status = "cancelled" if status != "limit_reached" else status
|
|
444
|
+
errors.append("Batch message delivery cancelled")
|
|
445
|
+
except Exception as exc:
|
|
446
|
+
status = "failed"
|
|
447
|
+
errors.append(f"{type(exc).__name__}: {exc}")
|
|
369
448
|
tail = agent._messages[-1] if len(agent._messages) > self.start else None
|
|
370
449
|
if failure is not None and not (
|
|
371
450
|
isinstance(tail, AssistantMessage) and tail.stop_reason in {"error", "aborted"}
|
|
@@ -420,9 +499,10 @@ class Run:
|
|
|
420
499
|
self.fail(errors[-1])
|
|
421
500
|
await self.cleanup()
|
|
422
501
|
# Explicit config updates persist even if no further model request occurred.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
502
|
+
if not self.application_batch:
|
|
503
|
+
for update in agent._updates:
|
|
504
|
+
merge(agent._defaults, update)
|
|
505
|
+
agent._updates.clear()
|
|
426
506
|
usage: dict[str, Any] = {}
|
|
427
507
|
for m in agent._messages[self.start :]:
|
|
428
508
|
if isinstance(m, AssistantMessage):
|
pi_python/tools.py
CHANGED
|
@@ -348,8 +348,45 @@ def apply_result_update(result: ToolResult, update: ToolResultUpdate) -> None:
|
|
|
348
348
|
result.terminate = update.terminate
|
|
349
349
|
|
|
350
350
|
|
|
351
|
+
@dataclass(frozen=True)
|
|
352
|
+
class ToolInvocation:
|
|
353
|
+
"""An application-requested call; the Agent assigns its ID."""
|
|
354
|
+
|
|
355
|
+
name: str
|
|
356
|
+
arguments: dict[str, Any]
|
|
357
|
+
|
|
358
|
+
def __post_init__(self) -> None:
|
|
359
|
+
if not isinstance(self.name, str) or not self.name:
|
|
360
|
+
raise ConfigurationError("ToolInvocation requires a nonempty tool name")
|
|
361
|
+
validate_json(self.arguments)
|
|
362
|
+
if not isinstance(self.arguments, dict):
|
|
363
|
+
raise ConfigurationError("ToolInvocation arguments must be an object")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class _ToolOrigin:
|
|
367
|
+
"""Read-only provenance without changing legacy dataclass fields/asdict output.
|
|
368
|
+
|
|
369
|
+
Model calls need no per-instance storage. Application calls attach provenance
|
|
370
|
+
explicitly; deepcopy preserves it even though dataclasses.asdict excludes it.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
_origin = "model"
|
|
374
|
+
_source: str | None = None
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def origin(self) -> str:
|
|
378
|
+
return self._origin
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def source(self) -> str | None:
|
|
382
|
+
return self._source
|
|
383
|
+
|
|
384
|
+
def _set_origin(self, origin: str, source: str | None) -> None:
|
|
385
|
+
self._origin, self._source = origin, source
|
|
386
|
+
|
|
387
|
+
|
|
351
388
|
@dataclass
|
|
352
|
-
class ToolContext:
|
|
389
|
+
class ToolContext(_ToolOrigin):
|
|
353
390
|
run_id: str
|
|
354
391
|
call_id: str
|
|
355
392
|
cancel: CancelToken
|
|
@@ -449,7 +486,7 @@ class Tool:
|
|
|
449
486
|
|
|
450
487
|
|
|
451
488
|
@dataclass
|
|
452
|
-
class ToolOutcome:
|
|
489
|
+
class ToolOutcome(_ToolOrigin):
|
|
453
490
|
call: ToolCall
|
|
454
491
|
execution_status: str = "not_started"
|
|
455
492
|
original_arguments: dict[str, Any] = field(default_factory=dict)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pi-python-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: An embeddable asyncio agent core with opt-in Claude and OpenAI providers
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -111,7 +111,7 @@ Apart from `provider_chat`, which needs real credentials, every example runs off
|
|
|
111
111
|
|
|
112
112
|
The run loop, event order, hooks, queues, and the handling of errors and cancellation all match Pi, and this is checked differentially: the same inputs go to the pinned upstream code and to this library, and the model requests, tool calls, events and final transcript are compared item by item. All cases currently agree: 25 for the core loop, 50 for model connectors, 2 for multi-turn WebSocket, and 44 error-classification samples. The plugin rules for skills, prompt templates and frontmatter are compared the same way against Pi's coding-agent code.
|
|
113
113
|
|
|
114
|
-
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. Plugins are
|
|
114
|
+
A few differences are deliberate, such as strict tool-argument validation without type coercion, and returning copies of state to callers. Others are additions for Python users, such as `@tool`, blocking calls, and calling `continue_run()` directly after a failure. Each one is recorded in the [coverage map](compat/COVERAGE.md) (Chinese). Features Pi keeps in its application layer (terminal UI, session file format, context compaction) are not in this core; compaction can be built with hooks, and an example shows the full approach. The Agent also supplies cache-only session IDs for Codex, a responsibility upstream assigns to its coding-agent layer; see [cache sessions and the opt-out](docs/PROVIDERS.md#automatic-codex-cache-sessions). Plugins are another piece taken from that layer: an optional module that reads Pi's package format and builds an ordinary Agent from it. This project uses its own version numbers and is not an official Pi release.
|
|
115
115
|
|
|
116
116
|
## Documentation
|
|
117
117
|
|
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
pi_python/__init__.py,sha256
|
|
1
|
+
pi_python/__init__.py,sha256=-32DW8gNgcvPYNQJZUago0hrkgx_v4VZnOsr4BmG46M,3958
|
|
2
2
|
pi_python/_mcp_host.py,sha256=CXio-tiEeUCUfdl65tatwSrzIOYdgecdS00sO6tNy5k,8630
|
|
3
3
|
pi_python/_mcp_interaction.py,sha256=jPL-C8wLjBDV9tOdvGVFfXOl8-1FQIDJLq-MojlzXaM,43851
|
|
4
4
|
pi_python/_mcp_process.py,sha256=4szi5gMEP_0GUGOcV-oLPUuVQAUSb0ToAaiM979zHME,6324
|
|
5
5
|
pi_python/_mcp_sampling.py,sha256=BcWCrPgjsRC-GNx65fIB-JPq2WzWnoBK2w9iTnyPUww,12148
|
|
6
|
-
pi_python/_version.py,sha256=
|
|
7
|
-
pi_python/agent.py,sha256=
|
|
6
|
+
pi_python/_version.py,sha256=raMu9XA9JEjvdoTmFqcOw7qhJX24rYDP7XmS59TAO-Q,23
|
|
7
|
+
pi_python/agent.py,sha256=5yd5D_hDGwp74dtvH1SCQUQdLFpc1nxTqAEKBbleSDQ,19797
|
|
8
8
|
pi_python/cancellation.py,sha256=DoQVdX7DcQdVLkl4OvhcpMhKZJEW0Kscl2-xxF6i6Y0,608
|
|
9
9
|
pi_python/errors.py,sha256=65lT0nYfzqxRfUlFG-Y9vgYxfDHEuFiBfgqmFnX9_O8,675
|
|
10
10
|
pi_python/estimate.py,sha256=dQQJ9UEGp57F3wO_CcU2oubgPe841ttgpUseK3Fu_0w,5048
|
|
11
|
-
pi_python/events.py,sha256=
|
|
12
|
-
pi_python/features.py,sha256=
|
|
11
|
+
pi_python/events.py,sha256=HFYaPB9-yjOvmuM2Ly7SgfWDMJJfX9e1Q7ethqOHSB8,4626
|
|
12
|
+
pi_python/features.py,sha256=Q7X1ke7FcGCyb8XURw4iDa2-wfbaWQQCdbR4-Zh5fsk,1720
|
|
13
13
|
pi_python/function_tools.py,sha256=DO8fZD-n5i62w22zLIjblCOTwnG8N-HIoSUo4yOpRM4,18862
|
|
14
14
|
pi_python/hooks.py,sha256=jm6ep78NjdHxIPZvcOXTZIPxkfBMkffVTNzLvnHS9mM,1321
|
|
15
15
|
pi_python/limits.py,sha256=K1aP1cFH4fVhtWIXqpWMmpkB9AI0wK-bz3Xd2hxnOs4,1157
|
|
16
|
-
pi_python/loop.py,sha256=
|
|
16
|
+
pi_python/loop.py,sha256=Q67rzG0eERY3Y_NMDdCIbKZiadgdcF2sSB83qni92ms,19263
|
|
17
17
|
pi_python/lowlevel.py,sha256=56wtwqD0Ur1AGAXGa7zclb1IpmRPFpSEss_nspkAdrI,5514
|
|
18
18
|
pi_python/mcp.py,sha256=f7VOiOeBwJ48SrowJWmqtH3d-iavrv2GZo2KH45qeTA,16713
|
|
19
|
-
pi_python/messages.py,sha256=
|
|
19
|
+
pi_python/messages.py,sha256=jUoQxR-UZbIQYWu257dGiCx8K5z_RV89fJpHgv4t-Ww,18068
|
|
20
20
|
pi_python/models.py,sha256=IejtaWvbURM9vv_2kkoDzfto2TR0OSLjuQeOh3IJZ1k,6094
|
|
21
|
-
pi_python/provider.py,sha256=
|
|
21
|
+
pi_python/provider.py,sha256=WH1-8YQKuV0PrBTLdNJKN9oXYnnOQiCxt4kD2BL-ebw,5047
|
|
22
22
|
pi_python/proxy.py,sha256=kpEUoE1oDkJW7VVaV72slN3uJUFgYR4Q6jtYOEe8LmQ,12159
|
|
23
23
|
pi_python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
pi_python/queues.py,sha256=nQ5xRSA3MMOHMs_ZJnG9sT3-d_yKn72oSnxXqiKPrcI,3032
|
|
25
25
|
pi_python/recovery.py,sha256=z5ht-6FoKtC5BPEaSgXiRE2KVf94mZDiKMSxV0mkZ-4,8138
|
|
26
|
-
pi_python/run.py,sha256=
|
|
26
|
+
pi_python/run.py,sha256=S-mJ5xHVzVXdt8aBicRr4jU1-VZW1-QDi4Cru2hPgmE,21831
|
|
27
27
|
pi_python/stream.py,sha256=R3v-qjCHFQJhHQk83qATz7lC3yr8paJguaHwk8RoHow,10729
|
|
28
28
|
pi_python/sync.py,sha256=uTTX07hxe6TOCgwtfBxSJh2oJFDa2UklNmlCJ9g5-ss,5261
|
|
29
29
|
pi_python/tasks.py,sha256=UNPhb_HSTAaALcstV4X9T4q8JZTyMf90LjRR5bFw26M,4738
|
|
30
30
|
pi_python/testing.py,sha256=XfTSOkEv0CenqrBILI_T205jOhyitW53YQZ9yiMXir4,1046
|
|
31
|
-
pi_python/tools.py,sha256=
|
|
31
|
+
pi_python/tools.py,sha256=507d9JXMdn1fsoS1Y1TZIauEf4d570vDeRMfavb9itY,24417
|
|
32
32
|
pi_python/transcript.py,sha256=g6Tg8eiyMCAK43iLpcBv220MHBq8kO7Ty-6OYLCzw2A,6685
|
|
33
33
|
pi_python/data/models.json,sha256=oa_LGv7vaRrS-l7_Z7iu8f7WTyoJJLPhUQyptUz7OmE,77549
|
|
34
34
|
pi_python/plugins/__init__.py,sha256=FeYHYxgaOjKuq3QBBv1ezOyfORi5Fl2SdnImN_O0IrE,56156
|
|
@@ -41,10 +41,10 @@ pi_python/providers/anthropic.py,sha256=PCH9h9WHNXbz7Pv01NM1d8NtHANthLRaU1mZUVUc
|
|
|
41
41
|
pi_python/providers/common.py,sha256=z8dB2pIc-dRscPAppnA3ZwF_wJygunQb9pI9Wl4CvaA,8051
|
|
42
42
|
pi_python/providers/completions.py,sha256=vcqoLfJ-9quMREEEAJcHYs3Oq-A0Sz8Q1NV1QLd3OJA,51362
|
|
43
43
|
pi_python/providers/oauth.py,sha256=ZY8j7P-I-R6hZ8sInkGao957KWNGsqfvMF0F-YzlSgU,22786
|
|
44
|
-
pi_python/providers/openai.py,sha256=
|
|
44
|
+
pi_python/providers/openai.py,sha256=c2xzfRd0RgYxbtgl-ruA0yLySq5W63mmwyu_HFQm2g0,30931
|
|
45
45
|
pi_python/providers/transport.py,sha256=B1mGKlVEqYGVK1MlcA9opwRhnuUYGuaYuTslNC3O8xs,26772
|
|
46
|
-
pi_python_core-0.
|
|
47
|
-
pi_python_core-0.
|
|
48
|
-
pi_python_core-0.
|
|
49
|
-
pi_python_core-0.
|
|
50
|
-
pi_python_core-0.
|
|
46
|
+
pi_python_core-0.11.0.dist-info/METADATA,sha256=m36iv6YTPLV0GsXN29gp-rIwgRQSnO8m-uAtgvsovYs,9146
|
|
47
|
+
pi_python_core-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
48
|
+
pi_python_core-0.11.0.dist-info/licenses/LICENSE,sha256=BFf1vOw7OyEWBd-10aSQQv1jjzaGpBD-CZwkolrxPEg,1069
|
|
49
|
+
pi_python_core-0.11.0.dist-info/licenses/NOTICE,sha256=fJna16fm1ECHN7zVZhtyNmjLw8KGxGp8mL9blISF1K0,633
|
|
50
|
+
pi_python_core-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|