python-corekit 0.2.0__py3-none-any.whl → 0.4.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.
- corekit/api/application.py +47 -9
- corekit/api/lifespan.py +26 -3
- corekit/concurrency/__init__.py +2 -2
- corekit/concurrency/decorators.py +32 -5
- corekit/concurrency/thread_local.py +2 -2
- corekit/concurrency/worker.py +9 -0
- corekit/config/loader.py +42 -5
- corekit/config/settings.py +11 -1
- corekit/connections/__init__.py +7 -1
- corekit/connections/connectable.py +45 -4
- corekit/connections/redis/connection.py +53 -10
- corekit/connections/sql/__init__.py +2 -1
- corekit/connections/sql/connection.py +39 -5
- corekit/connections/sql/fields/__init__.py +2 -2
- corekit/connections/sql/fields/jsonb.py +13 -6
- corekit/connections/sql/migration/__init__.py +4 -0
- corekit/connections/sql/migration/operations.py +69 -2
- corekit/connections/sql/operations/base.py +11 -2
- corekit/connections/sql/operations/statements.py +25 -5
- corekit/connections/sql/table.py +7 -29
- corekit/crypto/__init__.py +3 -1
- corekit/crypto/constants.py +2 -2
- corekit/crypto/hasher.py +9 -4
- corekit/data/dataset.py +8 -2
- corekit/data/expressions/__init__.py +3 -3
- corekit/data/expressions/comparison.py +19 -80
- corekit/data/expressions/expression.py +0 -32
- corekit/data/expressions/operator.py +13 -28
- corekit/data/stats.py +3 -0
- corekit/decorators/exception_handling.py +36 -8
- corekit/docker/watchdog.py +50 -31
- corekit/etl/__init__.py +2 -1
- corekit/etl/connection.py +14 -12
- corekit/etl/extract/extractor.py +6 -13
- corekit/etl/orchestrator.py +19 -2
- corekit/etl/schemas.py +2 -2
- corekit/etl/transform/transformer.py +4 -1
- corekit/events/publisher.py +1 -1
- corekit/events/reader.py +26 -21
- corekit/events/sse.py +4 -1
- corekit/events/websocket.py +24 -11
- corekit/exceptions/__init__.py +24 -9
- corekit/exceptions/base.py +139 -10
- corekit/exceptions/enum.py +17 -0
- corekit/exceptions/types.py +6 -6
- corekit/files/__init__.py +2 -4
- corekit/files/base.py +15 -2
- corekit/files/enum.py +0 -5
- corekit/files/json.py +16 -2
- corekit/http/__init__.py +48 -5
- corekit/http/api.py +24 -0
- corekit/http/client.py +133 -75
- corekit/http/exceptions.py +140 -0
- corekit/http/response.py +50 -1
- corekit/http/status.py +89 -0
- corekit/http/stream.py +110 -0
- corekit/jobs/runner.py +12 -1
- corekit/jobs/task.py +23 -2
- corekit/llm/__init__.py +134 -0
- corekit/llm/client.py +179 -0
- corekit/llm/enum.py +123 -0
- corekit/llm/events.py +96 -0
- corekit/llm/messages.py +173 -0
- corekit/llm/prompts/__init__.py +19 -0
- corekit/llm/prompts/enum.py +54 -0
- corekit/llm/prompts/exceptions.py +22 -0
- corekit/llm/prompts/loader.py +139 -0
- corekit/llm/prompts/template.py +53 -0
- corekit/llm/protocols.py +65 -0
- corekit/llm/streaming.py +149 -0
- corekit/llm/tools/__init__.py +19 -0
- corekit/llm/tools/base.py +118 -0
- corekit/llm/tools/detection.py +99 -0
- corekit/llm/tools/loop.py +255 -0
- corekit/llm/tools/registry.py +103 -0
- corekit/llm/wire.py +199 -0
- corekit/log_monitor/models.py +8 -2
- corekit/log_monitor/service.py +77 -38
- corekit/notifications/base.py +18 -10
- corekit/observability/__init__.py +12 -5
- corekit/observability/benchmarkable.py +37 -5
- corekit/observability/loggable.py +21 -0
- corekit/observability/request_context.py +55 -2
- corekit/observability/timing/split.py +14 -0
- corekit/observability/timing/timer.py +33 -9
- corekit/registry/__init__.py +2 -2
- corekit/registry/registry.py +55 -14
- corekit/schemas/__init__.py +2 -1
- corekit/schemas/enum.py +22 -1
- corekit/schemas/types.py +6 -1
- corekit/schemas/version.py +58 -0
- corekit/serialization/__init__.py +2 -0
- corekit/serialization/pickle_file.py +61 -0
- corekit/serialization/serializable.py +22 -2
- corekit/serialization/serializer.py +9 -2
- corekit/utils/__init__.py +2 -1
- corekit/utils/collections.py +38 -14
- corekit/utils/payload.py +12 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/METADATA +38 -9
- python_corekit-0.4.0.dist-info/RECORD +165 -0
- corekit/constants.py +0 -45
- corekit/exceptions/http/exceptions.py +0 -37
- corekit/files/pickle.py +0 -12
- python_corekit-0.2.0.dist-info/RECORD +0 -143
- {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/WHEEL +0 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/top_level.txt +0 -0
corekit/jobs/runner.py
CHANGED
|
@@ -13,6 +13,8 @@ Enqueueing a bound method makes the queue serialize the instance, which is the
|
|
|
13
13
|
thing worth avoiding.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
+
import asyncio
|
|
17
|
+
import inspect
|
|
16
18
|
import logging
|
|
17
19
|
from typing import Any
|
|
18
20
|
|
|
@@ -31,6 +33,9 @@ def run_task(task_name: str, payload: str | bytes | None = None) -> Any:
|
|
|
31
33
|
``success`` runs after ``task_function`` returns; ``failure`` runs if it
|
|
32
34
|
raises, and the original exception is re-raised so the queue records the
|
|
33
35
|
job as failed. A hook that raises does not mask the original error.
|
|
36
|
+
``last_result`` and ``last_error`` are set on the task before the hook
|
|
37
|
+
runs. A coroutine ``task_function`` is awaited. ``timeout`` is not applied
|
|
38
|
+
here; the scheduling adapter owns that.
|
|
34
39
|
|
|
35
40
|
Args:
|
|
36
41
|
task_name: The registered name of the task to run.
|
|
@@ -48,10 +53,16 @@ def run_task(task_name: str, payload: str | bytes | None = None) -> Any:
|
|
|
48
53
|
|
|
49
54
|
try:
|
|
50
55
|
result = task.task_function(*args, **kwargs)
|
|
51
|
-
|
|
56
|
+
if inspect.iscoroutine(result):
|
|
57
|
+
result = asyncio.run(result)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
task.last_result = None
|
|
60
|
+
task.last_error = exc
|
|
52
61
|
_run_hook(task, "failure", args, kwargs)
|
|
53
62
|
raise
|
|
54
63
|
|
|
64
|
+
task.last_result = result
|
|
65
|
+
task.last_error = None
|
|
55
66
|
_run_hook(task, "success", args, kwargs)
|
|
56
67
|
return result
|
|
57
68
|
|
corekit/jobs/task.py
CHANGED
|
@@ -88,6 +88,8 @@ class Task(abc.ABC):
|
|
|
88
88
|
"""
|
|
89
89
|
self.name = name or self.task_name or type(self).__name__
|
|
90
90
|
self.logger = logging.getLogger(type(self).__module__)
|
|
91
|
+
self.last_result: Any = None
|
|
92
|
+
self.last_error: BaseException | None = None
|
|
91
93
|
|
|
92
94
|
@abc.abstractmethod
|
|
93
95
|
def task_function(self, *args: Any, **kwargs: Any) -> None:
|
|
@@ -99,12 +101,18 @@ class Task(abc.ABC):
|
|
|
99
101
|
def success(self, *args: Any, **kwargs: Any) -> None:
|
|
100
102
|
"""
|
|
101
103
|
Called after ``task_function`` returns. Override to add behaviour.
|
|
104
|
+
|
|
105
|
+
``last_result`` is set before this runs. The call signature stays
|
|
106
|
+
``(*args, **kwargs)`` so an existing override is not broken.
|
|
102
107
|
"""
|
|
103
108
|
self.logger.info(f"Task {self.name} completed successfully")
|
|
104
109
|
|
|
105
110
|
def failure(self, *args: Any, **kwargs: Any) -> None:
|
|
106
111
|
"""
|
|
107
112
|
Called when ``task_function`` raises. Override to add behaviour.
|
|
113
|
+
|
|
114
|
+
``last_error`` is set before this runs. The call signature stays
|
|
115
|
+
``(*args, **kwargs)`` so an existing override is not broken.
|
|
108
116
|
"""
|
|
109
117
|
self.logger.error(f"Task {self.name} failed")
|
|
110
118
|
|
|
@@ -119,8 +127,11 @@ class ScheduledTask(Task):
|
|
|
119
127
|
"""
|
|
120
128
|
A task that runs on a schedule.
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
``interval``, ``repeat``, ``timeout``, and ``schedule_delay`` are for the
|
|
131
|
+
adapter that talks to a scheduler. ``run_task`` does not apply ``timeout``
|
|
132
|
+
and does not sleep for ``interval`` — wrapping a still-running queue job
|
|
133
|
+
would change production workers. Copy ``schedule_delay`` per instance so
|
|
134
|
+
one task cannot mutate the shared class dict.
|
|
124
135
|
"""
|
|
125
136
|
|
|
126
137
|
interval: int | None = None
|
|
@@ -133,6 +144,16 @@ class ScheduledTask(Task):
|
|
|
133
144
|
args: list[Any] | None = None
|
|
134
145
|
kwargs: dict[str, Any] | None = None
|
|
135
146
|
|
|
147
|
+
def __init__(self, name: str | None = None) -> None:
|
|
148
|
+
super().__init__(name)
|
|
149
|
+
# Class attributes are shared. Replace them on the instance so a later
|
|
150
|
+
# in-place edit cannot change every other scheduled task.
|
|
151
|
+
self.schedule_delay = dict(self.schedule_delay)
|
|
152
|
+
if self.args is not None:
|
|
153
|
+
self.args = list(self.args)
|
|
154
|
+
if self.kwargs is not None:
|
|
155
|
+
self.kwargs = dict(self.kwargs)
|
|
156
|
+
|
|
136
157
|
def get_start_time(self) -> datetime:
|
|
137
158
|
"""
|
|
138
159
|
When the first run should happen.
|
corekit/llm/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-shaped chat primitives, tool registry, and agentic loop.
|
|
3
|
+
|
|
4
|
+
Wire format in, structured stream events out. Use ``ChatCompletionsClient``
|
|
5
|
+
against any OpenAI-compatible server (llama.cpp, vLLM, OpenAI, …):
|
|
6
|
+
|
|
7
|
+
from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
|
|
8
|
+
|
|
9
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
10
|
+
prompts = PromptLoader("/path/to/prompts")
|
|
11
|
+
tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
|
|
12
|
+
registry = ToolRegistry()
|
|
13
|
+
registry.register(MyTool())
|
|
14
|
+
loop = ToolLoop(client, registry)
|
|
15
|
+
async for event in loop.stream(tmpl.messages(text="…", style="brief", length="1 paragraph")):
|
|
16
|
+
...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from corekit.llm.client import DEFAULT_LLM_TIMEOUT, ChatCompletionsClient
|
|
20
|
+
from corekit.llm.enum import (
|
|
21
|
+
ContentPartType,
|
|
22
|
+
FinishReason,
|
|
23
|
+
Role,
|
|
24
|
+
StopReason,
|
|
25
|
+
StreamEventType,
|
|
26
|
+
ToolCallType,
|
|
27
|
+
ToolKind,
|
|
28
|
+
WireField,
|
|
29
|
+
)
|
|
30
|
+
from corekit.llm.events import (
|
|
31
|
+
DoneEvent,
|
|
32
|
+
StopEvent,
|
|
33
|
+
StreamEvent,
|
|
34
|
+
TextEvent,
|
|
35
|
+
ThinkingEvent,
|
|
36
|
+
ToolCallEvent,
|
|
37
|
+
ToolResultEvent,
|
|
38
|
+
UIComponentEvent,
|
|
39
|
+
UsageEvent,
|
|
40
|
+
)
|
|
41
|
+
from corekit.llm.messages import (
|
|
42
|
+
AssistantMessage,
|
|
43
|
+
ChatTurn,
|
|
44
|
+
Message,
|
|
45
|
+
SystemMessage,
|
|
46
|
+
ToolMessage,
|
|
47
|
+
UserMessage,
|
|
48
|
+
)
|
|
49
|
+
from corekit.llm.prompts import (
|
|
50
|
+
DEFAULT_PROMPT_VERSION,
|
|
51
|
+
PromptError,
|
|
52
|
+
PromptFileRole,
|
|
53
|
+
PromptKind,
|
|
54
|
+
PromptLoader,
|
|
55
|
+
PromptNotFoundError,
|
|
56
|
+
PromptTemplate,
|
|
57
|
+
ThinkingLevel,
|
|
58
|
+
)
|
|
59
|
+
from corekit.llm.protocols import ChatClient, MetricsRecorder
|
|
60
|
+
from corekit.llm.streaming import (
|
|
61
|
+
THINK_PATTERN,
|
|
62
|
+
ModelStreamingState,
|
|
63
|
+
ThinkingMarker,
|
|
64
|
+
ThinkingMarkerPair,
|
|
65
|
+
strip_thinking,
|
|
66
|
+
)
|
|
67
|
+
from corekit.llm.tools.base import Tool, ToolCall, ToolResult
|
|
68
|
+
from corekit.llm.tools.detection import MAX_TOOL_ITERATIONS, ToolCallLoopDetector
|
|
69
|
+
from corekit.llm.tools.loop import ToolLoop
|
|
70
|
+
from corekit.llm.tools.registry import ToolRegistry
|
|
71
|
+
from corekit.llm.wire import (
|
|
72
|
+
ChoiceDelta,
|
|
73
|
+
CompletionTurn,
|
|
74
|
+
FunctionCallDelta,
|
|
75
|
+
ToolCallAccumulator,
|
|
76
|
+
ToolCallDelta,
|
|
77
|
+
UsageInfo,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"DEFAULT_LLM_TIMEOUT",
|
|
82
|
+
"DEFAULT_PROMPT_VERSION",
|
|
83
|
+
"MAX_TOOL_ITERATIONS",
|
|
84
|
+
"THINK_PATTERN",
|
|
85
|
+
"AssistantMessage",
|
|
86
|
+
"ChatClient",
|
|
87
|
+
"ChatCompletionsClient",
|
|
88
|
+
"ChatTurn",
|
|
89
|
+
"ChoiceDelta",
|
|
90
|
+
"CompletionTurn",
|
|
91
|
+
"ContentPartType",
|
|
92
|
+
"DoneEvent",
|
|
93
|
+
"FinishReason",
|
|
94
|
+
"FunctionCallDelta",
|
|
95
|
+
"Message",
|
|
96
|
+
"MetricsRecorder",
|
|
97
|
+
"ModelStreamingState",
|
|
98
|
+
"PromptError",
|
|
99
|
+
"PromptFileRole",
|
|
100
|
+
"PromptKind",
|
|
101
|
+
"PromptLoader",
|
|
102
|
+
"PromptNotFoundError",
|
|
103
|
+
"PromptTemplate",
|
|
104
|
+
"Role",
|
|
105
|
+
"StopEvent",
|
|
106
|
+
"StopReason",
|
|
107
|
+
"StreamEvent",
|
|
108
|
+
"StreamEventType",
|
|
109
|
+
"SystemMessage",
|
|
110
|
+
"TextEvent",
|
|
111
|
+
"ThinkingEvent",
|
|
112
|
+
"ThinkingLevel",
|
|
113
|
+
"ThinkingMarker",
|
|
114
|
+
"ThinkingMarkerPair",
|
|
115
|
+
"Tool",
|
|
116
|
+
"ToolCall",
|
|
117
|
+
"ToolCallAccumulator",
|
|
118
|
+
"ToolCallDelta",
|
|
119
|
+
"ToolCallEvent",
|
|
120
|
+
"ToolCallLoopDetector",
|
|
121
|
+
"ToolCallType",
|
|
122
|
+
"ToolKind",
|
|
123
|
+
"ToolLoop",
|
|
124
|
+
"ToolMessage",
|
|
125
|
+
"ToolRegistry",
|
|
126
|
+
"ToolResult",
|
|
127
|
+
"ToolResultEvent",
|
|
128
|
+
"UIComponentEvent",
|
|
129
|
+
"UsageEvent",
|
|
130
|
+
"UsageInfo",
|
|
131
|
+
"UserMessage",
|
|
132
|
+
"WireField",
|
|
133
|
+
"strip_thinking",
|
|
134
|
+
]
|
corekit/llm/client.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-compatible chat completions client.
|
|
3
|
+
|
|
4
|
+
A thin ``BaseApiClient`` for ``/chat/completions``. Transport and status
|
|
5
|
+
handling come from the HTTP layer; streaming bodies go through
|
|
6
|
+
``SseJsonDecoder``. This module only knows the OpenAI request wire shape.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import AsyncIterator, Coroutine, Mapping, Sequence
|
|
12
|
+
from http import HTTPMethod
|
|
13
|
+
from typing import Any, Literal, overload
|
|
14
|
+
|
|
15
|
+
from corekit.concurrency.decorators import allow_sync
|
|
16
|
+
from corekit.http import BaseApiClient, BaseApiResponse, SseJsonDecoder
|
|
17
|
+
from corekit.llm.messages import ChatTurn, Message
|
|
18
|
+
|
|
19
|
+
__all__ = ["ChatCompletionsClient", "DEFAULT_LLM_TIMEOUT"]
|
|
20
|
+
|
|
21
|
+
# LLM calls can run for minutes; the HTTP default of 30s is too short.
|
|
22
|
+
DEFAULT_LLM_TIMEOUT = 600.0
|
|
23
|
+
|
|
24
|
+
# Owned by this client — sampling kwargs and friends pass through untouched.
|
|
25
|
+
_BODY_RESERVED = frozenset({"model", "messages", "stream", "stream_options", "tools", "tool_choice"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChatCompletionsClient(BaseApiClient):
|
|
29
|
+
"""
|
|
30
|
+
OpenAI-shaped chat completions over HTTP.
|
|
31
|
+
|
|
32
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
33
|
+
|
|
34
|
+
# one-shot: a dict from sync code, a coroutine inside a running loop
|
|
35
|
+
result = client.complete([UserMessage(content="hi")])
|
|
36
|
+
result = await client.complete([UserMessage(content="hi")])
|
|
37
|
+
|
|
38
|
+
# streaming is always an async iterator — there is no sync equivalent
|
|
39
|
+
async for chunk in client.complete(messages, stream=True):
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
``base_url`` should include the API root (typically ``…/v1``). Accepts
|
|
43
|
+
``Message`` instances or raw OpenAI message dicts.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
base_url: str,
|
|
49
|
+
*,
|
|
50
|
+
api_key: str | None = None,
|
|
51
|
+
model: str | None = None,
|
|
52
|
+
timeout: float = DEFAULT_LLM_TIMEOUT,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._api_key = api_key
|
|
56
|
+
self._model = model
|
|
57
|
+
# Trailing slash so urljoin keeps the ``/v1`` segment for relative paths.
|
|
58
|
+
self._api_base = base_url if base_url.endswith("/") else f"{base_url}/"
|
|
59
|
+
self._stream_decoder = SseJsonDecoder()
|
|
60
|
+
super().__init__(timeout=timeout, **kwargs)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def base_url(self) -> str:
|
|
64
|
+
return self._api_base
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def headers(self) -> dict[str, str]:
|
|
68
|
+
if not self._api_key:
|
|
69
|
+
return {}
|
|
70
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
71
|
+
|
|
72
|
+
def _resolve_model(self, model: str | None) -> str:
|
|
73
|
+
resolved = model if model is not None else self._model
|
|
74
|
+
if not resolved:
|
|
75
|
+
raise ValueError("model is required (pass model= to complete() or the constructor)")
|
|
76
|
+
return resolved
|
|
77
|
+
|
|
78
|
+
def _build_body(
|
|
79
|
+
self,
|
|
80
|
+
messages: Sequence[ChatTurn],
|
|
81
|
+
*,
|
|
82
|
+
stream: bool,
|
|
83
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
84
|
+
model: str | None = None,
|
|
85
|
+
internal: bool = False,
|
|
86
|
+
**params: Any,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
body: dict[str, Any] = {
|
|
89
|
+
"model": self._resolve_model(model),
|
|
90
|
+
"messages": Message.as_wire(messages, internal=internal),
|
|
91
|
+
"stream": stream,
|
|
92
|
+
}
|
|
93
|
+
if stream:
|
|
94
|
+
body["stream_options"] = {"include_usage": True}
|
|
95
|
+
if tools:
|
|
96
|
+
body["tools"] = list(tools)
|
|
97
|
+
body["tool_choice"] = params.pop("tool_choice", "auto")
|
|
98
|
+
for key, value in params.items():
|
|
99
|
+
if key not in _BODY_RESERVED:
|
|
100
|
+
body[key] = value
|
|
101
|
+
return body
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def complete(
|
|
105
|
+
self,
|
|
106
|
+
messages: Sequence[ChatTurn],
|
|
107
|
+
*,
|
|
108
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
109
|
+
model: str | None = None,
|
|
110
|
+
stream: Literal[False] = False,
|
|
111
|
+
internal: bool = False,
|
|
112
|
+
**kwargs: Any,
|
|
113
|
+
) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]]: ...
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def complete(
|
|
117
|
+
self,
|
|
118
|
+
messages: Sequence[ChatTurn],
|
|
119
|
+
*,
|
|
120
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
121
|
+
model: str | None = None,
|
|
122
|
+
stream: Literal[True],
|
|
123
|
+
internal: bool = False,
|
|
124
|
+
**kwargs: Any,
|
|
125
|
+
) -> AsyncIterator[dict[str, Any]]: ...
|
|
126
|
+
|
|
127
|
+
def complete(
|
|
128
|
+
self,
|
|
129
|
+
messages: Sequence[ChatTurn],
|
|
130
|
+
*,
|
|
131
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
132
|
+
model: str | None = None,
|
|
133
|
+
stream: bool = False,
|
|
134
|
+
internal: bool = False,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]] | AsyncIterator[dict[str, Any]]:
|
|
137
|
+
"""
|
|
138
|
+
POST ``/chat/completions``.
|
|
139
|
+
|
|
140
|
+
``stream=False`` (default) uses ``@allow_sync``: a script gets the
|
|
141
|
+
response dict back; inside a running event loop you must ``await`` it.
|
|
142
|
+
``stream=True`` is always an async iterator — ``@allow_sync`` only
|
|
143
|
+
unwraps one awaitable, and buffering a stream would defeat streaming.
|
|
144
|
+
"""
|
|
145
|
+
body = self._build_body(
|
|
146
|
+
messages,
|
|
147
|
+
stream=stream,
|
|
148
|
+
tools=tools,
|
|
149
|
+
model=model,
|
|
150
|
+
internal=internal,
|
|
151
|
+
**kwargs,
|
|
152
|
+
)
|
|
153
|
+
if stream:
|
|
154
|
+
return self._complete_stream(body)
|
|
155
|
+
return self._complete(body)
|
|
156
|
+
|
|
157
|
+
@allow_sync
|
|
158
|
+
async def _complete(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
159
|
+
self.info(
|
|
160
|
+
f"[ChatCompletionsClient] Completing {body['model']!r} — "
|
|
161
|
+
f"messages={len(body['messages'])}, tools={'tools' in body}"
|
|
162
|
+
)
|
|
163
|
+
response = await self.post("chat/completions", json=body)
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
if not response.data:
|
|
166
|
+
raise ValueError(f"chat/completions returned non-JSON body: {response.text!r}")
|
|
167
|
+
return dict(response.data)
|
|
168
|
+
|
|
169
|
+
async def _complete_stream(self, body: dict[str, Any]) -> AsyncIterator[dict[str, Any]]:
|
|
170
|
+
self.info(
|
|
171
|
+
f"[ChatCompletionsClient] Streaming {body['model']!r} — "
|
|
172
|
+
f"messages={len(body['messages'])}, tools={'tools' in body}"
|
|
173
|
+
)
|
|
174
|
+
async with self.stream_request(HTTPMethod.POST, "chat/completions", json=body) as response:
|
|
175
|
+
if int(response.status_code) >= 400:
|
|
176
|
+
error_body = (await response.aread()).decode("utf-8", errors="replace")
|
|
177
|
+
BaseApiResponse(status_code=response.status_code, text=error_body).raise_for_status()
|
|
178
|
+
async for chunk in self._stream_decoder.decode(response):
|
|
179
|
+
yield chunk
|
corekit/llm/enum.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wire-facing LLM enumerations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from corekit.schemas import StringEnum
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ContentPartType",
|
|
9
|
+
"FinishReason",
|
|
10
|
+
"Role",
|
|
11
|
+
"StopReason",
|
|
12
|
+
"StreamEventType",
|
|
13
|
+
"ToolCallType",
|
|
14
|
+
"ToolKind",
|
|
15
|
+
"WireField",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Role(StringEnum):
|
|
20
|
+
"""
|
|
21
|
+
OpenAI-shaped chat roles. Product bookkeeping roles (slash commands, and
|
|
22
|
+
so on) stay in the consumer.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
USER = "user"
|
|
26
|
+
SYSTEM = "system"
|
|
27
|
+
ASSISTANT = "assistant"
|
|
28
|
+
TOOL = "tool"
|
|
29
|
+
|
|
30
|
+
def is_for_llm(self) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
True when this role may be included in model context.
|
|
33
|
+
"""
|
|
34
|
+
return self in {Role.USER, Role.SYSTEM, Role.ASSISTANT, Role.TOOL}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ToolKind(StringEnum):
|
|
38
|
+
"""
|
|
39
|
+
How a tool participates in the agentic loop.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
DATA = "data" # executed server-side; result fed back into the model
|
|
43
|
+
UI = "ui" # payload for the client; halts the model loop
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StreamEventType(StringEnum):
|
|
47
|
+
"""
|
|
48
|
+
Kind of event yielded by a tool-aware streaming loop.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
TEXT = "text"
|
|
52
|
+
THINKING = "thinking"
|
|
53
|
+
TOOL_CALL = "tool_call"
|
|
54
|
+
TOOL_RESULT = "tool_result"
|
|
55
|
+
UI_COMPONENT = "ui_component"
|
|
56
|
+
USAGE = "usage"
|
|
57
|
+
DONE = "done"
|
|
58
|
+
STOP = "stop"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StopReason(StringEnum):
|
|
62
|
+
"""
|
|
63
|
+
Why a ``StopEvent`` ended the loop early.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
LOOP_DETECTED = "loop_detected"
|
|
67
|
+
MAX_ITERATIONS = "max_iterations"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FinishReason(StringEnum):
|
|
71
|
+
"""
|
|
72
|
+
OpenAI-compatible ``finish_reason`` values on a completion choice.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
STOP = "stop"
|
|
76
|
+
TOOL_CALLS = "tool_calls"
|
|
77
|
+
LENGTH = "length"
|
|
78
|
+
CONTENT_FILTER = "content_filter"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ContentPartType(StringEnum):
|
|
82
|
+
"""
|
|
83
|
+
Multimodal content-part ``type`` values in OpenAI chat messages.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
TEXT = "text"
|
|
87
|
+
IMAGE_URL = "image_url"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ToolCallType(StringEnum):
|
|
91
|
+
"""
|
|
92
|
+
OpenAI tool-call ``type`` on assistant messages.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
FUNCTION = "function"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class WireField(StringEnum):
|
|
99
|
+
"""
|
|
100
|
+
Recurring OpenAI wire field names used across messages and chunks.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
ROLE = "role"
|
|
104
|
+
CONTENT = "content"
|
|
105
|
+
TOOL_CALLS = "tool_calls"
|
|
106
|
+
TOOL_CALL_ID = "tool_call_id"
|
|
107
|
+
REASONING_CONTENT = "reasoning_content"
|
|
108
|
+
TYPE = "type"
|
|
109
|
+
ID = "id"
|
|
110
|
+
NAME = "name"
|
|
111
|
+
ARGUMENTS = "arguments"
|
|
112
|
+
FUNCTION = "function"
|
|
113
|
+
INDEX = "index"
|
|
114
|
+
CHOICES = "choices"
|
|
115
|
+
DELTA = "delta"
|
|
116
|
+
USAGE = "usage"
|
|
117
|
+
FINISH_REASON = "finish_reason"
|
|
118
|
+
PROMPT_TOKENS = "prompt_tokens"
|
|
119
|
+
COMPLETION_TOKENS = "completion_tokens"
|
|
120
|
+
TOTAL_TOKENS = "total_tokens"
|
|
121
|
+
IMAGE_URL = "image_url"
|
|
122
|
+
URL = "url"
|
|
123
|
+
TEXT = "text"
|
corekit/llm/events.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transport-agnostic stream events from a tool-aware loop.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from corekit.llm.enum import StopReason, StreamEventType
|
|
10
|
+
from corekit.llm.tools.base import ToolCall, ToolResult
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"DoneEvent",
|
|
14
|
+
"StopEvent",
|
|
15
|
+
"StreamEvent",
|
|
16
|
+
"TextEvent",
|
|
17
|
+
"ThinkingEvent",
|
|
18
|
+
"ToolCallEvent",
|
|
19
|
+
"ToolResultEvent",
|
|
20
|
+
"UIComponentEvent",
|
|
21
|
+
"UsageEvent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class StreamEvent(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
Base event yielded by the tool-aware streaming loop.
|
|
28
|
+
|
|
29
|
+
Subclasses fix ``kind``; ``payload()`` is what an SSE ``data:`` line would
|
|
30
|
+
carry (``kind`` is omitted — it belongs on the event name).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
kind: StreamEventType
|
|
34
|
+
|
|
35
|
+
def payload(self) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
JSON-serializable payload without the ``kind`` field.
|
|
38
|
+
"""
|
|
39
|
+
return self.model_dump(exclude={"kind"})
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TextEvent(StreamEvent):
|
|
43
|
+
kind: StreamEventType = StreamEventType.TEXT
|
|
44
|
+
content: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ThinkingEvent(StreamEvent):
|
|
48
|
+
kind: StreamEventType = StreamEventType.THINKING
|
|
49
|
+
content: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ToolCallEvent(StreamEvent):
|
|
53
|
+
kind: StreamEventType = StreamEventType.TOOL_CALL
|
|
54
|
+
tool_call: ToolCall
|
|
55
|
+
|
|
56
|
+
def payload(self) -> dict[str, Any]:
|
|
57
|
+
return self.tool_call.model_dump()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ToolResultEvent(StreamEvent):
|
|
61
|
+
kind: StreamEventType = StreamEventType.TOOL_RESULT
|
|
62
|
+
tool_result: ToolResult
|
|
63
|
+
|
|
64
|
+
def payload(self) -> dict[str, Any]:
|
|
65
|
+
return self.tool_result.model_dump()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UIComponentEvent(StreamEvent):
|
|
69
|
+
"""
|
|
70
|
+
Halt payload for a UI tool — shape is consumer-defined.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
kind: StreamEventType = StreamEventType.UI_COMPONENT
|
|
74
|
+
tool_call_id: str
|
|
75
|
+
name: str
|
|
76
|
+
component: dict[str, Any]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class UsageEvent(StreamEvent):
|
|
80
|
+
kind: StreamEventType = StreamEventType.USAGE
|
|
81
|
+
total_tokens: int | None = None
|
|
82
|
+
prompt_tokens: int | None = None
|
|
83
|
+
completion_tokens: int | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class StopEvent(StreamEvent):
|
|
87
|
+
kind: StreamEventType = StreamEventType.STOP
|
|
88
|
+
reason: StopReason
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DoneEvent(StreamEvent):
|
|
92
|
+
"""
|
|
93
|
+
Normal completion of a streamed turn with no further tool work.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
kind: StreamEventType = StreamEventType.DONE
|