krons 0.1.0__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 +127 -0
- krons/core/base/__init__.py +121 -0
- {kronos/core → krons/core/base}/broadcaster.py +7 -3
- {kronos/core → krons/core/base}/element.py +15 -7
- {kronos/core → krons/core/base}/event.py +41 -8
- {kronos/core → krons/core/base}/eventbus.py +4 -2
- {kronos/core → krons/core/base}/flow.py +14 -7
- {kronos/core → krons/core/base}/graph.py +27 -11
- {kronos/core → krons/core/base}/node.py +47 -22
- {kronos/core → krons/core/base}/pile.py +26 -12
- {kronos/core → krons/core/base}/processor.py +23 -9
- {kronos/core → krons/core/base}/progression.py +5 -3
- {kronos → krons/core}/specs/__init__.py +0 -5
- {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
- {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
- {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
- {kronos → krons/core}/specs/catalog/__init__.py +2 -2
- {kronos → krons/core}/specs/catalog/_audit.py +3 -3
- {kronos → krons/core}/specs/catalog/_common.py +2 -2
- {kronos → krons/core}/specs/catalog/_content.py +5 -5
- {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
- {kronos → krons/core}/specs/factory.py +7 -7
- {kronos → krons/core}/specs/operable.py +9 -3
- {kronos → krons/core}/specs/protocol.py +4 -2
- {kronos → krons/core}/specs/spec.py +25 -13
- {kronos → krons/core}/types/base.py +7 -5
- {kronos → krons/core}/types/db_types.py +2 -2
- {kronos → krons/core}/types/identity.py +1 -1
- {kronos → krons}/errors.py +13 -13
- {kronos → krons}/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- {kronos/services → krons/resource}/backend.py +50 -24
- {kronos/services → krons/resource}/endpoint.py +28 -14
- {kronos/services → krons/resource}/hook.py +22 -9
- {kronos/services → krons/resource}/imodel.py +50 -32
- {kronos/services → krons/resource}/registry.py +27 -25
- {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
- {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
- {kronos/services → krons/resource}/utilities/resilience.py +17 -7
- krons/resource/utilities/token_calculator.py +185 -0
- {kronos → krons}/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- {kronos → krons}/session/exchange.py +14 -6
- {kronos → krons}/session/message.py +4 -2
- krons/session/registry.py +35 -0
- {kronos → krons}/session/session.py +165 -174
- krons/utils/__init__.py +85 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- {kronos → krons}/utils/_to_list.py +9 -3
- {kronos → krons}/utils/_utils.py +9 -5
- {kronos → krons}/utils/concurrency/__init__.py +38 -38
- {kronos → krons}/utils/concurrency/_async_call.py +6 -4
- {kronos → krons}/utils/concurrency/_errors.py +3 -1
- {kronos → krons}/utils/concurrency/_patterns.py +3 -1
- {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- {kronos → krons}/utils/fuzzy/__init__.py +6 -1
- {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
- {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
- {kronos → 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
- {kronos → krons}/utils/sql/_sql_validation.py +1 -1
- 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
- {kronos → krons/work}/operations/__init__.py +7 -4
- {kronos → krons/work}/operations/builder.py +4 -4
- {kronos/enforcement → krons/work/operations}/context.py +37 -6
- {kronos → krons/work}/operations/flow.py +17 -9
- krons/work/operations/node.py +103 -0
- krons/work/operations/registry.py +103 -0
- {kronos/specs → krons/work}/phrase.py +131 -14
- {kronos/enforcement → krons/work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
- {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
- {kronos/enforcement → krons/work/rules}/rule.py +2 -2
- {kronos/enforcement → krons/work/rules}/validator.py +21 -6
- {kronos/enforcement → krons/work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
- krons-0.2.0.dist-info/RECORD +154 -0
- kronos/core/__init__.py +0 -145
- kronos/enforcement/__init__.py +0 -57
- kronos/operations/node.py +0 -101
- kronos/operations/registry.py +0 -92
- kronos/services/__init__.py +0 -81
- kronos/specs/adapters/__init__.py +0 -0
- kronos/utils/__init__.py +0 -40
- krons-0.1.0.dist-info/RECORD +0 -101
- {kronos → krons/core/specs/adapters}/__init__.py +0 -0
- {kronos → krons/core}/specs/adapters/_utils.py +0 -0
- {kronos → krons/core}/specs/adapters/factory.py +0 -0
- {kronos → krons/core}/types/__init__.py +0 -0
- {kronos → krons/core}/types/_sentinel.py +0 -0
- {kronos → krons}/py.typed +0 -0
- {kronos/services → krons/resource}/utilities/__init__.py +0 -0
- {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
- {kronos → krons}/utils/_hash.py +0 -0
- {kronos → krons}/utils/_json_dump.py +0 -0
- {kronos → krons}/utils/_lazy_init.py +0 -0
- {kronos → krons}/utils/_to_num.py +0 -0
- {kronos → krons}/utils/concurrency/_cancel.py +0 -0
- {kronos → krons}/utils/concurrency/_primitives.py +0 -0
- {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
- {kronos → krons}/utils/concurrency/_run_async.py +0 -0
- {kronos → krons}/utils/concurrency/_task.py +0 -0
- {kronos → krons}/utils/concurrency/_utils.py +0 -0
- {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
- {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
- {kronos → krons}/utils/sql/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
- {kronos/enforcement → krons/work/rules}/registry.py +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
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 Any
|
|
7
|
+
|
|
8
|
+
import orjson
|
|
9
|
+
import yaml # type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
__all__ = ("minimal_yaml",)
|
|
12
|
+
|
|
13
|
+
# Maximum recursion depth for pruning to prevent stack overflow
|
|
14
|
+
MAX_PRUNE_DEPTH = 100
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MinimalDumper(yaml.SafeDumper):
|
|
18
|
+
"""YAML dumper with minimal, readable settings."""
|
|
19
|
+
|
|
20
|
+
def ignore_aliases(self, data: Any) -> bool: # type: ignore[override]
|
|
21
|
+
"""Disable anchors/aliases (&id001, *id001) for repeated objects."""
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _represent_str(dumper: yaml.SafeDumper, data: str):
|
|
26
|
+
"""Use block scalars for multiline text; plain style otherwise."""
|
|
27
|
+
if "\n" in data:
|
|
28
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
|
|
29
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
MinimalDumper.add_representer(str, _represent_str)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_empty(x: Any) -> bool:
|
|
36
|
+
"""Define 'empty' for pruning. Keeps 0 and False."""
|
|
37
|
+
if x is None:
|
|
38
|
+
return True
|
|
39
|
+
if isinstance(x, str):
|
|
40
|
+
return x.strip() == ""
|
|
41
|
+
if isinstance(x, dict):
|
|
42
|
+
return len(x) == 0
|
|
43
|
+
if isinstance(x, list | tuple | set):
|
|
44
|
+
return len(x) == 0
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _prune(x: Any, *, _depth: int = 0, _max_depth: int = MAX_PRUNE_DEPTH) -> Any:
|
|
49
|
+
"""Recursively remove empty leaves and empty containers.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
x: Value to prune
|
|
53
|
+
_depth: Current recursion depth (internal use)
|
|
54
|
+
_max_depth: Maximum recursion depth (default: 100)
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Pruned value with empty containers removed
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
RecursionError: If depth exceeds _max_depth
|
|
61
|
+
"""
|
|
62
|
+
if _depth > _max_depth:
|
|
63
|
+
msg = f"Pruning depth exceeds maximum ({_max_depth})"
|
|
64
|
+
raise RecursionError(msg)
|
|
65
|
+
|
|
66
|
+
if isinstance(x, dict):
|
|
67
|
+
pruned = {
|
|
68
|
+
k: _prune(v, _depth=_depth + 1, _max_depth=_max_depth)
|
|
69
|
+
for k, v in x.items()
|
|
70
|
+
if not _is_empty(v)
|
|
71
|
+
}
|
|
72
|
+
return {k: v for k, v in pruned.items() if not _is_empty(v)}
|
|
73
|
+
if isinstance(x, list):
|
|
74
|
+
pruned_list = [
|
|
75
|
+
_prune(v, _depth=_depth + 1, _max_depth=_max_depth)
|
|
76
|
+
for v in x
|
|
77
|
+
if not _is_empty(v)
|
|
78
|
+
]
|
|
79
|
+
return [v for v in pruned_list if not _is_empty(v)]
|
|
80
|
+
if isinstance(x, tuple):
|
|
81
|
+
pruned_list = [
|
|
82
|
+
_prune(v, _depth=_depth + 1, _max_depth=_max_depth)
|
|
83
|
+
for v in x
|
|
84
|
+
if not _is_empty(v)
|
|
85
|
+
]
|
|
86
|
+
return tuple(v for v in pruned_list if not _is_empty(v))
|
|
87
|
+
if isinstance(x, set):
|
|
88
|
+
pruned_set = {
|
|
89
|
+
_prune(v, _depth=_depth + 1, _max_depth=_max_depth)
|
|
90
|
+
for v in x
|
|
91
|
+
if not _is_empty(v)
|
|
92
|
+
}
|
|
93
|
+
return {v for v in pruned_set if not _is_empty(v)}
|
|
94
|
+
return x
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def minimal_yaml(
|
|
98
|
+
value: Any,
|
|
99
|
+
*,
|
|
100
|
+
drop_empties: bool = True,
|
|
101
|
+
indent: int = 2,
|
|
102
|
+
line_width: int = 2**31 - 1,
|
|
103
|
+
sort_keys: bool = False,
|
|
104
|
+
unescape_html: bool = False,
|
|
105
|
+
) -> str:
|
|
106
|
+
"""Convert value to minimal YAML string.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
value: Value to convert (dict, list, or JSON string)
|
|
110
|
+
drop_empties: Remove empty/None values recursively (default: True)
|
|
111
|
+
indent: YAML indentation level (default: 2)
|
|
112
|
+
line_width: Maximum line width (default: unlimited)
|
|
113
|
+
sort_keys: Sort dictionary keys alphabetically (default: False)
|
|
114
|
+
unescape_html: Unescape HTML entities in output (default: False)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
YAML-formatted string
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
RecursionError: If value nesting exceeds maximum depth (100)
|
|
121
|
+
|
|
122
|
+
Security Note:
|
|
123
|
+
This function only DUMPS (outputs) YAML using SafeDumper - it does
|
|
124
|
+
not load/parse YAML, so it is not vulnerable to YAML deserialization
|
|
125
|
+
attacks. However, if unescape_html=True is used and the output is
|
|
126
|
+
later rendered in HTML without proper escaping, XSS vulnerabilities
|
|
127
|
+
may occur. Use caution with unescape_html in web contexts.
|
|
128
|
+
"""
|
|
129
|
+
# Auto-parse JSON strings for convenience (fails gracefully on invalid JSON)
|
|
130
|
+
if isinstance(value, str):
|
|
131
|
+
try:
|
|
132
|
+
value = orjson.loads(value)
|
|
133
|
+
except orjson.JSONDecodeError:
|
|
134
|
+
# Not valid JSON - treat as plain string
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
data = _prune(value) if drop_empties else value
|
|
138
|
+
str_ = yaml.dump(
|
|
139
|
+
data,
|
|
140
|
+
Dumper=MinimalDumper,
|
|
141
|
+
default_flow_style=False,
|
|
142
|
+
sort_keys=sort_keys,
|
|
143
|
+
allow_unicode=True,
|
|
144
|
+
indent=indent,
|
|
145
|
+
width=line_width,
|
|
146
|
+
)
|
|
147
|
+
if unescape_html:
|
|
148
|
+
import html
|
|
149
|
+
|
|
150
|
+
return html.unescape(str_)
|
|
151
|
+
return str_
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _type_map(json_type: str) -> str:
|
|
6
|
+
"""Map JSON Schema types to TypeScript-like types."""
|
|
7
|
+
mapping = {
|
|
8
|
+
"string": "string",
|
|
9
|
+
"integer": "int",
|
|
10
|
+
"number": "float",
|
|
11
|
+
"boolean": "bool",
|
|
12
|
+
"array": "array", # Will be handled specially
|
|
13
|
+
"object": "object",
|
|
14
|
+
"null": "null",
|
|
15
|
+
}
|
|
16
|
+
return mapping.get(json_type, json_type)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_enum_union(enum_values: list) -> str:
|
|
20
|
+
"""Format enum values as TypeScript union of literals."""
|
|
21
|
+
formatted = []
|
|
22
|
+
for val in enum_values:
|
|
23
|
+
if isinstance(val, str):
|
|
24
|
+
formatted.append(f'"{val}"')
|
|
25
|
+
elif val is None:
|
|
26
|
+
formatted.append("null")
|
|
27
|
+
else:
|
|
28
|
+
formatted.append(str(val))
|
|
29
|
+
return " | ".join(formatted)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extract_type_signature(field_spec: dict, required: bool) -> tuple[str, bool]:
|
|
33
|
+
"""Extract TypeScript-style type signature from JSON Schema field."""
|
|
34
|
+
# Handle enums first (most specific)
|
|
35
|
+
if "enum" in field_spec:
|
|
36
|
+
type_sig = _format_enum_union(field_spec["enum"])
|
|
37
|
+
# Check if enum includes null
|
|
38
|
+
has_null = None in field_spec["enum"]
|
|
39
|
+
is_optional = not required or has_null
|
|
40
|
+
return type_sig, is_optional
|
|
41
|
+
|
|
42
|
+
# Handle anyOf (unions)
|
|
43
|
+
if "anyOf" in field_spec:
|
|
44
|
+
options = field_spec["anyOf"]
|
|
45
|
+
type_parts = []
|
|
46
|
+
has_null = False
|
|
47
|
+
|
|
48
|
+
for opt in options:
|
|
49
|
+
if opt.get("type") == "null":
|
|
50
|
+
has_null = True
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
if "type" in opt:
|
|
54
|
+
if opt["type"] == "array" and "items" in opt:
|
|
55
|
+
item_type = _type_map(opt["items"].get("type", "any"))
|
|
56
|
+
if "$ref" in opt["items"]:
|
|
57
|
+
item_type = opt["items"]["$ref"].split("/")[-1]
|
|
58
|
+
type_parts.append(f"{item_type}[]")
|
|
59
|
+
else:
|
|
60
|
+
type_parts.append(_type_map(opt["type"]))
|
|
61
|
+
elif "$ref" in opt:
|
|
62
|
+
ref_name = opt["$ref"].split("/")[-1]
|
|
63
|
+
type_parts.append(ref_name)
|
|
64
|
+
elif "enum" in opt:
|
|
65
|
+
type_parts.append(_format_enum_union(opt["enum"]))
|
|
66
|
+
|
|
67
|
+
if has_null:
|
|
68
|
+
type_parts.append("null")
|
|
69
|
+
|
|
70
|
+
type_sig = " | ".join(type_parts) if type_parts else "any"
|
|
71
|
+
is_optional = not required or has_null
|
|
72
|
+
return type_sig, is_optional
|
|
73
|
+
|
|
74
|
+
# Handle arrays
|
|
75
|
+
if "type" in field_spec and field_spec["type"] == "array":
|
|
76
|
+
if "items" in field_spec:
|
|
77
|
+
items_spec = field_spec["items"]
|
|
78
|
+
if "type" in items_spec:
|
|
79
|
+
item_type = _type_map(items_spec["type"])
|
|
80
|
+
elif "$ref" in items_spec:
|
|
81
|
+
item_type = items_spec["$ref"].split("/")[-1]
|
|
82
|
+
elif "enum" in items_spec:
|
|
83
|
+
item_type = f"({_format_enum_union(items_spec['enum'])})"
|
|
84
|
+
else:
|
|
85
|
+
item_type = "any"
|
|
86
|
+
else:
|
|
87
|
+
item_type = "any"
|
|
88
|
+
type_sig = f"{item_type}[]"
|
|
89
|
+
is_optional = not required
|
|
90
|
+
return type_sig, is_optional
|
|
91
|
+
|
|
92
|
+
# Handle $ref
|
|
93
|
+
if "$ref" in field_spec:
|
|
94
|
+
ref_name = field_spec["$ref"].split("/")[-1]
|
|
95
|
+
is_optional = not required
|
|
96
|
+
return ref_name, is_optional
|
|
97
|
+
|
|
98
|
+
# Handle simple types
|
|
99
|
+
if "type" in field_spec:
|
|
100
|
+
base_type = field_spec["type"]
|
|
101
|
+
# Handle nullable simple types (type? suffix from simplification)
|
|
102
|
+
if isinstance(base_type, str) and base_type.endswith("?"):
|
|
103
|
+
type_sig = _type_map(base_type[:-1])
|
|
104
|
+
is_optional = True
|
|
105
|
+
else:
|
|
106
|
+
type_sig = _type_map(base_type)
|
|
107
|
+
is_optional = not required
|
|
108
|
+
return type_sig, is_optional
|
|
109
|
+
|
|
110
|
+
# Fallback
|
|
111
|
+
return "any", not required
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def typescript_schema(schema: dict, indent: int = 0) -> str:
|
|
115
|
+
"""Convert JSON Schema to TypeScript-style notation for optimal LLM comprehension."""
|
|
116
|
+
lines = []
|
|
117
|
+
prefix = " " * indent
|
|
118
|
+
|
|
119
|
+
if "properties" not in schema:
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
required_fields = set(schema.get("required", []))
|
|
123
|
+
|
|
124
|
+
for field_name, field_spec in schema["properties"].items():
|
|
125
|
+
is_required = field_name in required_fields
|
|
126
|
+
|
|
127
|
+
# Extract type signature
|
|
128
|
+
type_sig, is_optional = _extract_type_signature(field_spec, is_required)
|
|
129
|
+
|
|
130
|
+
# Add default value if present
|
|
131
|
+
default_str = ""
|
|
132
|
+
if "default" in field_spec:
|
|
133
|
+
default_val = field_spec["default"]
|
|
134
|
+
if isinstance(default_val, str):
|
|
135
|
+
default_str = f' = "{default_val}"'
|
|
136
|
+
elif default_val is None:
|
|
137
|
+
default_str = " = null"
|
|
138
|
+
elif isinstance(default_val, bool):
|
|
139
|
+
default_str = f" = {'true' if default_val else 'false'}"
|
|
140
|
+
else:
|
|
141
|
+
default_str = f" = {default_val}"
|
|
142
|
+
|
|
143
|
+
# Build field definition
|
|
144
|
+
optional_marker = "?" if is_optional else ""
|
|
145
|
+
field_def = f"{prefix}{field_name}{optional_marker}: {type_sig}{default_str}"
|
|
146
|
+
|
|
147
|
+
# Add description if present
|
|
148
|
+
if field_spec.get("description"):
|
|
149
|
+
field_def += f" - {field_spec['description']}"
|
|
150
|
+
|
|
151
|
+
lines.append(field_def)
|
|
152
|
+
|
|
153
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from urllib.parse import urlparse
|
|
2
|
+
|
|
3
|
+
__all__ = ("validate_image_url",)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_image_url(url: str) -> None:
|
|
7
|
+
"""Validate image URL to prevent security vulnerabilities.
|
|
8
|
+
|
|
9
|
+
Security checks:
|
|
10
|
+
- Reject null bytes (path truncation attacks)
|
|
11
|
+
- Reject file:// URLs (local file access)
|
|
12
|
+
- Reject javascript: URLs (XSS attacks)
|
|
13
|
+
- Reject data:// URLs (DoS via large embedded images)
|
|
14
|
+
- Only allow http:// and https:// schemes
|
|
15
|
+
- Validate URL format
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
url: URL to validate
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
ValueError: If URL is invalid or uses disallowed scheme
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
if not url or not isinstance(url, str):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Image URL must be non-empty string, got: {type(url).__name__}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Reject null bytes (path truncation attacks)
|
|
30
|
+
# Check both literal null bytes and percent-encoded %00
|
|
31
|
+
if "\x00" in url:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
"Image URL contains null byte - potential path truncation attack"
|
|
34
|
+
)
|
|
35
|
+
if "%00" in url.lower():
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"Image URL contains percent-encoded null byte (%00) - potential path truncation attack"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
parsed = urlparse(url)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise ValueError(f"Malformed image URL '{url}': {e}") from e
|
|
44
|
+
|
|
45
|
+
# Only allow http and https schemes
|
|
46
|
+
if parsed.scheme not in ("http", "https"):
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Image URL must use http:// or https:// scheme, got: {parsed.scheme}://"
|
|
49
|
+
f"\nRejected URL: {url}"
|
|
50
|
+
f"\nReason: Disallowed schemes (file://, javascript://, data://) pose "
|
|
51
|
+
f"security risks (local file access, XSS, DoS)"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Ensure netloc (domain) is present for http/https
|
|
55
|
+
if not parsed.netloc:
|
|
56
|
+
raise ValueError(f"Image URL missing domain: {url}")
|
krons/work/__init__.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""Work system - Declarative workflow orchestration.
|
|
5
|
+
|
|
6
|
+
Two complementary patterns at different abstraction levels:
|
|
7
|
+
|
|
8
|
+
**Report** (artifact state):
|
|
9
|
+
Declarative workflow definition via form_assignments DSL.
|
|
10
|
+
Tracks one specific job's progress through the workflow.
|
|
11
|
+
Dependencies implicit from field names.
|
|
12
|
+
|
|
13
|
+
class HiringBriefReport(Report):
|
|
14
|
+
role_classification: RoleClassification | None = None
|
|
15
|
+
strategic_context: StrategicContext | None = None
|
|
16
|
+
|
|
17
|
+
assignment: str = "job_input -> executive_summary"
|
|
18
|
+
|
|
19
|
+
form_assignments: list[str] = [
|
|
20
|
+
"classifier: job_input -> role_classification | api:fast",
|
|
21
|
+
"strategist: job_input, role_classification -> strategic_context | api:synthesis",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
**Worker** (execution capability):
|
|
25
|
+
Functional station that can execute forms.
|
|
26
|
+
Has internal DAG for retries/error handling.
|
|
27
|
+
Matches to forms via resource hints.
|
|
28
|
+
|
|
29
|
+
class ClassifierWorker(Worker):
|
|
30
|
+
@work(assignment="job_input -> role_classification")
|
|
31
|
+
async def classify(self, job_input, **kwargs):
|
|
32
|
+
return await self.llm.chat(**kwargs)
|
|
33
|
+
|
|
34
|
+
Core concepts:
|
|
35
|
+
- Phrase: Typed operation signature (inputs -> outputs)
|
|
36
|
+
- Form: Data binding + scheduling (stateful artifact)
|
|
37
|
+
- Report: Multi-step workflow declaration (stateful artifact)
|
|
38
|
+
- Worker: Execution capability (stateless station)
|
|
39
|
+
- WorkerEngine: Execution driver
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from typing import TYPE_CHECKING
|
|
45
|
+
|
|
46
|
+
# Lazy import mapping
|
|
47
|
+
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
|
48
|
+
# engine
|
|
49
|
+
"WorkerEngine": ("krons.work.engine", "WorkerEngine"),
|
|
50
|
+
"WorkerTask": ("krons.work.engine", "WorkerTask"),
|
|
51
|
+
# form
|
|
52
|
+
"Form": ("krons.work.form", "Form"),
|
|
53
|
+
"ParsedAssignment": ("krons.work.form", "ParsedAssignment"),
|
|
54
|
+
"parse_assignment": ("krons.work.form", "parse_assignment"),
|
|
55
|
+
"parse_full_assignment": ("krons.work.form", "parse_full_assignment"),
|
|
56
|
+
# phrase
|
|
57
|
+
"CrudOperation": ("krons.work.phrase", "CrudOperation"),
|
|
58
|
+
"CrudPattern": ("krons.work.phrase", "CrudPattern"),
|
|
59
|
+
"Phrase": ("krons.work.phrase", "Phrase"),
|
|
60
|
+
"phrase": ("krons.work.phrase", "phrase"),
|
|
61
|
+
# report
|
|
62
|
+
"Report": ("krons.work.report", "Report"),
|
|
63
|
+
# worker
|
|
64
|
+
"Worker": ("krons.work.worker", "Worker"),
|
|
65
|
+
"WorkConfig": ("krons.work.worker", "WorkConfig"),
|
|
66
|
+
"WorkLink": ("krons.work.worker", "WorkLink"),
|
|
67
|
+
"work": ("krons.work.worker", "work"),
|
|
68
|
+
"worklink": ("krons.work.worker", "worklink"),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_LOADED: dict[str, object] = {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def __getattr__(name: str) -> object:
|
|
75
|
+
"""Lazy import attributes on first access."""
|
|
76
|
+
if name in _LOADED:
|
|
77
|
+
return _LOADED[name]
|
|
78
|
+
|
|
79
|
+
if name in _LAZY_IMPORTS:
|
|
80
|
+
from importlib import import_module
|
|
81
|
+
|
|
82
|
+
module_name, attr_name = _LAZY_IMPORTS[name]
|
|
83
|
+
module = import_module(module_name)
|
|
84
|
+
value = getattr(module, attr_name)
|
|
85
|
+
_LOADED[name] = value
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
raise AttributeError(f"module 'krons.work' has no attribute {name!r}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def __dir__() -> list[str]:
|
|
92
|
+
"""Return all available attributes for autocomplete."""
|
|
93
|
+
return list(__all__)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# TYPE_CHECKING block for static analysis
|
|
97
|
+
if TYPE_CHECKING:
|
|
98
|
+
from krons.work.engine import WorkerEngine, WorkerTask
|
|
99
|
+
from krons.work.form import (
|
|
100
|
+
Form,
|
|
101
|
+
ParsedAssignment,
|
|
102
|
+
parse_assignment,
|
|
103
|
+
parse_full_assignment,
|
|
104
|
+
)
|
|
105
|
+
from krons.work.phrase import CrudOperation, CrudPattern, Phrase, phrase
|
|
106
|
+
from krons.work.report import Report
|
|
107
|
+
from krons.work.worker import WorkConfig, Worker, WorkLink, work, worklink
|
|
108
|
+
|
|
109
|
+
__all__ = (
|
|
110
|
+
"CrudOperation",
|
|
111
|
+
"CrudPattern",
|
|
112
|
+
"Form",
|
|
113
|
+
"ParsedAssignment",
|
|
114
|
+
"Phrase",
|
|
115
|
+
"Report",
|
|
116
|
+
"WorkConfig",
|
|
117
|
+
"WorkLink",
|
|
118
|
+
"Worker",
|
|
119
|
+
"WorkerEngine",
|
|
120
|
+
"WorkerTask",
|
|
121
|
+
"parse_assignment",
|
|
122
|
+
"parse_full_assignment",
|
|
123
|
+
"phrase",
|
|
124
|
+
"work",
|
|
125
|
+
"worklink",
|
|
126
|
+
)
|