praxo-core 0.2.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.
- praxo/__init__.py +104 -0
- praxo/adapters/__init__.py +3 -0
- praxo/adapters/openaicompat.py +231 -0
- praxo/agent.py +371 -0
- praxo/checkpoint.py +141 -0
- praxo/conformance.py +549 -0
- praxo/conformance_agent.py +118 -0
- praxo/context.py +258 -0
- praxo/errors.py +105 -0
- praxo/event.py +81 -0
- praxo/jsonschema.py +171 -0
- praxo/memory.py +111 -0
- praxo/middleware/__init__.py +61 -0
- praxo/middleware/base.py +101 -0
- praxo/middleware/calllimit.py +55 -0
- praxo/middleware/fallback.py +22 -0
- praxo/middleware/pii.py +86 -0
- praxo/middleware/structured.py +136 -0
- praxo/middleware/toolselect.py +129 -0
- praxo/model.py +235 -0
- praxo/parity.py +44 -0
- praxo/plugin.py +327 -0
- praxo/policy.py +118 -0
- praxo/stream.py +33 -0
- praxo/tool.py +92 -0
- praxo/toolcall.py +246 -0
- praxo_core-0.2.1.dist-info/METADATA +135 -0
- praxo_core-0.2.1.dist-info/RECORD +31 -0
- praxo_core-0.2.1.dist-info/WHEEL +5 -0
- praxo_core-0.2.1.dist-info/licenses/LICENSE +21 -0
- praxo_core-0.2.1.dist-info/top_level.txt +1 -0
praxo/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from .agent import Agent, AgentOptions, Result, Session
|
|
2
|
+
from .checkpoint import Allowlist, Checkpoint, Message as CheckpointMessage
|
|
3
|
+
from .context import (
|
|
4
|
+
ContextBudget,
|
|
5
|
+
ContextCompactor,
|
|
6
|
+
ModelSummarizer,
|
|
7
|
+
OffloadFunc,
|
|
8
|
+
Offloader,
|
|
9
|
+
estimate_tokens,
|
|
10
|
+
load_agents_md,
|
|
11
|
+
)
|
|
12
|
+
from .event import Event, FilterSink, MultiSink, Sink
|
|
13
|
+
from .middleware import (
|
|
14
|
+
Middleware,
|
|
15
|
+
ModelCall,
|
|
16
|
+
ModelMiddleware,
|
|
17
|
+
ModelToolSelector,
|
|
18
|
+
PIIRedactor,
|
|
19
|
+
RunState,
|
|
20
|
+
StructuredOutput,
|
|
21
|
+
ToolResult,
|
|
22
|
+
call_limit_middleware,
|
|
23
|
+
default_pii_rules,
|
|
24
|
+
fallback_model,
|
|
25
|
+
pii_middleware,
|
|
26
|
+
tool_selection_middleware,
|
|
27
|
+
)
|
|
28
|
+
from .model import (
|
|
29
|
+
CacheHint,
|
|
30
|
+
ChatModel,
|
|
31
|
+
ContentBlock,
|
|
32
|
+
Message,
|
|
33
|
+
Model,
|
|
34
|
+
Request,
|
|
35
|
+
Response,
|
|
36
|
+
ToolCall,
|
|
37
|
+
ToolSpec,
|
|
38
|
+
Usage,
|
|
39
|
+
)
|
|
40
|
+
from .policy import Action, Decision, FuncHook, Hook, PathChecker, PathGrant
|
|
41
|
+
from .tool import FuncTool, Gateway, GatewayInfo, GatewayTool, Registry, Tool
|
|
42
|
+
from .toolcall import parse_tool_call, strip_tool_call_markup
|
|
43
|
+
|
|
44
|
+
__version__ = "0.2.1"
|
|
45
|
+
SPEC_VERSION = "0.2.0"
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"Action",
|
|
49
|
+
"Agent",
|
|
50
|
+
"AgentOptions",
|
|
51
|
+
"Allowlist",
|
|
52
|
+
"CacheHint",
|
|
53
|
+
"ChatModel",
|
|
54
|
+
"Checkpoint",
|
|
55
|
+
"CheckpointMessage",
|
|
56
|
+
"ContentBlock",
|
|
57
|
+
"ContextBudget",
|
|
58
|
+
"ContextCompactor",
|
|
59
|
+
"Decision",
|
|
60
|
+
"Event",
|
|
61
|
+
"FilterSink",
|
|
62
|
+
"FuncHook",
|
|
63
|
+
"FuncTool",
|
|
64
|
+
"Gateway",
|
|
65
|
+
"GatewayInfo",
|
|
66
|
+
"GatewayTool",
|
|
67
|
+
"Hook",
|
|
68
|
+
"Message",
|
|
69
|
+
"Middleware",
|
|
70
|
+
"Model",
|
|
71
|
+
"ModelCall",
|
|
72
|
+
"ModelMiddleware",
|
|
73
|
+
"ModelSummarizer",
|
|
74
|
+
"ModelToolSelector",
|
|
75
|
+
"MultiSink",
|
|
76
|
+
"OffloadFunc",
|
|
77
|
+
"Offloader",
|
|
78
|
+
"PIIRedactor",
|
|
79
|
+
"PathChecker",
|
|
80
|
+
"PathGrant",
|
|
81
|
+
"Registry",
|
|
82
|
+
"Request",
|
|
83
|
+
"Response",
|
|
84
|
+
"Result",
|
|
85
|
+
"RunState",
|
|
86
|
+
"SPEC_VERSION",
|
|
87
|
+
"Session",
|
|
88
|
+
"Sink",
|
|
89
|
+
"StructuredOutput",
|
|
90
|
+
"Tool",
|
|
91
|
+
"ToolCall",
|
|
92
|
+
"ToolResult",
|
|
93
|
+
"ToolSpec",
|
|
94
|
+
"Usage",
|
|
95
|
+
"call_limit_middleware",
|
|
96
|
+
"default_pii_rules",
|
|
97
|
+
"estimate_tokens",
|
|
98
|
+
"fallback_model",
|
|
99
|
+
"load_agents_md",
|
|
100
|
+
"parse_tool_call",
|
|
101
|
+
"pii_middleware",
|
|
102
|
+
"strip_tool_call_markup",
|
|
103
|
+
"tool_selection_middleware",
|
|
104
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
from urllib.error import HTTPError, URLError
|
|
8
|
+
from urllib.request import Request as HTTPRequest
|
|
9
|
+
from urllib.request import urlopen
|
|
10
|
+
|
|
11
|
+
from ..errors import PraxoError
|
|
12
|
+
from ..model import (
|
|
13
|
+
ROLE_ASSISTANT,
|
|
14
|
+
Message,
|
|
15
|
+
Request,
|
|
16
|
+
Response,
|
|
17
|
+
ToolCall,
|
|
18
|
+
Usage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class OpenAIConfig:
|
|
24
|
+
base_url: str
|
|
25
|
+
api_key: str = ""
|
|
26
|
+
model: str = ""
|
|
27
|
+
timeout: float = 60.0
|
|
28
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OpenAICompatibleModel:
|
|
32
|
+
def __init__(self, config: OpenAIConfig) -> None:
|
|
33
|
+
self.config = config
|
|
34
|
+
|
|
35
|
+
async def generate(self, request: Request) -> Response:
|
|
36
|
+
payload = self._payload(request, stream=False)
|
|
37
|
+
data = await asyncio.to_thread(self._post, payload)
|
|
38
|
+
return self._parse_response(data)
|
|
39
|
+
|
|
40
|
+
async def generate_stream(
|
|
41
|
+
self, request: Request, on_delta: Callable[[str], Any]
|
|
42
|
+
) -> Response:
|
|
43
|
+
payload = self._payload(request, stream=True)
|
|
44
|
+
return await asyncio.to_thread(self._post_stream, payload, on_delta)
|
|
45
|
+
|
|
46
|
+
def _payload(self, request: Request, stream: bool) -> dict[str, Any]:
|
|
47
|
+
messages: list[dict[str, Any]] = []
|
|
48
|
+
for message in request.messages:
|
|
49
|
+
wire = message.to_wire()
|
|
50
|
+
if "content" not in wire and not wire.get("toolCalls"):
|
|
51
|
+
wire["content"] = ""
|
|
52
|
+
messages.append(wire)
|
|
53
|
+
payload: dict[str, Any] = {
|
|
54
|
+
"model": request.model or self.config.model,
|
|
55
|
+
"messages": messages,
|
|
56
|
+
"stream": stream,
|
|
57
|
+
}
|
|
58
|
+
if request.tools:
|
|
59
|
+
tools = []
|
|
60
|
+
for spec in request.tools:
|
|
61
|
+
function: dict[str, Any] = {
|
|
62
|
+
"name": spec.name,
|
|
63
|
+
"description": spec.description,
|
|
64
|
+
}
|
|
65
|
+
if spec.parameters is not None:
|
|
66
|
+
function["parameters"] = spec.parameters
|
|
67
|
+
tools.append({"type": "function", "function": function})
|
|
68
|
+
payload["tools"] = tools
|
|
69
|
+
if request.tool_choice is not None:
|
|
70
|
+
payload["tool_choice"] = request.tool_choice
|
|
71
|
+
if request.response_format is not None:
|
|
72
|
+
payload["response_format"] = {
|
|
73
|
+
"type": "json_schema",
|
|
74
|
+
"json_schema": {
|
|
75
|
+
"name": request.response_format.name or "output",
|
|
76
|
+
"schema": request.response_format.schema or {},
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
if request.temperature:
|
|
80
|
+
payload["temperature"] = request.temperature
|
|
81
|
+
return payload
|
|
82
|
+
|
|
83
|
+
def _post(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
84
|
+
data = json.dumps(payload).encode("utf-8")
|
|
85
|
+
request = HTTPRequest(
|
|
86
|
+
self._endpoint(),
|
|
87
|
+
data=data,
|
|
88
|
+
headers=self._headers(),
|
|
89
|
+
method="POST",
|
|
90
|
+
)
|
|
91
|
+
try:
|
|
92
|
+
with urlopen(request, timeout=self.config.timeout) as response:
|
|
93
|
+
return json.loads(response.read().decode("utf-8"))
|
|
94
|
+
except HTTPError as exc:
|
|
95
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
96
|
+
raise PraxoError(
|
|
97
|
+
f"openaicompat: HTTP {exc.code}: {body[:512]}"
|
|
98
|
+
) from exc
|
|
99
|
+
except URLError as exc:
|
|
100
|
+
raise PraxoError(f"openaicompat: {exc.reason}") from exc
|
|
101
|
+
|
|
102
|
+
def _post_stream(
|
|
103
|
+
self,
|
|
104
|
+
payload: dict[str, Any],
|
|
105
|
+
on_delta: Callable[[str], Any],
|
|
106
|
+
) -> Response:
|
|
107
|
+
data = json.dumps(payload).encode("utf-8")
|
|
108
|
+
request = HTTPRequest(
|
|
109
|
+
self._endpoint(),
|
|
110
|
+
data=data,
|
|
111
|
+
headers=self._headers(),
|
|
112
|
+
method="POST",
|
|
113
|
+
)
|
|
114
|
+
content: list[str] = []
|
|
115
|
+
calls: dict[int, dict[str, Any]] = {}
|
|
116
|
+
usage = Usage()
|
|
117
|
+
finish_reason: str | None = None
|
|
118
|
+
try:
|
|
119
|
+
with urlopen(request, timeout=self.config.timeout) as response:
|
|
120
|
+
for raw_line in response:
|
|
121
|
+
line = raw_line.decode("utf-8").strip()
|
|
122
|
+
if not line.startswith("data:"):
|
|
123
|
+
continue
|
|
124
|
+
chunk = line[5:].strip()
|
|
125
|
+
if chunk == "[DONE]":
|
|
126
|
+
break
|
|
127
|
+
if not chunk:
|
|
128
|
+
continue
|
|
129
|
+
event = json.loads(chunk)
|
|
130
|
+
if event.get("usage"):
|
|
131
|
+
usage = _parse_usage(event["usage"])
|
|
132
|
+
for choice in event.get("choices", []):
|
|
133
|
+
finish_reason = choice.get("finish_reason") or finish_reason
|
|
134
|
+
delta = choice.get("delta") or {}
|
|
135
|
+
text = delta.get("content")
|
|
136
|
+
if text:
|
|
137
|
+
content.append(text)
|
|
138
|
+
on_delta(text)
|
|
139
|
+
for call in delta.get("tool_calls") or []:
|
|
140
|
+
index = call.get("index", 0)
|
|
141
|
+
slot = calls.setdefault(
|
|
142
|
+
index,
|
|
143
|
+
{"id": "", "name": "", "arguments": ""},
|
|
144
|
+
)
|
|
145
|
+
if call.get("id"):
|
|
146
|
+
slot["id"] = call["id"]
|
|
147
|
+
function = call.get("function") or {}
|
|
148
|
+
if function.get("name"):
|
|
149
|
+
slot["name"] += function["name"]
|
|
150
|
+
if function.get("arguments"):
|
|
151
|
+
slot["arguments"] += function["arguments"]
|
|
152
|
+
except HTTPError as exc:
|
|
153
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
154
|
+
raise PraxoError(
|
|
155
|
+
f"openaicompat: HTTP {exc.code}: {body[:512]}"
|
|
156
|
+
) from exc
|
|
157
|
+
except URLError as exc:
|
|
158
|
+
raise PraxoError(f"openaicompat: {exc.reason}") from exc
|
|
159
|
+
tool_calls = [_build_call(index, value) for index, value in sorted(calls.items())]
|
|
160
|
+
return Response(
|
|
161
|
+
message=Message(
|
|
162
|
+
role=ROLE_ASSISTANT,
|
|
163
|
+
content="".join(content),
|
|
164
|
+
tool_calls=tool_calls,
|
|
165
|
+
),
|
|
166
|
+
usage=usage,
|
|
167
|
+
finish_reason=finish_reason,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _endpoint(self) -> str:
|
|
171
|
+
return self.config.base_url.rstrip("/") + "/chat/completions"
|
|
172
|
+
|
|
173
|
+
def _headers(self) -> dict[str, str]:
|
|
174
|
+
headers = {
|
|
175
|
+
"Content-Type": "application/json",
|
|
176
|
+
"Accept": "application/json",
|
|
177
|
+
}
|
|
178
|
+
if self.config.api_key:
|
|
179
|
+
headers["Authorization"] = f"Bearer {self.config.api_key}"
|
|
180
|
+
headers.update(self.config.headers)
|
|
181
|
+
return headers
|
|
182
|
+
|
|
183
|
+
def _parse_response(self, data: dict[str, Any]) -> Response:
|
|
184
|
+
choices = data.get("choices") or []
|
|
185
|
+
if not choices:
|
|
186
|
+
raise PraxoError("openaicompat: response has no choices")
|
|
187
|
+
choice = choices[0]
|
|
188
|
+
message = choice.get("message") or {}
|
|
189
|
+
calls = [
|
|
190
|
+
_build_call(index, call)
|
|
191
|
+
for index, call in enumerate(message.get("tool_calls") or [])
|
|
192
|
+
]
|
|
193
|
+
return Response(
|
|
194
|
+
message=Message(
|
|
195
|
+
role=ROLE_ASSISTANT,
|
|
196
|
+
content=message.get("content") or "",
|
|
197
|
+
tool_calls=calls,
|
|
198
|
+
),
|
|
199
|
+
usage=_parse_usage(data.get("usage") or {}),
|
|
200
|
+
finish_reason=choice.get("finish_reason"),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _build_call(index: int, raw: dict[str, Any]) -> ToolCall:
|
|
205
|
+
function = raw.get("function")
|
|
206
|
+
if function is None:
|
|
207
|
+
function = {
|
|
208
|
+
"name": raw.get("name", ""),
|
|
209
|
+
"arguments": raw.get("arguments", "{}"),
|
|
210
|
+
}
|
|
211
|
+
arguments = function.get("arguments") or "{}"
|
|
212
|
+
try:
|
|
213
|
+
parsed = json.loads(arguments)
|
|
214
|
+
except (ValueError, TypeError):
|
|
215
|
+
parsed = {"content": arguments}
|
|
216
|
+
if not isinstance(parsed, dict):
|
|
217
|
+
parsed = {"content": parsed}
|
|
218
|
+
return ToolCall(
|
|
219
|
+
id=raw.get("id") or f"call-{index}",
|
|
220
|
+
name=function.get("name", ""),
|
|
221
|
+
arguments=parsed,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _parse_usage(raw: dict[str, Any]) -> Usage:
|
|
226
|
+
details = raw.get("prompt_tokens_details") or {}
|
|
227
|
+
return Usage(
|
|
228
|
+
input_tokens=int(raw.get("prompt_tokens", 0)),
|
|
229
|
+
output_tokens=int(raw.get("completion_tokens", 0)),
|
|
230
|
+
cached_tokens=int(details.get("cached_tokens", 0)),
|
|
231
|
+
)
|