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.
Files changed (107) hide show
  1. corekit/api/application.py +47 -9
  2. corekit/api/lifespan.py +26 -3
  3. corekit/concurrency/__init__.py +2 -2
  4. corekit/concurrency/decorators.py +32 -5
  5. corekit/concurrency/thread_local.py +2 -2
  6. corekit/concurrency/worker.py +9 -0
  7. corekit/config/loader.py +42 -5
  8. corekit/config/settings.py +11 -1
  9. corekit/connections/__init__.py +7 -1
  10. corekit/connections/connectable.py +45 -4
  11. corekit/connections/redis/connection.py +53 -10
  12. corekit/connections/sql/__init__.py +2 -1
  13. corekit/connections/sql/connection.py +39 -5
  14. corekit/connections/sql/fields/__init__.py +2 -2
  15. corekit/connections/sql/fields/jsonb.py +13 -6
  16. corekit/connections/sql/migration/__init__.py +4 -0
  17. corekit/connections/sql/migration/operations.py +69 -2
  18. corekit/connections/sql/operations/base.py +11 -2
  19. corekit/connections/sql/operations/statements.py +25 -5
  20. corekit/connections/sql/table.py +7 -29
  21. corekit/crypto/__init__.py +3 -1
  22. corekit/crypto/constants.py +2 -2
  23. corekit/crypto/hasher.py +9 -4
  24. corekit/data/dataset.py +8 -2
  25. corekit/data/expressions/__init__.py +3 -3
  26. corekit/data/expressions/comparison.py +19 -80
  27. corekit/data/expressions/expression.py +0 -32
  28. corekit/data/expressions/operator.py +13 -28
  29. corekit/data/stats.py +3 -0
  30. corekit/decorators/exception_handling.py +36 -8
  31. corekit/docker/watchdog.py +50 -31
  32. corekit/etl/__init__.py +2 -1
  33. corekit/etl/connection.py +14 -12
  34. corekit/etl/extract/extractor.py +6 -13
  35. corekit/etl/orchestrator.py +19 -2
  36. corekit/etl/schemas.py +2 -2
  37. corekit/etl/transform/transformer.py +4 -1
  38. corekit/events/publisher.py +1 -1
  39. corekit/events/reader.py +26 -21
  40. corekit/events/sse.py +4 -1
  41. corekit/events/websocket.py +24 -11
  42. corekit/exceptions/__init__.py +24 -9
  43. corekit/exceptions/base.py +139 -10
  44. corekit/exceptions/enum.py +17 -0
  45. corekit/exceptions/types.py +6 -6
  46. corekit/files/__init__.py +2 -4
  47. corekit/files/base.py +15 -2
  48. corekit/files/enum.py +0 -5
  49. corekit/files/json.py +16 -2
  50. corekit/http/__init__.py +48 -5
  51. corekit/http/api.py +24 -0
  52. corekit/http/client.py +133 -75
  53. corekit/http/exceptions.py +140 -0
  54. corekit/http/response.py +50 -1
  55. corekit/http/status.py +89 -0
  56. corekit/http/stream.py +110 -0
  57. corekit/jobs/runner.py +12 -1
  58. corekit/jobs/task.py +23 -2
  59. corekit/llm/__init__.py +134 -0
  60. corekit/llm/client.py +179 -0
  61. corekit/llm/enum.py +123 -0
  62. corekit/llm/events.py +96 -0
  63. corekit/llm/messages.py +173 -0
  64. corekit/llm/prompts/__init__.py +19 -0
  65. corekit/llm/prompts/enum.py +54 -0
  66. corekit/llm/prompts/exceptions.py +22 -0
  67. corekit/llm/prompts/loader.py +139 -0
  68. corekit/llm/prompts/template.py +53 -0
  69. corekit/llm/protocols.py +65 -0
  70. corekit/llm/streaming.py +149 -0
  71. corekit/llm/tools/__init__.py +19 -0
  72. corekit/llm/tools/base.py +118 -0
  73. corekit/llm/tools/detection.py +99 -0
  74. corekit/llm/tools/loop.py +255 -0
  75. corekit/llm/tools/registry.py +103 -0
  76. corekit/llm/wire.py +199 -0
  77. corekit/log_monitor/models.py +8 -2
  78. corekit/log_monitor/service.py +77 -38
  79. corekit/notifications/base.py +18 -10
  80. corekit/observability/__init__.py +12 -5
  81. corekit/observability/benchmarkable.py +37 -5
  82. corekit/observability/loggable.py +21 -0
  83. corekit/observability/request_context.py +55 -2
  84. corekit/observability/timing/split.py +14 -0
  85. corekit/observability/timing/timer.py +33 -9
  86. corekit/registry/__init__.py +2 -2
  87. corekit/registry/registry.py +55 -14
  88. corekit/schemas/__init__.py +2 -1
  89. corekit/schemas/enum.py +22 -1
  90. corekit/schemas/types.py +6 -1
  91. corekit/schemas/version.py +58 -0
  92. corekit/serialization/__init__.py +2 -0
  93. corekit/serialization/pickle_file.py +61 -0
  94. corekit/serialization/serializable.py +22 -2
  95. corekit/serialization/serializer.py +9 -2
  96. corekit/utils/__init__.py +2 -1
  97. corekit/utils/collections.py +38 -14
  98. corekit/utils/payload.py +12 -0
  99. {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/METADATA +38 -9
  100. python_corekit-0.4.0.dist-info/RECORD +165 -0
  101. corekit/constants.py +0 -45
  102. corekit/exceptions/http/exceptions.py +0 -37
  103. corekit/files/pickle.py +0 -12
  104. python_corekit-0.2.0.dist-info/RECORD +0 -143
  105. {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/WHEEL +0 -0
  106. {python_corekit-0.2.0.dist-info → python_corekit-0.4.0.dist-info}/licenses/LICENSE +0 -0
  107. {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
- except Exception:
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
- The timing attributes are read by whichever adapter schedules the task;
123
- nothing here talks to a scheduler itself.
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.
@@ -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