krons 0.1.1__py3-none-any.whl → 0.2.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 (142) hide show
  1. krons/__init__.py +49 -0
  2. krons/agent/__init__.py +144 -0
  3. krons/agent/mcps/__init__.py +14 -0
  4. krons/agent/mcps/loader.py +287 -0
  5. krons/agent/mcps/wrapper.py +799 -0
  6. krons/agent/message/__init__.py +20 -0
  7. krons/agent/message/action.py +69 -0
  8. krons/agent/message/assistant.py +52 -0
  9. krons/agent/message/common.py +49 -0
  10. krons/agent/message/instruction.py +130 -0
  11. krons/agent/message/prepare_msg.py +187 -0
  12. krons/agent/message/role.py +53 -0
  13. krons/agent/message/system.py +53 -0
  14. krons/agent/operations/__init__.py +82 -0
  15. krons/agent/operations/act.py +100 -0
  16. krons/agent/operations/generate.py +145 -0
  17. krons/agent/operations/llm_reparse.py +89 -0
  18. krons/agent/operations/operate.py +247 -0
  19. krons/agent/operations/parse.py +243 -0
  20. krons/agent/operations/react.py +286 -0
  21. krons/agent/operations/specs.py +235 -0
  22. krons/agent/operations/structure.py +151 -0
  23. krons/agent/operations/utils.py +79 -0
  24. krons/agent/providers/__init__.py +17 -0
  25. krons/agent/providers/anthropic_messages.py +146 -0
  26. krons/agent/providers/claude_code.py +276 -0
  27. krons/agent/providers/gemini.py +268 -0
  28. krons/agent/providers/match.py +75 -0
  29. krons/agent/providers/oai_chat.py +174 -0
  30. krons/agent/third_party/__init__.py +2 -0
  31. krons/agent/third_party/anthropic_models.py +154 -0
  32. krons/agent/third_party/claude_code.py +682 -0
  33. krons/agent/third_party/gemini_models.py +508 -0
  34. krons/agent/third_party/openai_models.py +295 -0
  35. krons/agent/tool.py +291 -0
  36. krons/core/__init__.py +56 -74
  37. krons/core/base/__init__.py +121 -0
  38. krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
  39. krons/core/{element.py → base/element.py} +13 -5
  40. krons/core/{event.py → base/event.py} +39 -6
  41. krons/core/{eventbus.py → base/eventbus.py} +3 -1
  42. krons/core/{flow.py → base/flow.py} +11 -4
  43. krons/core/{graph.py → base/graph.py} +24 -8
  44. krons/core/{node.py → base/node.py} +44 -19
  45. krons/core/{pile.py → base/pile.py} +22 -8
  46. krons/core/{processor.py → base/processor.py} +21 -7
  47. krons/core/{progression.py → base/progression.py} +3 -1
  48. krons/{specs → core/specs}/__init__.py +0 -5
  49. krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
  50. krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
  51. krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
  52. krons/{specs → core/specs}/catalog/__init__.py +2 -2
  53. krons/{specs → core/specs}/catalog/_audit.py +2 -2
  54. krons/{specs → core/specs}/catalog/_common.py +2 -2
  55. krons/{specs → core/specs}/catalog/_content.py +4 -4
  56. krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
  57. krons/{specs → core/specs}/factory.py +5 -5
  58. krons/{specs → core/specs}/operable.py +8 -2
  59. krons/{specs → core/specs}/protocol.py +4 -2
  60. krons/{specs → core/specs}/spec.py +23 -11
  61. krons/{types → core/types}/base.py +4 -2
  62. krons/{types → core/types}/db_types.py +2 -2
  63. krons/errors.py +13 -13
  64. krons/protocols.py +9 -4
  65. krons/resource/__init__.py +89 -0
  66. krons/{services → resource}/backend.py +48 -22
  67. krons/{services → resource}/endpoint.py +28 -14
  68. krons/{services → resource}/hook.py +20 -7
  69. krons/{services → resource}/imodel.py +46 -28
  70. krons/{services → resource}/registry.py +26 -24
  71. krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
  72. krons/{services → resource}/utilities/rate_limiter.py +3 -1
  73. krons/{services → resource}/utilities/resilience.py +15 -5
  74. krons/resource/utilities/token_calculator.py +185 -0
  75. krons/session/__init__.py +12 -17
  76. krons/session/constraints.py +70 -0
  77. krons/session/exchange.py +11 -3
  78. krons/session/message.py +3 -1
  79. krons/session/registry.py +35 -0
  80. krons/session/session.py +165 -174
  81. krons/utils/__init__.py +45 -0
  82. krons/utils/_function_arg_parser.py +99 -0
  83. krons/utils/_pythonic_function_call.py +249 -0
  84. krons/utils/_to_list.py +9 -3
  85. krons/utils/_utils.py +6 -2
  86. krons/utils/concurrency/_async_call.py +4 -2
  87. krons/utils/concurrency/_errors.py +3 -1
  88. krons/utils/concurrency/_patterns.py +3 -1
  89. krons/utils/concurrency/_resource_tracker.py +6 -2
  90. krons/utils/display.py +257 -0
  91. krons/utils/fuzzy/__init__.py +6 -1
  92. krons/utils/fuzzy/_fuzzy_match.py +14 -8
  93. krons/utils/fuzzy/_string_similarity.py +3 -1
  94. krons/utils/fuzzy/_to_dict.py +3 -1
  95. krons/utils/schemas/__init__.py +26 -0
  96. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  97. krons/utils/schemas/_formatter.py +72 -0
  98. krons/utils/schemas/_minimal_yaml.py +151 -0
  99. krons/utils/schemas/_typescript.py +153 -0
  100. krons/utils/validators/__init__.py +3 -0
  101. krons/utils/validators/_validate_image_url.py +56 -0
  102. krons/work/__init__.py +126 -0
  103. krons/work/engine.py +333 -0
  104. krons/work/form.py +305 -0
  105. krons/{operations → work/operations}/__init__.py +7 -4
  106. krons/{operations → work/operations}/builder.py +1 -1
  107. krons/{enforcement → work/operations}/context.py +36 -5
  108. krons/{operations → work/operations}/flow.py +13 -5
  109. krons/{operations → work/operations}/node.py +45 -43
  110. krons/work/operations/registry.py +103 -0
  111. krons/{specs → work}/phrase.py +130 -13
  112. krons/{enforcement → work}/policy.py +3 -3
  113. krons/work/report.py +268 -0
  114. krons/work/rules/__init__.py +47 -0
  115. krons/{enforcement → work/rules}/common/boolean.py +3 -1
  116. krons/{enforcement → work/rules}/common/choice.py +9 -3
  117. krons/{enforcement → work/rules}/common/number.py +3 -1
  118. krons/{enforcement → work/rules}/common/string.py +9 -3
  119. krons/{enforcement → work/rules}/rule.py +1 -1
  120. krons/{enforcement → work/rules}/validator.py +20 -5
  121. krons/{enforcement → work}/service.py +16 -7
  122. krons/work/worker.py +266 -0
  123. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/METADATA +15 -1
  124. krons-0.2.0.dist-info/RECORD +154 -0
  125. krons/enforcement/__init__.py +0 -57
  126. krons/operations/registry.py +0 -92
  127. krons/services/__init__.py +0 -81
  128. krons-0.1.1.dist-info/RECORD +0 -101
  129. /krons/{specs → core/specs}/adapters/__init__.py +0 -0
  130. /krons/{specs → core/specs}/adapters/_utils.py +0 -0
  131. /krons/{specs → core/specs}/adapters/factory.py +0 -0
  132. /krons/{types → core/types}/__init__.py +0 -0
  133. /krons/{types → core/types}/_sentinel.py +0 -0
  134. /krons/{types → core/types}/identity.py +0 -0
  135. /krons/{services → resource}/utilities/__init__.py +0 -0
  136. /krons/{services → resource}/utilities/header_factory.py +0 -0
  137. /krons/{enforcement → work/rules}/common/__init__.py +0 -0
  138. /krons/{enforcement → work/rules}/common/mapping.py +0 -0
  139. /krons/{enforcement → work/rules}/common/model.py +0 -0
  140. /krons/{enforcement → work/rules}/registry.py +0 -0
  141. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
  142. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,174 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from types import MappingProxyType
7
+ from typing import Any
8
+
9
+ from krons.resource.backend import NormalizedResponseModel
10
+ from krons.resource.endpoint import Endpoint, EndpointConfig
11
+
12
+ __all__ = (
13
+ "OAIChatEndpoint",
14
+ "create_oai_chat",
15
+ )
16
+
17
+ _PROVIDER_MAP: MappingProxyType | None = None
18
+
19
+
20
+ def _get_providers() -> MappingProxyType:
21
+ """Get mapping of provider names to base URLs.
22
+
23
+ Returns:
24
+ Immutable mapping of provider name to base_url for OpenAI-compatible providers:
25
+ - openai: OpenAI official API
26
+ - groq: Groq fast inference
27
+ - openrouter: Multi-provider gateway
28
+ - nvidia_nim: NVIDIA Inference Microservices
29
+ """
30
+ global _PROVIDER_MAP
31
+ if _PROVIDER_MAP is not None:
32
+ return _PROVIDER_MAP
33
+
34
+ _PROVIDER_MAP = MappingProxyType(
35
+ {
36
+ "openai": ("https://api.openai.com/v1", "OPENAI_API_KEY"),
37
+ "groq": ("https://api.groq.com/openai/v1", "GROQ_API_KEY"),
38
+ "openrouter": ("https://openrouter.ai/api/v1", "OPENROUTER_API_KEY"),
39
+ "nvidia_nim": ("https://integrate.api.nvidia.com/v1", "NVIDIA_NIM_API_KEY"),
40
+ }
41
+ )
42
+ return _PROVIDER_MAP
43
+
44
+
45
+ def _ensure_request_options(config: dict) -> dict:
46
+ if config.get("request_options") is not None:
47
+ return config
48
+
49
+ from ..third_party.openai_models import OpenAIChatCompletionsRequest
50
+
51
+ return {**config, "request_options": OpenAIChatCompletionsRequest}
52
+
53
+
54
+ def create_oai_chat(
55
+ provider: str = "openai",
56
+ name: str | None = None,
57
+ base_url: str | None = None,
58
+ api_key: str | None = None,
59
+ endpoint: str = "chat/completions",
60
+ ) -> dict:
61
+ """Factory for OpenAI-compatible Chat API config.
62
+
63
+ Args:
64
+ provider: Provider name (openai, groq, openrouter, nvidia_nim)
65
+ name: Config name (default: "{provider}_chat")
66
+ base_url: Base API URL (optional, auto-detected from provider)
67
+ api_key: API key or env var name (default: "{PROVIDER}_API_KEY")
68
+ endpoint: Endpoint path
69
+ """
70
+
71
+ # Get provider base URL
72
+ base_url = base_url or _get_providers().get(provider, (None,))[0]
73
+ if base_url is None:
74
+ raise ValueError(
75
+ f"Unknown provider: {provider}. Available: {list(_get_providers().keys())}"
76
+ )
77
+
78
+ # Set default name
79
+ if name is None:
80
+ name = f"{provider}_chat"
81
+
82
+ # Set default API key env var
83
+ if api_key is None:
84
+ api_key = _get_providers().get(provider, (None, None))[1]
85
+
86
+ return {
87
+ "name": name,
88
+ "provider": provider,
89
+ "base_url": base_url,
90
+ "endpoint": endpoint,
91
+ "api_key": api_key,
92
+ }
93
+
94
+
95
+ class OAIChatEndpoint(Endpoint):
96
+ """OpenAI Chat Completions API endpoint.
97
+
98
+ Supports OpenAI and OpenAI-compatible providers.
99
+
100
+ Usage:
101
+ endpoint = OpenAIChatEndpoint()
102
+ response = await endpoint.call({
103
+ "model": "gpt-4o-mini",
104
+ "messages": [{"role": "user", "content": "Hello"}]
105
+ })
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ config: dict | EndpointConfig | None = None,
111
+ circuit_breaker: Any | None = None,
112
+ **kwargs,
113
+ ):
114
+ """Initialize with OpenAI config."""
115
+ if config is None:
116
+ # Extract provider-specific kwargs for create_oai_chat
117
+ config = create_oai_chat(
118
+ provider=kwargs.pop("provider", "openai"),
119
+ name=kwargs.pop("name", None),
120
+ base_url=kwargs.pop("base_url", None),
121
+ api_key=kwargs.pop("api_key", None),
122
+ endpoint=kwargs.pop("endpoint", "chat/completions"),
123
+ )
124
+ elif isinstance(config, EndpointConfig):
125
+ config = config.model_dump()
126
+ if not isinstance(config, dict):
127
+ raise ValueError(
128
+ "Provided config must be a dict or EndpointConfig instance"
129
+ )
130
+
131
+ if kwargs.get("request_options") is None:
132
+ config = _ensure_request_options(config)
133
+
134
+ super().__init__(config=config, circuit_breaker=circuit_breaker, **kwargs)
135
+
136
+ def normalize_response(
137
+ self, raw_response: dict[str, Any]
138
+ ) -> NormalizedResponseModel:
139
+ """Normalize OpenAI response to standard format.
140
+
141
+ Extracts:
142
+ - Text from choices[0].message.content
143
+ - Usage stats
144
+ - Model info
145
+ - Finish reason
146
+ - Tool calls (if present)
147
+ """
148
+ # Extract text from first choice
149
+ text = ""
150
+ choices = raw_response.get("choices")
151
+ if choices and len(choices) > 0:
152
+ choice = choices[0]
153
+ message = choice.get("message", {})
154
+ text = message.get("content") or ""
155
+
156
+ # Extract metadata
157
+ metadata: dict[str, Any] = {
158
+ k: raw_response[k] for k in ("model", "usage", "id") if k in raw_response
159
+ }
160
+
161
+ if choices and len(choices) > 0:
162
+ choice = choices[0]
163
+ metadata.update({k: choice[k] for k in ("finish_reason",) if k in choice})
164
+
165
+ # Extract tool calls if present
166
+ message = choice.get("message", {})
167
+ metadata.update({k: message[k] for k in ("tool_calls",) if k in message})
168
+
169
+ return NormalizedResponseModel(
170
+ status="success",
171
+ data=text,
172
+ raw_response=raw_response,
173
+ metadata=metadata,
174
+ )
@@ -0,0 +1,2 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,154 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Literal, Union
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
9
+
10
+
11
+ class TextContentBlock(BaseModel):
12
+ type: Literal["text"] = "text"
13
+ text: str
14
+ cache_control: dict | None = None
15
+
16
+
17
+ class ImageSource(BaseModel):
18
+ type: Literal["base64"] = "base64"
19
+ media_type: Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
20
+ data: str
21
+
22
+
23
+ class ImageContentBlock(BaseModel):
24
+ type: Literal["image"] = "image"
25
+ source: ImageSource
26
+
27
+
28
+ ContentBlock = Union[TextContentBlock, ImageContentBlock]
29
+
30
+
31
+ class Message(BaseModel):
32
+ role: Literal["user", "assistant"]
33
+ content: str | list[str | ContentBlock]
34
+
35
+ @field_validator("content", mode="before")
36
+ @classmethod
37
+ def validate_content(cls, v):
38
+ """Convert string content to proper format."""
39
+ if isinstance(v, str):
40
+ return v
41
+ if isinstance(v, list):
42
+ # Ensure all items are either strings or proper content blocks
43
+ result = []
44
+ for item in v:
45
+ if isinstance(item, str):
46
+ result.append({"type": "text", "text": item})
47
+ else:
48
+ result.append(item)
49
+ return result
50
+ return v
51
+
52
+
53
+ class ToolDefinition(BaseModel):
54
+ name: str = Field(..., min_length=1, max_length=64, pattern="^[a-zA-Z0-9_-]+$")
55
+ description: str | None = None
56
+ input_schema: dict
57
+
58
+
59
+ class ToolChoice(BaseModel):
60
+ type: Literal["auto", "any", "tool"]
61
+ name: str | None = None
62
+
63
+
64
+ class CreateMessageRequest(BaseModel):
65
+ """Request model for Anthropic messages API."""
66
+
67
+ model: str = Field(..., min_length=1, max_length=256)
68
+ messages: list[Message]
69
+ max_tokens: int = Field(..., ge=1)
70
+
71
+ # Optional fields
72
+ system: str | list[ContentBlock] | None = None
73
+ temperature: float | None = Field(None, ge=0, le=1)
74
+ top_p: float | None = Field(None, ge=0, le=1)
75
+ top_k: int | None = Field(None, ge=0)
76
+ stop_sequences: list[str] | None = None
77
+ stream: bool | None = False
78
+ metadata: dict | None = None
79
+ tools: list[ToolDefinition] | None = None
80
+ tool_choice: ToolChoice | dict | None = None
81
+
82
+ model_config = ConfigDict(extra="forbid")
83
+
84
+
85
+ class Usage(BaseModel):
86
+ """Token usage information."""
87
+
88
+ input_tokens: int
89
+ output_tokens: int
90
+
91
+
92
+ class ContentBlockResponse(BaseModel):
93
+ """Response content block."""
94
+
95
+ type: Literal["text"]
96
+ text: str
97
+
98
+
99
+ class CreateMessageResponse(BaseModel):
100
+ """Response model for Anthropic messages API."""
101
+
102
+ id: str
103
+ type: Literal["message"] = "message"
104
+ role: Literal["assistant"] = "assistant"
105
+ content: list[ContentBlockResponse]
106
+ model: str
107
+ stop_reason: (
108
+ None | (Literal["end_turn", "max_tokens", "stop_sequence", "tool_use"])
109
+ ) = None
110
+ stop_sequence: str | None = None
111
+ usage: Usage
112
+
113
+
114
+ # Streaming response models
115
+ class MessageStartEvent(BaseModel):
116
+ type: Literal["message_start"] = "message_start"
117
+ message: CreateMessageResponse
118
+
119
+
120
+ class ContentBlockStartEvent(BaseModel):
121
+ type: Literal["content_block_start"] = "content_block_start"
122
+ index: int
123
+ content_block: ContentBlockResponse
124
+
125
+
126
+ class ContentBlockDeltaEvent(BaseModel):
127
+ type: Literal["content_block_delta"] = "content_block_delta"
128
+ index: int
129
+ delta: dict
130
+
131
+
132
+ class ContentBlockStopEvent(BaseModel):
133
+ type: Literal["content_block_stop"] = "content_block_stop"
134
+ index: int
135
+
136
+
137
+ class MessageDeltaEvent(BaseModel):
138
+ type: Literal["message_delta"] = "message_delta"
139
+ delta: dict
140
+ usage: Usage | None = None
141
+
142
+
143
+ class MessageStopEvent(BaseModel):
144
+ type: Literal["message_stop"] = "message_stop"
145
+
146
+
147
+ StreamEvent = Union[
148
+ MessageStartEvent,
149
+ ContentBlockStartEvent,
150
+ ContentBlockDeltaEvent,
151
+ ContentBlockStopEvent,
152
+ MessageDeltaEvent,
153
+ MessageStopEvent,
154
+ ]