prefactor-core 0.1.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.
- prefactor_core/__init__.py +57 -0
- prefactor_core/client.py +405 -0
- prefactor_core/config.py +60 -0
- prefactor_core/context_stack.py +118 -0
- prefactor_core/exceptions.py +49 -0
- prefactor_core/managers/__init__.py +6 -0
- prefactor_core/managers/agent_instance.py +255 -0
- prefactor_core/managers/span.py +323 -0
- prefactor_core/models.py +67 -0
- prefactor_core/operations.py +61 -0
- prefactor_core/queue/__init__.py +16 -0
- prefactor_core/queue/base.py +91 -0
- prefactor_core/queue/executor.py +183 -0
- prefactor_core/queue/memory.py +104 -0
- prefactor_core/schema_registry.py +263 -0
- prefactor_core/span_context.py +192 -0
- prefactor_core-0.1.0.dist-info/METADATA +273 -0
- prefactor_core-0.1.0.dist-info/RECORD +19 -0
- prefactor_core-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Public API for prefactor-core.
|
|
2
|
+
|
|
3
|
+
This module exports the main classes and functions for the prefactor-core SDK.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .client import PrefactorCoreClient
|
|
7
|
+
from .config import PrefactorCoreConfig, QueueConfig
|
|
8
|
+
from .context_stack import SpanContextStack
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
ClientAlreadyInitializedError,
|
|
11
|
+
ClientNotInitializedError,
|
|
12
|
+
InstanceNotFoundError,
|
|
13
|
+
OperationError,
|
|
14
|
+
PrefactorCoreError,
|
|
15
|
+
SpanNotFoundError,
|
|
16
|
+
)
|
|
17
|
+
from .managers.agent_instance import AgentInstanceHandle
|
|
18
|
+
from .models import AgentInstance, Span
|
|
19
|
+
from .operations import Operation, OperationType
|
|
20
|
+
from .queue import InMemoryQueue, Queue, QueueClosedError, TaskExecutor
|
|
21
|
+
from .schema_registry import SchemaRegistry
|
|
22
|
+
from .span_context import SpanContext
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Client
|
|
28
|
+
"PrefactorCoreClient",
|
|
29
|
+
# Config
|
|
30
|
+
"PrefactorCoreConfig",
|
|
31
|
+
"QueueConfig",
|
|
32
|
+
# Context
|
|
33
|
+
"SpanContext",
|
|
34
|
+
"SpanContextStack",
|
|
35
|
+
# Exceptions
|
|
36
|
+
"PrefactorCoreError",
|
|
37
|
+
"ClientNotInitializedError",
|
|
38
|
+
"ClientAlreadyInitializedError",
|
|
39
|
+
"OperationError",
|
|
40
|
+
"InstanceNotFoundError",
|
|
41
|
+
"SpanNotFoundError",
|
|
42
|
+
# Models
|
|
43
|
+
"AgentInstance",
|
|
44
|
+
"Span",
|
|
45
|
+
# Operations
|
|
46
|
+
"Operation",
|
|
47
|
+
"OperationType",
|
|
48
|
+
# Queue
|
|
49
|
+
"Queue",
|
|
50
|
+
"QueueClosedError",
|
|
51
|
+
"InMemoryQueue",
|
|
52
|
+
"TaskExecutor",
|
|
53
|
+
# Handle
|
|
54
|
+
"AgentInstanceHandle",
|
|
55
|
+
# Schema Registry
|
|
56
|
+
"SchemaRegistry",
|
|
57
|
+
]
|
prefactor_core/client.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Main client for prefactor-core.
|
|
2
|
+
|
|
3
|
+
This module provides the PrefactorCoreClient, which is the main entry point
|
|
4
|
+
for the SDK. It manages the complete lifecycle of agent instances and spans
|
|
5
|
+
through an async queue-based architecture.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
from prefactor_http.client import PrefactorHttpClient
|
|
15
|
+
|
|
16
|
+
from .config import PrefactorCoreConfig
|
|
17
|
+
from .context_stack import SpanContextStack
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
ClientAlreadyInitializedError,
|
|
20
|
+
ClientNotInitializedError,
|
|
21
|
+
)
|
|
22
|
+
from .managers.agent_instance import AgentInstanceManager
|
|
23
|
+
from .managers.span import SpanManager
|
|
24
|
+
from .operations import Operation, OperationType
|
|
25
|
+
from .queue.base import Queue
|
|
26
|
+
from .queue.executor import TaskExecutor
|
|
27
|
+
from .queue.memory import InMemoryQueue
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .managers.agent_instance import AgentInstanceHandle
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PrefactorCoreClient:
|
|
34
|
+
"""Main entry point for the prefactor-core SDK.
|
|
35
|
+
|
|
36
|
+
This client provides a high-level interface for managing agent instances
|
|
37
|
+
and spans. All operations are queued and processed asynchronously, ensuring
|
|
38
|
+
minimal impact on agent execution flow.
|
|
39
|
+
|
|
40
|
+
The client must be initialized before use, either by calling initialize()
|
|
41
|
+
or using it as an async context manager.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
config = PrefactorCoreConfig(http_config=...)
|
|
45
|
+
|
|
46
|
+
async with PrefactorCoreClient(config) as client:
|
|
47
|
+
instance = await client.create_agent_instance(...)
|
|
48
|
+
await instance.start()
|
|
49
|
+
|
|
50
|
+
async with instance.span("agent:llm") as span:
|
|
51
|
+
span.set_payload({"model": "gpt-4"})
|
|
52
|
+
# Your agent logic here
|
|
53
|
+
|
|
54
|
+
await instance.finish()
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
config: PrefactorCoreConfig,
|
|
60
|
+
queue: Queue[Operation] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initialize the client.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
config: Configuration for the client.
|
|
66
|
+
queue: Optional custom queue implementation. If not provided,
|
|
67
|
+
an InMemoryQueue is used.
|
|
68
|
+
"""
|
|
69
|
+
self._config = config
|
|
70
|
+
self._queue = queue or InMemoryQueue()
|
|
71
|
+
self._http: PrefactorHttpClient | None = None
|
|
72
|
+
self._executor: TaskExecutor | None = None
|
|
73
|
+
self._instance_manager: AgentInstanceManager | None = None
|
|
74
|
+
self._span_manager: SpanManager | None = None
|
|
75
|
+
self._initialized = False
|
|
76
|
+
|
|
77
|
+
async def __aenter__(self) -> "PrefactorCoreClient":
|
|
78
|
+
"""Enter async context manager."""
|
|
79
|
+
await self.initialize()
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
83
|
+
"""Exit async context manager."""
|
|
84
|
+
await self.close()
|
|
85
|
+
|
|
86
|
+
async def initialize(self) -> None:
|
|
87
|
+
"""Initialize the client and start processing.
|
|
88
|
+
|
|
89
|
+
This method:
|
|
90
|
+
1. Initializes the HTTP client
|
|
91
|
+
2. Starts the task executor
|
|
92
|
+
3. Initializes managers
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ClientAlreadyInitializedError: If already initialized.
|
|
96
|
+
"""
|
|
97
|
+
if self._initialized:
|
|
98
|
+
raise ClientAlreadyInitializedError("Client is already initialized")
|
|
99
|
+
|
|
100
|
+
# Initialize HTTP client
|
|
101
|
+
self._http = PrefactorHttpClient(self._config.http_config)
|
|
102
|
+
await self._http.__aenter__()
|
|
103
|
+
|
|
104
|
+
# Initialize executor
|
|
105
|
+
self._executor = TaskExecutor(
|
|
106
|
+
queue=self._queue,
|
|
107
|
+
handler=self._process_operation,
|
|
108
|
+
num_workers=self._config.queue_config.num_workers,
|
|
109
|
+
max_retries=self._config.queue_config.max_retries,
|
|
110
|
+
)
|
|
111
|
+
self._executor.start()
|
|
112
|
+
|
|
113
|
+
# Initialize managers
|
|
114
|
+
self._instance_manager = AgentInstanceManager(
|
|
115
|
+
http_client=self._http,
|
|
116
|
+
enqueue=self._enqueue,
|
|
117
|
+
)
|
|
118
|
+
self._span_manager = SpanManager(
|
|
119
|
+
http_client=self._http,
|
|
120
|
+
enqueue=self._enqueue,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self._initialized = True
|
|
124
|
+
|
|
125
|
+
async def close(self) -> None:
|
|
126
|
+
"""Close the client and cleanup resources.
|
|
127
|
+
|
|
128
|
+
This method gracefully shuts down the executor and closes the
|
|
129
|
+
HTTP client. It should be called when the client is no longer needed.
|
|
130
|
+
"""
|
|
131
|
+
if not self._initialized:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# Stop executor
|
|
135
|
+
if self._executor:
|
|
136
|
+
await self._executor.stop()
|
|
137
|
+
|
|
138
|
+
# Close HTTP client
|
|
139
|
+
if self._http:
|
|
140
|
+
await self._http.__aexit__(None, None, None)
|
|
141
|
+
|
|
142
|
+
self._initialized = False
|
|
143
|
+
|
|
144
|
+
def _ensure_initialized(self) -> None:
|
|
145
|
+
"""Ensure the client is initialized.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ClientNotInitializedError: If not initialized.
|
|
149
|
+
"""
|
|
150
|
+
if not self._initialized:
|
|
151
|
+
raise ClientNotInitializedError(
|
|
152
|
+
"Client is not initialized. Call initialize() or "
|
|
153
|
+
"use as context manager."
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
async def _enqueue(self, operation: Operation) -> None:
|
|
157
|
+
"""Add an operation to the queue.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
operation: The operation to queue.
|
|
161
|
+
"""
|
|
162
|
+
await self._queue.put(operation)
|
|
163
|
+
|
|
164
|
+
async def _process_operation(self, operation: Operation) -> None:
|
|
165
|
+
"""Process a single operation from the queue.
|
|
166
|
+
|
|
167
|
+
This method routes operations to the appropriate handler based on type.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
operation: The operation to process.
|
|
171
|
+
"""
|
|
172
|
+
if not self._http:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
if operation.type == OperationType.REGISTER_AGENT_INSTANCE:
|
|
177
|
+
await self._http.agent_instances.register(
|
|
178
|
+
agent_id=operation.payload["agent_id"],
|
|
179
|
+
agent_version=operation.payload["agent_version"],
|
|
180
|
+
agent_schema_version=operation.payload["agent_schema_version"],
|
|
181
|
+
id=operation.payload.get("id"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
elif operation.type == OperationType.START_AGENT_INSTANCE:
|
|
185
|
+
await self._http.agent_instances.start(
|
|
186
|
+
agent_instance_id=operation.payload["instance_id"],
|
|
187
|
+
timestamp=operation.timestamp,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
elif operation.type == OperationType.FINISH_AGENT_INSTANCE:
|
|
191
|
+
await self._http.agent_instances.finish(
|
|
192
|
+
agent_instance_id=operation.payload["instance_id"],
|
|
193
|
+
timestamp=operation.timestamp,
|
|
194
|
+
)
|
|
195
|
+
elif operation.type == OperationType.CREATE_SPAN:
|
|
196
|
+
await self._http.agent_spans.create(
|
|
197
|
+
agent_instance_id=operation.payload["instance_id"],
|
|
198
|
+
schema_name=operation.payload["schema_name"],
|
|
199
|
+
status=operation.payload.get("status", "pending"),
|
|
200
|
+
id=operation.payload.get("span_id"),
|
|
201
|
+
parent_span_id=operation.payload.get("parent_span_id"),
|
|
202
|
+
payload=operation.payload.get("payload"),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
elif operation.type == OperationType.FINISH_SPAN:
|
|
206
|
+
await self._http.agent_spans.finish(
|
|
207
|
+
agent_span_id=operation.payload["span_id"],
|
|
208
|
+
status=operation.payload.get("status", "complete"),
|
|
209
|
+
result_payload=operation.payload.get("result_payload"),
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Note: UPDATE_SPAN_PAYLOAD requires API support or batching
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
# Log error but don't re-raise - we don't want to crash the worker
|
|
216
|
+
import logging
|
|
217
|
+
|
|
218
|
+
logger = logging.getLogger(__name__)
|
|
219
|
+
logger.error(
|
|
220
|
+
f"Failed to process operation {operation.type}: {e}",
|
|
221
|
+
exc_info=True,
|
|
222
|
+
)
|
|
223
|
+
raise
|
|
224
|
+
|
|
225
|
+
async def create_agent_instance(
|
|
226
|
+
self,
|
|
227
|
+
agent_id: str,
|
|
228
|
+
agent_version: dict[str, Any],
|
|
229
|
+
agent_schema_version: dict[str, Any] | None = None,
|
|
230
|
+
instance_id: str | None = None,
|
|
231
|
+
external_schema_version_id: str | None = None,
|
|
232
|
+
) -> "AgentInstanceHandle":
|
|
233
|
+
"""Create a new agent instance.
|
|
234
|
+
|
|
235
|
+
Returns immediately with a handle. The actual registration happens
|
|
236
|
+
asynchronously via the queue.
|
|
237
|
+
|
|
238
|
+
If agent_schema_version is not provided but the client has a schema_registry,
|
|
239
|
+
the registry's schemas will be used automatically.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
agent_id: ID of the agent to create an instance for.
|
|
243
|
+
agent_version: Version information (name, etc.).
|
|
244
|
+
agent_schema_version: Schema version. Uses registry if not provided
|
|
245
|
+
and registry is configured.
|
|
246
|
+
instance_id: Optional custom ID for the instance.
|
|
247
|
+
external_schema_version_id: Optional external identifier for the
|
|
248
|
+
schema version. Defaults to "auto-generated" when using registry.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
AgentInstanceHandle for the created instance.
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ClientNotInitializedError: If the client is not initialized.
|
|
255
|
+
ValueError: If no schema version provided and registry not configured.
|
|
256
|
+
"""
|
|
257
|
+
self._ensure_initialized()
|
|
258
|
+
assert self._instance_manager is not None
|
|
259
|
+
|
|
260
|
+
# Determine the agent_schema_version to use
|
|
261
|
+
final_schema_version: dict[str, Any]
|
|
262
|
+
if agent_schema_version is not None:
|
|
263
|
+
final_schema_version = agent_schema_version
|
|
264
|
+
elif self._config.schema_registry is not None:
|
|
265
|
+
# Use registry to generate schema version
|
|
266
|
+
ext_id = external_schema_version_id
|
|
267
|
+
if ext_id is None:
|
|
268
|
+
ext_id = f"auto-{agent_id}-{time.time()}"
|
|
269
|
+
final_schema_version = self._config.schema_registry.to_agent_schema_version(
|
|
270
|
+
ext_id
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
msg1 = "agent_schema_version required when no schema_registry configured"
|
|
274
|
+
msg2 = "Either provide agent_schema_version or configure a SchemaRegistry"
|
|
275
|
+
raise ValueError(f"{msg1}. {msg2}.")
|
|
276
|
+
|
|
277
|
+
# Import here to avoid circular import
|
|
278
|
+
from .managers.agent_instance import AgentInstanceHandle
|
|
279
|
+
|
|
280
|
+
instance_id = await self._instance_manager.register(
|
|
281
|
+
agent_id=agent_id,
|
|
282
|
+
agent_version=agent_version,
|
|
283
|
+
agent_schema_version=final_schema_version,
|
|
284
|
+
instance_id=instance_id,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return AgentInstanceHandle(
|
|
288
|
+
instance_id=instance_id,
|
|
289
|
+
client=self,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
async def create_span(
|
|
293
|
+
self,
|
|
294
|
+
instance_id: str,
|
|
295
|
+
schema_name: str,
|
|
296
|
+
parent_span_id: str | None = None,
|
|
297
|
+
payload: dict[str, Any] | None = None,
|
|
298
|
+
) -> str:
|
|
299
|
+
"""Create a span and return its ID without finishing it.
|
|
300
|
+
|
|
301
|
+
Use this for spans that need to stay open across multiple operations.
|
|
302
|
+
Call finish_span() when done.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
instance_id: ID of the agent instance this span belongs to.
|
|
306
|
+
schema_name: Name of the schema for this span.
|
|
307
|
+
parent_span_id: Optional explicit parent span ID.
|
|
308
|
+
payload: Optional initial payload (params/inputs) stored on creation.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
The span ID.
|
|
312
|
+
"""
|
|
313
|
+
self._ensure_initialized()
|
|
314
|
+
assert self._span_manager is not None
|
|
315
|
+
|
|
316
|
+
if parent_span_id is None:
|
|
317
|
+
parent_span_id = SpanContextStack.peek()
|
|
318
|
+
|
|
319
|
+
return await self._span_manager.create(
|
|
320
|
+
instance_id=instance_id,
|
|
321
|
+
schema_name=schema_name,
|
|
322
|
+
parent_span_id=parent_span_id,
|
|
323
|
+
payload=payload,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
async def finish_span(
|
|
327
|
+
self,
|
|
328
|
+
span_id: str,
|
|
329
|
+
result_payload: dict[str, Any] | None = None,
|
|
330
|
+
) -> None:
|
|
331
|
+
"""Finish a previously created span.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
span_id: The ID of the span to finish.
|
|
335
|
+
result_payload: Optional result data to store on the span.
|
|
336
|
+
"""
|
|
337
|
+
self._ensure_initialized()
|
|
338
|
+
assert self._span_manager is not None
|
|
339
|
+
|
|
340
|
+
await self._span_manager.finish(span_id, result_payload=result_payload)
|
|
341
|
+
|
|
342
|
+
@asynccontextmanager
|
|
343
|
+
async def span(
|
|
344
|
+
self,
|
|
345
|
+
instance_id: str,
|
|
346
|
+
schema_name: str,
|
|
347
|
+
parent_span_id: str | None = None,
|
|
348
|
+
span_id: str | None = None,
|
|
349
|
+
payload: dict[str, Any] | None = None,
|
|
350
|
+
):
|
|
351
|
+
"""Context manager for creating and finishing a span.
|
|
352
|
+
|
|
353
|
+
If parent_span_id is not provided, the current span from the
|
|
354
|
+
SpanContextStack is used as the parent.
|
|
355
|
+
|
|
356
|
+
The returned :class:`SpanContext` supports an explicit lifecycle:
|
|
357
|
+
|
|
358
|
+
1. ``await span.start(payload)`` — POST the span to the API.
|
|
359
|
+
2. Do work.
|
|
360
|
+
3. ``await span.complete(result)`` / ``span.fail(result)`` /
|
|
361
|
+
``span.cancel()`` — finish with a specific status.
|
|
362
|
+
|
|
363
|
+
If ``start()`` or a finish method is not called explicitly, the
|
|
364
|
+
context manager handles them automatically on exit.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
instance_id: ID of the agent instance this span belongs to.
|
|
368
|
+
schema_name: Name of the schema for this span.
|
|
369
|
+
parent_span_id: Optional explicit parent span ID.
|
|
370
|
+
span_id: Ignored (API generates IDs).
|
|
371
|
+
payload: Optional initial payload sent via auto-start on exit
|
|
372
|
+
if ``start()`` is never called explicitly.
|
|
373
|
+
|
|
374
|
+
Yields:
|
|
375
|
+
SpanContext for the created span.
|
|
376
|
+
"""
|
|
377
|
+
self._ensure_initialized()
|
|
378
|
+
assert self._span_manager is not None
|
|
379
|
+
|
|
380
|
+
# Import here to avoid circular import
|
|
381
|
+
from .span_context import SpanContext
|
|
382
|
+
|
|
383
|
+
# Auto-detect parent from stack if not explicit
|
|
384
|
+
if parent_span_id is None:
|
|
385
|
+
parent_span_id = SpanContextStack.peek()
|
|
386
|
+
|
|
387
|
+
temp_id = self._span_manager.prepare(
|
|
388
|
+
instance_id=instance_id,
|
|
389
|
+
schema_name=schema_name,
|
|
390
|
+
parent_span_id=parent_span_id,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
context = SpanContext(
|
|
394
|
+
temp_id=temp_id,
|
|
395
|
+
span_manager=self._span_manager,
|
|
396
|
+
default_payload=payload,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
yield context
|
|
401
|
+
finally:
|
|
402
|
+
await context.finish()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
__all__ = ["PrefactorCoreClient"]
|
prefactor_core/config.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Configuration for prefactor-core.
|
|
2
|
+
|
|
3
|
+
This module contains configuration classes for the prefactor-core SDK.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from prefactor_http.config import HttpClientConfig
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QueueConfig(BaseModel):
|
|
18
|
+
"""Configuration for queue processing.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
num_workers: Number of concurrent worker tasks.
|
|
22
|
+
max_retries: Maximum retry attempts per failed operation.
|
|
23
|
+
retry_delay_base: Base delay for exponential backoff (seconds).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
num_workers: int = Field(default=3, ge=1, le=20)
|
|
27
|
+
max_retries: int = Field(default=3, ge=0)
|
|
28
|
+
retry_delay_base: float = Field(default=1.0, gt=0)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PrefactorCoreConfig(BaseModel):
|
|
32
|
+
"""Complete configuration for PrefactorCoreClient.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
http_config: Configuration for the HTTP client.
|
|
36
|
+
queue_config: Configuration for queue processing.
|
|
37
|
+
schema_registry: Optional schema registry for aggregating span type definitions.
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
from prefactor_core.schema_registry import SchemaRegistry
|
|
41
|
+
from prefactor_core import PrefactorCoreConfig
|
|
42
|
+
|
|
43
|
+
registry = SchemaRegistry()
|
|
44
|
+
registry.register("langchain:llm", {"type": "object"})
|
|
45
|
+
|
|
46
|
+
config = PrefactorCoreConfig(
|
|
47
|
+
http_config=HttpClientConfig(...),
|
|
48
|
+
schema_registry=registry
|
|
49
|
+
)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
http_config: HttpClientConfig
|
|
53
|
+
queue_config: QueueConfig = Field(default_factory=QueueConfig)
|
|
54
|
+
schema_registry: Any = Field(
|
|
55
|
+
default=None,
|
|
56
|
+
description="Optional SchemaRegistry for aggregating span type definitions",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
__all__ = ["QueueConfig", "PrefactorCoreConfig"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Span context stack for managing nested span relationships.
|
|
2
|
+
|
|
3
|
+
The SpanContextStack provides a stack-based context tracking system for
|
|
4
|
+
nested spans. Each async context maintains its own stack of active span IDs,
|
|
5
|
+
allowing automatic parent detection when creating new spans.
|
|
6
|
+
|
|
7
|
+
This module uses contextvars to ensure proper isolation between concurrent
|
|
8
|
+
async operations.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from contextvars import ContextVar
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
# Context variable to hold the span stack for the current async context
|
|
15
|
+
# Each async task gets its own copy of this variable
|
|
16
|
+
_current_span_stack: ContextVar[list[str]] = ContextVar("span_stack", default=[])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SpanContextStack:
|
|
20
|
+
"""Manages a stack of active span IDs within an async context.
|
|
21
|
+
|
|
22
|
+
The stack tracks the hierarchy of nested spans. The top of the stack
|
|
23
|
+
is always the current (innermost) active span, which serves as the
|
|
24
|
+
default parent for any new spans created within the same context.
|
|
25
|
+
|
|
26
|
+
This class uses contextvars to ensure that each async task maintains
|
|
27
|
+
its own independent stack, preventing interference between concurrent
|
|
28
|
+
operations.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
# Root span
|
|
32
|
+
SpanContextStack.push("span-1")
|
|
33
|
+
assert SpanContextStack.peek() == "span-1"
|
|
34
|
+
|
|
35
|
+
# Nested span (child of span-1)
|
|
36
|
+
SpanContextStack.push("span-2")
|
|
37
|
+
assert SpanContextStack.peek() == "span-2"
|
|
38
|
+
|
|
39
|
+
# Exit nested span
|
|
40
|
+
SpanContextStack.pop()
|
|
41
|
+
assert SpanContextStack.peek() == "span-1"
|
|
42
|
+
|
|
43
|
+
# Exit root span
|
|
44
|
+
SpanContextStack.pop()
|
|
45
|
+
assert SpanContextStack.peek() is None
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_stack(cls) -> list[str]:
|
|
50
|
+
"""Get the current span stack for this async context.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A list of span IDs, from outermost to innermost.
|
|
54
|
+
Returns an empty list if no spans are active.
|
|
55
|
+
"""
|
|
56
|
+
return _current_span_stack.get()
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def push(cls, span_id: str) -> None:
|
|
60
|
+
"""Push a span ID onto the stack.
|
|
61
|
+
|
|
62
|
+
This marks the span as the current (innermost) active span.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
span_id: The ID of the span to push onto the stack.
|
|
66
|
+
"""
|
|
67
|
+
stack = cls.get_stack()
|
|
68
|
+
new_stack = stack + [span_id]
|
|
69
|
+
_current_span_stack.set(new_stack)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def pop(cls) -> Optional[str]:
|
|
73
|
+
"""Pop and return the current span ID from the stack.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The span ID that was removed from the stack, or None
|
|
77
|
+
if the stack was empty.
|
|
78
|
+
"""
|
|
79
|
+
stack = cls.get_stack()
|
|
80
|
+
if not stack:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
span_id = stack[-1]
|
|
84
|
+
new_stack = stack[:-1]
|
|
85
|
+
_current_span_stack.set(new_stack)
|
|
86
|
+
return span_id
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def peek(cls) -> Optional[str]:
|
|
90
|
+
"""Get the current span ID without removing it from the stack.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The ID of the current (innermost) span, or None if no
|
|
94
|
+
spans are active in this context.
|
|
95
|
+
"""
|
|
96
|
+
stack = cls.get_stack()
|
|
97
|
+
return stack[-1] if stack else None
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def depth(cls) -> int:
|
|
101
|
+
"""Get the current nesting depth.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
The number of active spans in the stack (0 if empty).
|
|
105
|
+
"""
|
|
106
|
+
return len(cls.get_stack())
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def is_empty(cls) -> bool:
|
|
110
|
+
"""Check if the stack is empty.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if no spans are currently active.
|
|
114
|
+
"""
|
|
115
|
+
return len(cls.get_stack()) == 0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
__all__ = ["SpanContextStack"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Custom exceptions for prefactor-core."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PrefactorCoreError(Exception):
|
|
5
|
+
"""Base exception for all prefactor-core errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClientNotInitializedError(PrefactorCoreError):
|
|
11
|
+
"""Raised when attempting to use a client that hasn't been initialized."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClientAlreadyInitializedError(PrefactorCoreError):
|
|
17
|
+
"""Raised when attempting to initialize a client that's already initialized."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OperationError(PrefactorCoreError):
|
|
23
|
+
"""Raised when an operation fails to process."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str, operation_type: str | None = None) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.operation_type = operation_type
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InstanceNotFoundError(PrefactorCoreError):
|
|
31
|
+
"""Raised when an agent instance is not found."""
|
|
32
|
+
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SpanNotFoundError(PrefactorCoreError):
|
|
37
|
+
"""Raised when a span is not found."""
|
|
38
|
+
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"PrefactorCoreError",
|
|
44
|
+
"ClientNotInitializedError",
|
|
45
|
+
"ClientAlreadyInitializedError",
|
|
46
|
+
"OperationError",
|
|
47
|
+
"InstanceNotFoundError",
|
|
48
|
+
"SpanNotFoundError",
|
|
49
|
+
]
|