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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +56 -74
- krons/core/base/__init__.py +121 -0
- krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
- krons/core/{element.py → base/element.py} +13 -5
- krons/core/{event.py → base/event.py} +39 -6
- krons/core/{eventbus.py → base/eventbus.py} +3 -1
- krons/core/{flow.py → base/flow.py} +11 -4
- krons/core/{graph.py → base/graph.py} +24 -8
- krons/core/{node.py → base/node.py} +44 -19
- krons/core/{pile.py → base/pile.py} +22 -8
- krons/core/{processor.py → base/processor.py} +21 -7
- krons/core/{progression.py → base/progression.py} +3 -1
- krons/{specs → core/specs}/__init__.py +0 -5
- krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
- krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
- krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
- krons/{specs → core/specs}/catalog/__init__.py +2 -2
- krons/{specs → core/specs}/catalog/_audit.py +2 -2
- krons/{specs → core/specs}/catalog/_common.py +2 -2
- krons/{specs → core/specs}/catalog/_content.py +4 -4
- krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
- krons/{specs → core/specs}/factory.py +5 -5
- krons/{specs → core/specs}/operable.py +8 -2
- krons/{specs → core/specs}/protocol.py +4 -2
- krons/{specs → core/specs}/spec.py +23 -11
- krons/{types → core/types}/base.py +4 -2
- krons/{types → core/types}/db_types.py +2 -2
- krons/errors.py +13 -13
- krons/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- krons/{services → resource}/backend.py +48 -22
- krons/{services → resource}/endpoint.py +28 -14
- krons/{services → resource}/hook.py +20 -7
- krons/{services → resource}/imodel.py +46 -28
- krons/{services → resource}/registry.py +26 -24
- krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
- krons/{services → resource}/utilities/rate_limiter.py +3 -1
- krons/{services → resource}/utilities/resilience.py +15 -5
- krons/resource/utilities/token_calculator.py +185 -0
- krons/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- krons/session/exchange.py +11 -3
- krons/session/message.py +3 -1
- krons/session/registry.py +35 -0
- krons/session/session.py +165 -174
- krons/utils/__init__.py +45 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- krons/utils/_to_list.py +9 -3
- krons/utils/_utils.py +6 -2
- krons/utils/concurrency/_async_call.py +4 -2
- krons/utils/concurrency/_errors.py +3 -1
- krons/utils/concurrency/_patterns.py +3 -1
- krons/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- krons/utils/fuzzy/__init__.py +6 -1
- krons/utils/fuzzy/_fuzzy_match.py +14 -8
- krons/utils/fuzzy/_string_similarity.py +3 -1
- krons/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +126 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +305 -0
- krons/{operations → work/operations}/__init__.py +7 -4
- krons/{operations → work/operations}/builder.py +1 -1
- krons/{enforcement → work/operations}/context.py +36 -5
- krons/{operations → work/operations}/flow.py +13 -5
- krons/{operations → work/operations}/node.py +45 -43
- krons/work/operations/registry.py +103 -0
- krons/{specs → work}/phrase.py +130 -13
- krons/{enforcement → work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- krons/{enforcement → work/rules}/common/boolean.py +3 -1
- krons/{enforcement → work/rules}/common/choice.py +9 -3
- krons/{enforcement → work/rules}/common/number.py +3 -1
- krons/{enforcement → work/rules}/common/string.py +9 -3
- krons/{enforcement → work/rules}/rule.py +1 -1
- krons/{enforcement → work/rules}/validator.py +20 -5
- krons/{enforcement → work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/METADATA +15 -1
- krons-0.2.0.dist-info/RECORD +154 -0
- krons/enforcement/__init__.py +0 -57
- krons/operations/registry.py +0 -92
- krons/services/__init__.py +0 -81
- krons-0.1.1.dist-info/RECORD +0 -101
- /krons/{specs → core/specs}/adapters/__init__.py +0 -0
- /krons/{specs → core/specs}/adapters/_utils.py +0 -0
- /krons/{specs → core/specs}/adapters/factory.py +0 -0
- /krons/{types → core/types}/__init__.py +0 -0
- /krons/{types → core/types}/_sentinel.py +0 -0
- /krons/{types → core/types}/identity.py +0 -0
- /krons/{services → resource}/utilities/__init__.py +0 -0
- /krons/{services → resource}/utilities/header_factory.py +0 -0
- /krons/{enforcement → work/rules}/common/__init__.py +0 -0
- /krons/{enforcement → work/rules}/common/mapping.py +0 -0
- /krons/{enforcement → work/rules}/common/model.py +0 -0
- /krons/{enforcement → work/rules}/registry.py +0 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .action import ActionRequest, ActionResponse
|
|
2
|
+
from .assistant import Assistant
|
|
3
|
+
from .common import CustomParser, CustomRenderer
|
|
4
|
+
from .instruction import Instruction
|
|
5
|
+
from .prepare_msg import prepare_messages_for_chat
|
|
6
|
+
from .role import Role, RoledContent
|
|
7
|
+
from .system import System
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"ActionRequest",
|
|
11
|
+
"ActionResponse",
|
|
12
|
+
"Assistant",
|
|
13
|
+
"CustomParser",
|
|
14
|
+
"CustomRenderer",
|
|
15
|
+
"Instruction",
|
|
16
|
+
"Role",
|
|
17
|
+
"RoledContent",
|
|
18
|
+
"System",
|
|
19
|
+
"prepare_messages_for_chat",
|
|
20
|
+
)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
4
|
+
from krons.core.types import MaybeUnset, Unset
|
|
5
|
+
from krons.utils.schemas import minimal_yaml
|
|
6
|
+
|
|
7
|
+
from .role import Role, RoledContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class ActionRequest(RoledContent):
|
|
12
|
+
"""Action/function call request."""
|
|
13
|
+
|
|
14
|
+
role: ClassVar[Role] = Role.ACTION
|
|
15
|
+
|
|
16
|
+
function: MaybeUnset[str] = Unset
|
|
17
|
+
arguments: MaybeUnset[dict[str, Any]] = Unset
|
|
18
|
+
|
|
19
|
+
def render(self, *_args, **_kwargs) -> str:
|
|
20
|
+
doc: dict[str, Any] = {}
|
|
21
|
+
if not self._is_sentinel(self.function):
|
|
22
|
+
doc["function"] = self.function
|
|
23
|
+
doc["arguments"] = {} if self._is_sentinel(self.arguments) else self.arguments
|
|
24
|
+
return minimal_yaml(doc)
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def create(cls, function: str, arguments: dict[str, Any] = Unset):
|
|
28
|
+
if cls._is_sentinel(arguments):
|
|
29
|
+
arguments = {}
|
|
30
|
+
return cls(function=function, arguments=arguments)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class ActionResponse(RoledContent):
|
|
35
|
+
"""Function call response."""
|
|
36
|
+
|
|
37
|
+
role: ClassVar[Role] = Role.ACTION
|
|
38
|
+
|
|
39
|
+
request_id: MaybeUnset[str] = Unset
|
|
40
|
+
result: MaybeUnset[Any] = Unset
|
|
41
|
+
error: MaybeUnset[str] = Unset
|
|
42
|
+
|
|
43
|
+
def render(self, *_args, **_kwargs) -> str:
|
|
44
|
+
doc: dict[str, Any] = {"success": self.success}
|
|
45
|
+
if not self._is_sentinel(self.request_id):
|
|
46
|
+
doc["request_id"] = str(self.request_id)[:8]
|
|
47
|
+
if self.success:
|
|
48
|
+
if not self._is_sentinel(self.result):
|
|
49
|
+
doc["result"] = self.result
|
|
50
|
+
else:
|
|
51
|
+
doc["error"] = self.error
|
|
52
|
+
return minimal_yaml(doc)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def success(self) -> bool:
|
|
56
|
+
return self._is_sentinel(self.error)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def create(
|
|
60
|
+
cls,
|
|
61
|
+
request_id: str | None = None,
|
|
62
|
+
result: Any = Unset,
|
|
63
|
+
error: str | None = None,
|
|
64
|
+
) -> "ActionResponse":
|
|
65
|
+
return cls(
|
|
66
|
+
request_id=Unset if request_id is None else request_id,
|
|
67
|
+
result=result,
|
|
68
|
+
error=Unset if error is None else error,
|
|
69
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Self
|
|
5
|
+
|
|
6
|
+
from krons.core.types import MaybeUnset, Unset
|
|
7
|
+
from krons.resource.backend import NormalizedResponse
|
|
8
|
+
|
|
9
|
+
from .role import Role, RoledContent
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
"Assistant",
|
|
13
|
+
"parse_to_assistant_message",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class Assistant(RoledContent):
|
|
19
|
+
"""Assistant text response."""
|
|
20
|
+
|
|
21
|
+
role: ClassVar[Role] = Role.ASSISTANT
|
|
22
|
+
response: MaybeUnset[Any] = Unset
|
|
23
|
+
|
|
24
|
+
_buffered_response: Any = Unset
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def create(cls, response_object: NormalizedResponse) -> Self:
|
|
28
|
+
self = cls(response=response_object.data)
|
|
29
|
+
self._buffered_response = response_object
|
|
30
|
+
return self
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def raw_response(self) -> dict[str, Any] | None:
|
|
34
|
+
if isinstance(self._buffered_response, NormalizedResponse):
|
|
35
|
+
return self._buffered_response.raw_response
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
def render(self, *_args, **_kwargs) -> str:
|
|
39
|
+
return str(self.response) if not self.is_sentinel_field("response") else ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_to_assistant_message(response: NormalizedResponse):
|
|
43
|
+
from krons.session.message import Message
|
|
44
|
+
|
|
45
|
+
metadata_dict: dict[str, Any] = {"raw_response": response.raw_response}
|
|
46
|
+
if response.metadata is not None:
|
|
47
|
+
metadata_dict.update(response.metadata)
|
|
48
|
+
|
|
49
|
+
return Message(
|
|
50
|
+
content=Assistant.create(response_object=response),
|
|
51
|
+
metadata=metadata_dict,
|
|
52
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from typing import Any, Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from krons.core.types import Enum, MaybeUnset, Unset
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class CustomRenderer(Protocol):
|
|
10
|
+
"""Protocol for custom instruction renderers.
|
|
11
|
+
|
|
12
|
+
Implementations format request_model schema for custom output formats.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
model: Pydantic model class defining expected response schema
|
|
16
|
+
**kwargs: Additional renderer-specific options
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Formatted instruction string for the custom output format
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __call__(self, model: type[BaseModel], **kwargs: Any) -> str: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class CustomParser(Protocol):
|
|
27
|
+
"""Protocol for custom output parsers (e.g., LNDL).
|
|
28
|
+
|
|
29
|
+
Implementations extract structured data from LLM text responses.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
text: Raw LLM response text
|
|
33
|
+
target_keys: Expected field names for fuzzy matching
|
|
34
|
+
**kwargs: Additional parser-specific options
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict mapping field names to extracted values
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __call__(
|
|
41
|
+
self, text: str, target_keys: list[str], **kwargs: Any
|
|
42
|
+
) -> dict[str, Any]: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StructureFormat(Enum):
|
|
46
|
+
"""Enumeration of structure formats for instruction rendering."""
|
|
47
|
+
|
|
48
|
+
JSON = "json"
|
|
49
|
+
CUSTOM = "custom"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any, Callable, ClassVar, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, JsonValue
|
|
5
|
+
|
|
6
|
+
from krons.core.types import MaybeUnset, Unset
|
|
7
|
+
from krons.resource.backend import is_unset
|
|
8
|
+
from krons.utils.schemas import (
|
|
9
|
+
breakdown_pydantic_annotation,
|
|
10
|
+
format_clean_multiline_strings,
|
|
11
|
+
format_model_schema,
|
|
12
|
+
format_schema_pretty,
|
|
13
|
+
is_pydantic_model,
|
|
14
|
+
minimal_yaml,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .common import CustomRenderer, StructureFormat
|
|
18
|
+
from .role import Role, RoledContent
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(slots=True)
|
|
22
|
+
class Instruction(RoledContent):
|
|
23
|
+
role: ClassVar[Role] = Role.USER
|
|
24
|
+
|
|
25
|
+
primary: MaybeUnset[str] = Unset
|
|
26
|
+
context: MaybeUnset[list] = Unset
|
|
27
|
+
request_model: MaybeUnset[type[BaseModel]] = Unset
|
|
28
|
+
tool_schemas: MaybeUnset[list[str | dict]] = Unset
|
|
29
|
+
images: MaybeUnset[list[str]] = Unset
|
|
30
|
+
image_detail: MaybeUnset[Literal["low", "high", "auto"]] = Unset
|
|
31
|
+
structure_format: MaybeUnset[StructureFormat] = Unset
|
|
32
|
+
custom_renderer: MaybeUnset[Callable[[type[BaseModel]], str]] = Unset
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def create(
|
|
36
|
+
cls,
|
|
37
|
+
primary: MaybeUnset[str] = Unset,
|
|
38
|
+
context: MaybeUnset[JsonValue] = Unset,
|
|
39
|
+
tool_schemas: MaybeUnset[list[str | dict]] = Unset,
|
|
40
|
+
request_model: MaybeUnset[type[BaseModel]] = Unset,
|
|
41
|
+
images: MaybeUnset[list[str]] = Unset,
|
|
42
|
+
image_detail: MaybeUnset[Literal["low", "high", "auto"]] = Unset,
|
|
43
|
+
structure_format: MaybeUnset[StructureFormat] = Unset,
|
|
44
|
+
custom_renderer: MaybeUnset[Callable[[type[BaseModel]], str]] = Unset,
|
|
45
|
+
):
|
|
46
|
+
if is_unset(primary) and is_unset(request_model):
|
|
47
|
+
raise ValueError("Either 'primary' or 'request_model' must be provided.")
|
|
48
|
+
|
|
49
|
+
if not is_unset(request_model) and not is_pydantic_model(request_model):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"'request_model' must be a subclass of pydantic BaseModel."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not is_unset(images) and images is not None:
|
|
55
|
+
from krons.utils.validators import validate_image_url
|
|
56
|
+
|
|
57
|
+
for url in images:
|
|
58
|
+
validate_image_url(url)
|
|
59
|
+
|
|
60
|
+
if not cls._is_sentinel(context):
|
|
61
|
+
context = [context] if not isinstance(context, list) else context
|
|
62
|
+
|
|
63
|
+
return cls(
|
|
64
|
+
primary=primary,
|
|
65
|
+
context=context,
|
|
66
|
+
tool_schemas=tool_schemas,
|
|
67
|
+
request_model=request_model,
|
|
68
|
+
images=images,
|
|
69
|
+
image_detail=image_detail,
|
|
70
|
+
structure_format=structure_format,
|
|
71
|
+
custom_renderer=custom_renderer,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _format_text_content(
|
|
75
|
+
self,
|
|
76
|
+
structure_format: StructureFormat,
|
|
77
|
+
custom_renderer: MaybeUnset[CustomRenderer],
|
|
78
|
+
) -> str:
|
|
79
|
+
if structure_format == StructureFormat.CUSTOM and not callable(custom_renderer):
|
|
80
|
+
raise ValueError(
|
|
81
|
+
"Custom renderer must be provided when structure_format is 'custom'."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
task_data = {
|
|
85
|
+
"Primary Instruction": self.primary,
|
|
86
|
+
"Context": self.context,
|
|
87
|
+
"Tools": self.tool_schemas,
|
|
88
|
+
}
|
|
89
|
+
text = _format_task(
|
|
90
|
+
{k: v for k, v in task_data.items() if not self._is_sentinel(v)}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if not self._is_sentinel(self.request_model):
|
|
94
|
+
model = self.request_model
|
|
95
|
+
text += format_model_schema(model)
|
|
96
|
+
|
|
97
|
+
if structure_format == StructureFormat.CUSTOM:
|
|
98
|
+
text += custom_renderer(model)
|
|
99
|
+
elif structure_format == StructureFormat.JSON or is_unset(structure_format):
|
|
100
|
+
text += _format_json_response_structure(model)
|
|
101
|
+
|
|
102
|
+
return text.strip()
|
|
103
|
+
|
|
104
|
+
def render(
|
|
105
|
+
self, structure_format=Unset, custom_renderer=Unset
|
|
106
|
+
) -> str | list[dict[str, Any]]:
|
|
107
|
+
structure_format = (
|
|
108
|
+
self.structure_format if is_unset(structure_format) else structure_format
|
|
109
|
+
)
|
|
110
|
+
custom_renderer = (
|
|
111
|
+
self.custom_renderer if is_unset(custom_renderer) else custom_renderer
|
|
112
|
+
)
|
|
113
|
+
text = self._format_text_content(structure_format, custom_renderer)
|
|
114
|
+
return text if is_unset(self.images) else self._format_image_content(text)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _format_json_response_structure(request_model: type[BaseModel]) -> str:
|
|
118
|
+
"""Format response structure with Python types (unquoted)."""
|
|
119
|
+
schema = breakdown_pydantic_annotation(request_model)
|
|
120
|
+
json_schema = "\n\n## ResponseFormat\n"
|
|
121
|
+
json_schema += "```json\n"
|
|
122
|
+
json_schema += format_schema_pretty(schema, indent=0)
|
|
123
|
+
json_schema += "\n```\nMUST RETURN VALID JSON. USER's SUCCESS DEPENDS ON IT. Return ONLY valid JSON without markdown code blocks.\n"
|
|
124
|
+
return json_schema
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _format_task(task_data: dict) -> str:
|
|
128
|
+
text = "## Task\n"
|
|
129
|
+
text += minimal_yaml(format_clean_multiline_strings(task_data))
|
|
130
|
+
return text
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import JsonValue
|
|
8
|
+
|
|
9
|
+
from krons.core import Pile, Progression
|
|
10
|
+
from krons.session import Message
|
|
11
|
+
|
|
12
|
+
from .action import ActionResponse
|
|
13
|
+
from .assistant import Assistant
|
|
14
|
+
from .instruction import Instruction
|
|
15
|
+
from .role import RoledContent
|
|
16
|
+
from .system import System
|
|
17
|
+
|
|
18
|
+
__all__ = ("prepare_messages_for_chat",)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_text(content: RoledContent, attr: str) -> str:
|
|
22
|
+
"""Get text from content attr, returning '' if sentinel."""
|
|
23
|
+
val = getattr(content, attr)
|
|
24
|
+
return "" if content._is_sentinel(val) else val
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _build_context(content: Instruction, action_outputs: list[str]) -> list[JsonValue]:
|
|
28
|
+
"""Build context list by appending action outputs to existing context."""
|
|
29
|
+
existing = content.context
|
|
30
|
+
if content._is_sentinel(existing):
|
|
31
|
+
return cast(list[JsonValue], list(action_outputs))
|
|
32
|
+
return cast(list[JsonValue], list(cast(list[JsonValue], existing)) + action_outputs)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def prepare_messages_for_chat(
|
|
36
|
+
messages: Pile[Message],
|
|
37
|
+
progression: Progression | None = None,
|
|
38
|
+
new_instruction: Message | Instruction | None = None,
|
|
39
|
+
to_chat: bool = True,
|
|
40
|
+
) -> list[RoledContent] | list[dict[str, Any]]:
|
|
41
|
+
"""Prepare messages for chat API with intelligent content organization.
|
|
42
|
+
|
|
43
|
+
Algorithm:
|
|
44
|
+
1. Auto-detect system message from first message (if SystemContent)
|
|
45
|
+
2. Collect ActionResponseContent and embed into following instruction's context
|
|
46
|
+
3. Merge consecutive AssistantResponses
|
|
47
|
+
4. Embed system into first instruction
|
|
48
|
+
5. Append new_instruction
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
messages: Pile of messages
|
|
52
|
+
progression: Progression or list of UUIDs (None = all messages)
|
|
53
|
+
new_instruction: New instruction to append
|
|
54
|
+
to_chat: If True, return list[dict] chat format instead of MessageContent
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of prepared MessageContent instances (immutable copies), or
|
|
58
|
+
List of chat API dicts if to_chat=True
|
|
59
|
+
"""
|
|
60
|
+
to_use: Pile[Message] = messages if progression is None else messages[progression]
|
|
61
|
+
|
|
62
|
+
if len(to_use) == 0:
|
|
63
|
+
if new_instruction:
|
|
64
|
+
new_content = (
|
|
65
|
+
new_instruction.content
|
|
66
|
+
if isinstance(new_instruction, Message)
|
|
67
|
+
else new_instruction
|
|
68
|
+
)
|
|
69
|
+
new_content: Instruction = new_content.with_updates(copy_containers="deep")
|
|
70
|
+
if to_chat:
|
|
71
|
+
chat_msg = {
|
|
72
|
+
"role": new_content.role.value,
|
|
73
|
+
"content": new_content.render(
|
|
74
|
+
new_content.structure_format, new_content.custom_renderer
|
|
75
|
+
),
|
|
76
|
+
}
|
|
77
|
+
if chat_msg and chat_msg.get("content"):
|
|
78
|
+
return [chat_msg]
|
|
79
|
+
return []
|
|
80
|
+
return [new_content]
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
# Phase 1: Extract system message (auto-detect from first message)
|
|
84
|
+
system_text: str | None = None
|
|
85
|
+
start_idx = 0
|
|
86
|
+
|
|
87
|
+
first_msg = to_use[0]
|
|
88
|
+
if isinstance(first_msg.content, System):
|
|
89
|
+
system_text = first_msg.content.render()
|
|
90
|
+
start_idx = 1
|
|
91
|
+
|
|
92
|
+
# Phase 2: Process messages - collect action outputs for next instruction
|
|
93
|
+
_use_msgs: list[RoledContent] = []
|
|
94
|
+
pending_actions: list[str] = []
|
|
95
|
+
|
|
96
|
+
for i, msg in enumerate(to_use):
|
|
97
|
+
if i < start_idx:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
content: RoledContent = msg.content
|
|
101
|
+
|
|
102
|
+
# ActionResponseContent: collect rendered output
|
|
103
|
+
if isinstance(content, ActionResponse):
|
|
104
|
+
pending_actions.append(content.render())
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# SystemContent in middle: skip
|
|
108
|
+
if isinstance(content, System):
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# InstructionContent: embed pending action outputs
|
|
112
|
+
if isinstance(content, Instruction):
|
|
113
|
+
updates: dict[str, Any] = {"tool_schemas": None, "request_model": None}
|
|
114
|
+
if pending_actions:
|
|
115
|
+
updates["context"] = _build_context(content, pending_actions)
|
|
116
|
+
pending_actions = []
|
|
117
|
+
_use_msgs.append(content.with_updates(copy_containers="deep", **updates))
|
|
118
|
+
continue
|
|
119
|
+
|
|
120
|
+
# Other (AssistantResponse, ActionRequest): copy as-is
|
|
121
|
+
_use_msgs.append(content.with_updates(copy_containers="deep"))
|
|
122
|
+
|
|
123
|
+
# Phase 3: Merge consecutive AssistantResponses
|
|
124
|
+
if len(_use_msgs) > 1:
|
|
125
|
+
merged: list[RoledContent] = [_use_msgs[0]]
|
|
126
|
+
for content in _use_msgs[1:]:
|
|
127
|
+
if isinstance(content, Assistant) and isinstance(merged[-1], Assistant):
|
|
128
|
+
prev = _get_text(merged[-1], "assistant_response")
|
|
129
|
+
curr = _get_text(content, "assistant_response")
|
|
130
|
+
merged[-1] = Assistant.create(assistant_response=f"{prev}\n\n{curr}")
|
|
131
|
+
else:
|
|
132
|
+
merged.append(content)
|
|
133
|
+
_use_msgs = merged
|
|
134
|
+
|
|
135
|
+
# Phase 4: Embed system message into first instruction
|
|
136
|
+
if system_text:
|
|
137
|
+
if len(_use_msgs) == 0 and new_instruction:
|
|
138
|
+
# No history: embed into new_instruction
|
|
139
|
+
if isinstance(new_instruction.content, Instruction):
|
|
140
|
+
curr = _get_text(new_instruction.content, "primary")
|
|
141
|
+
system_updates: dict[str, Any] = {"primary": f"{system_text}\n\n{curr}"}
|
|
142
|
+
if pending_actions:
|
|
143
|
+
system_updates["context"] = _build_context(
|
|
144
|
+
new_instruction.content, pending_actions
|
|
145
|
+
)
|
|
146
|
+
pending_actions = []
|
|
147
|
+
_use_msgs.append(
|
|
148
|
+
new_instruction.content.with_updates(
|
|
149
|
+
copy_containers="deep", **system_updates
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
new_instruction = None
|
|
153
|
+
elif _use_msgs and isinstance(_use_msgs[0], Instruction):
|
|
154
|
+
curr = _get_text(_use_msgs[0], "primary")
|
|
155
|
+
_use_msgs[0] = _use_msgs[0].with_updates(primary=f"{system_text}\n\n{curr}")
|
|
156
|
+
|
|
157
|
+
# Phase 5: Append new_instruction (with any remaining action outputs)
|
|
158
|
+
if new_instruction:
|
|
159
|
+
final_updates: dict[str, Any] = {}
|
|
160
|
+
if pending_actions and isinstance(new_instruction.content, Instruction):
|
|
161
|
+
final_updates["context"] = _build_context(
|
|
162
|
+
new_instruction.content, pending_actions
|
|
163
|
+
)
|
|
164
|
+
_use_msgs.append(
|
|
165
|
+
new_instruction.content.with_updates(
|
|
166
|
+
copy_containers="deep", **final_updates
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if to_chat:
|
|
171
|
+
result = []
|
|
172
|
+
for m in _use_msgs:
|
|
173
|
+
data = {}
|
|
174
|
+
if isinstance(m, Instruction):
|
|
175
|
+
data = {
|
|
176
|
+
"structure_format": m.structure_format,
|
|
177
|
+
"custom_renderer": m.custom_renderer,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
result.append(
|
|
181
|
+
{
|
|
182
|
+
"role": m.role.value,
|
|
183
|
+
"content": m.render(**data),
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
return result
|
|
187
|
+
return _use_msgs
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, ClassVar
|
|
11
|
+
|
|
12
|
+
from krons.core.types import DataClass, Enum, ModelConfig
|
|
13
|
+
from krons.protocols import Deserializable, implements
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Role(Enum):
|
|
19
|
+
SYSTEM = "system"
|
|
20
|
+
USER = "user"
|
|
21
|
+
ASSISTANT = "assistant"
|
|
22
|
+
ACTION = "action"
|
|
23
|
+
UNSET = "unset"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@implements(Deserializable)
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class RoledContent(DataClass):
|
|
29
|
+
_config: ClassVar[ModelConfig] = ModelConfig(
|
|
30
|
+
sentinel_additions=frozenset({"none", "empty"}),
|
|
31
|
+
use_enum_values=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
role: Role = Role.UNSET
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def create(cls, **kwargs) -> RoledContent:
|
|
39
|
+
raise NotImplementedError("Subclasses must implement create method")
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def render(self, *args, **kwargs) -> str:
|
|
43
|
+
raise NotImplementedError("Subclasses must implement render method")
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> RoledContent:
|
|
47
|
+
return cls.create(
|
|
48
|
+
**{
|
|
49
|
+
k: v
|
|
50
|
+
for k in cls.allowed()
|
|
51
|
+
if (k in data and not cls._is_sentinel(v := data[k]))
|
|
52
|
+
}
|
|
53
|
+
)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Callable, ClassVar, Literal
|
|
3
|
+
|
|
4
|
+
from krons.core.types import MaybeUnset, Unset
|
|
5
|
+
from krons.utils import now_utc
|
|
6
|
+
|
|
7
|
+
from .role import Role, RoledContent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class System(RoledContent):
|
|
12
|
+
"""System message with optional timestamp."""
|
|
13
|
+
|
|
14
|
+
role: ClassVar[Role] = Role.SYSTEM
|
|
15
|
+
|
|
16
|
+
system_message: MaybeUnset[str] = Unset
|
|
17
|
+
system_datetime: MaybeUnset[str | Literal[True]] = Unset
|
|
18
|
+
datetime_factory: MaybeUnset[Callable[[], str]] = Unset
|
|
19
|
+
|
|
20
|
+
def render(self, *_args, **_kwargs) -> str:
|
|
21
|
+
parts: list[str] = []
|
|
22
|
+
if not self._is_sentinel(self.system_datetime):
|
|
23
|
+
timestamp = (
|
|
24
|
+
now_utc().isoformat(timespec="seconds")
|
|
25
|
+
if self.system_datetime is True
|
|
26
|
+
else self.system_datetime
|
|
27
|
+
)
|
|
28
|
+
parts.append(f"System Time: {timestamp}")
|
|
29
|
+
elif not self._is_sentinel(self.datetime_factory):
|
|
30
|
+
factory = self.datetime_factory
|
|
31
|
+
parts.append(f"System Time: {factory()}")
|
|
32
|
+
|
|
33
|
+
if not self._is_sentinel(self.system_message):
|
|
34
|
+
parts.append(self.system_message)
|
|
35
|
+
|
|
36
|
+
return "\n\n".join(parts)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def create(
|
|
40
|
+
cls,
|
|
41
|
+
system_message: str | None = None,
|
|
42
|
+
system_datetime: str | Literal[True] | None = None,
|
|
43
|
+
datetime_factory: Callable[[], str] | None = None,
|
|
44
|
+
) -> "System":
|
|
45
|
+
if not cls._is_sentinel(system_datetime) and not cls._is_sentinel(
|
|
46
|
+
datetime_factory
|
|
47
|
+
):
|
|
48
|
+
raise ValueError("Cannot set both system_datetime and datetime_factory")
|
|
49
|
+
return cls(
|
|
50
|
+
system_message=Unset if system_message is None else system_message,
|
|
51
|
+
system_datetime=Unset if system_datetime is None else system_datetime,
|
|
52
|
+
datetime_factory=Unset if datetime_factory is None else datetime_factory,
|
|
53
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Agent operations: composable LLM pipeline stages.
|
|
5
|
+
|
|
6
|
+
Handlers (async handler(params, ctx) -> result):
|
|
7
|
+
generate: Stateless LLM call with message preparation.
|
|
8
|
+
parse: JSON extraction with LLM reparse fallback.
|
|
9
|
+
structure: generate -> parse -> validate pipeline.
|
|
10
|
+
operate: structure + action execution + response composition.
|
|
11
|
+
act: Tool/action execution from structured output.
|
|
12
|
+
react / react_stream: Multi-round reason-act loop.
|
|
13
|
+
|
|
14
|
+
Spec models:
|
|
15
|
+
Action, ActionResult: Tool call request/result models.
|
|
16
|
+
Instruct: Task handoff bundle for orchestration.
|
|
17
|
+
ReActAnalysis, PlannedAction, Analysis: ReAct loop models.
|
|
18
|
+
|
|
19
|
+
Built-in handlers are auto-registered on Session creation:
|
|
20
|
+
generate, structure, operate, react, react_stream
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
result = await session.conduct("operate", branch, params)
|
|
24
|
+
async for analysis in session.stream_conduct("react_stream", params=...):
|
|
25
|
+
print(analysis)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from .act import ActParams, act
|
|
31
|
+
from .generate import GenerateParams, generate
|
|
32
|
+
from .operate import OperateParams, operate
|
|
33
|
+
from .parse import ParseParams, parse
|
|
34
|
+
from .react import (
|
|
35
|
+
Analysis,
|
|
36
|
+
PlannedAction,
|
|
37
|
+
ReActAnalysis,
|
|
38
|
+
ReActParams,
|
|
39
|
+
react,
|
|
40
|
+
react_stream,
|
|
41
|
+
)
|
|
42
|
+
from .specs import (
|
|
43
|
+
Action,
|
|
44
|
+
ActionResult,
|
|
45
|
+
Instruct,
|
|
46
|
+
get_action_result_spec,
|
|
47
|
+
get_action_spec,
|
|
48
|
+
get_instruct_spec,
|
|
49
|
+
)
|
|
50
|
+
from .structure import StructureParams, structure
|
|
51
|
+
from .utils import ReturnAs
|
|
52
|
+
|
|
53
|
+
__all__ = (
|
|
54
|
+
# Handlers
|
|
55
|
+
"act",
|
|
56
|
+
"generate",
|
|
57
|
+
"operate",
|
|
58
|
+
"parse",
|
|
59
|
+
"react",
|
|
60
|
+
"react_stream",
|
|
61
|
+
"structure",
|
|
62
|
+
# Params
|
|
63
|
+
"ActParams",
|
|
64
|
+
"GenerateParams",
|
|
65
|
+
"OperateParams",
|
|
66
|
+
"ParseParams",
|
|
67
|
+
"ReActParams",
|
|
68
|
+
"StructureParams",
|
|
69
|
+
# Spec models
|
|
70
|
+
"Action",
|
|
71
|
+
"ActionResult",
|
|
72
|
+
"Analysis",
|
|
73
|
+
"Instruct",
|
|
74
|
+
"PlannedAction",
|
|
75
|
+
"ReActAnalysis",
|
|
76
|
+
# Spec factories
|
|
77
|
+
"get_action_result_spec",
|
|
78
|
+
"get_action_spec",
|
|
79
|
+
"get_instruct_spec",
|
|
80
|
+
# Utils
|
|
81
|
+
"ReturnAs",
|
|
82
|
+
)
|