kinenix 0.1.0b1__tar.gz
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.
- kinenix-0.1.0b1/LICENSE +21 -0
- kinenix-0.1.0b1/PKG-INFO +73 -0
- kinenix-0.1.0b1/README.md +44 -0
- kinenix-0.1.0b1/kinenix/__init__.py +5 -0
- kinenix-0.1.0b1/kinenix/actions/__init__.py +23 -0
- kinenix-0.1.0b1/kinenix/actions/ai_ollama.py +156 -0
- kinenix-0.1.0b1/kinenix/actions/ai_systemone.py +201 -0
- kinenix-0.1.0b1/kinenix/actions/base.py +20 -0
- kinenix-0.1.0b1/kinenix/actions/data_excel.py +143 -0
- kinenix-0.1.0b1/kinenix/actions/email_smtp.py +131 -0
- kinenix-0.1.0b1/kinenix/actions/file_system.py +121 -0
- kinenix-0.1.0b1/kinenix/actions/flow_control.py +37 -0
- kinenix-0.1.0b1/kinenix/actions/http_api.py +73 -0
- kinenix-0.1.0b1/kinenix/actions/logic.py +59 -0
- kinenix-0.1.0b1/kinenix/actions/registry.py +32 -0
- kinenix-0.1.0b1/kinenix/actions/web_playwright.py +537 -0
- kinenix-0.1.0b1/kinenix/cli.py +345 -0
- kinenix-0.1.0b1/kinenix/engine/__init__.py +13 -0
- kinenix-0.1.0b1/kinenix/engine/evaluator.py +55 -0
- kinenix-0.1.0b1/kinenix/engine/interpreter.py +796 -0
- kinenix-0.1.0b1/kinenix/engine/logger.py +155 -0
- kinenix-0.1.0b1/kinenix/engine/markdown.py +544 -0
- kinenix-0.1.0b1/kinenix/models/__init__.py +17 -0
- kinenix-0.1.0b1/kinenix/models/context.py +55 -0
- kinenix-0.1.0b1/kinenix/models/flow.py +46 -0
- kinenix-0.1.0b1/kinenix.egg-info/PKG-INFO +73 -0
- kinenix-0.1.0b1/kinenix.egg-info/SOURCES.txt +39 -0
- kinenix-0.1.0b1/kinenix.egg-info/dependency_links.txt +1 -0
- kinenix-0.1.0b1/kinenix.egg-info/entry_points.txt +2 -0
- kinenix-0.1.0b1/kinenix.egg-info/requires.txt +8 -0
- kinenix-0.1.0b1/kinenix.egg-info/top_level.txt +1 -0
- kinenix-0.1.0b1/pyproject.toml +48 -0
- kinenix-0.1.0b1/setup.cfg +4 -0
- kinenix-0.1.0b1/tests/test_ai_action.py +120 -0
- kinenix-0.1.0b1/tests/test_ai_systemone.py +249 -0
- kinenix-0.1.0b1/tests/test_engine.py +788 -0
- kinenix-0.1.0b1/tests/test_markdown.py +220 -0
- kinenix-0.1.0b1/tests/test_new_actions.py +413 -0
- kinenix-0.1.0b1/tests/test_resilience.py +261 -0
- kinenix-0.1.0b1/tests/test_telemetry_auth.py +45 -0
- kinenix-0.1.0b1/tests/test_zombie_cleanup.py +237 -0
kinenix-0.1.0b1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 arttopix / Kinenix
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kinenix-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kinenix
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Core execution engine and standard action libraries for Kinenix RPA platform
|
|
5
|
+
Author: arttopix
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/arttopix/Kinenix
|
|
8
|
+
Project-URL: Documentation, https://github.com/arttopix/Kinenix/tree/main/docs
|
|
9
|
+
Project-URL: Issues, https://github.com/arttopix/Kinenix/issues
|
|
10
|
+
Keywords: rpa,automation,playwright,workflow,local-ai
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Topic :: Office/Business
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: pydantic>=2.0.0
|
|
22
|
+
Requires-Dist: playwright>=1.40.0
|
|
23
|
+
Requires-Dist: pandas>=2.0.0
|
|
24
|
+
Requires-Dist: openpyxl>=3.1.0
|
|
25
|
+
Requires-Dist: requests>=2.28.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# kinenix
|
|
31
|
+
|
|
32
|
+
Core execution engine and `kinenix` CLI for **Kinenix**, an open-source, local AI-native RPA framework in Python.
|
|
33
|
+
|
|
34
|
+
> **Status: beta.** Flow schemas and action parameters may still change between releases.
|
|
35
|
+
|
|
36
|
+
- Runs automation flows written as `flow.json` or `flow.md`
|
|
37
|
+
- Built-in actions for web automation (Playwright), Excel and CSV (no Microsoft Office required), files, HTTP, email, and flow control
|
|
38
|
+
- Local AI actions through Ollama, so business data stays on your machine
|
|
39
|
+
- Retries, fallbacks, failure screenshots, and structured JSON execution logs
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install kinenix
|
|
45
|
+
kinenix install-browsers # Playwright Chromium, needed for web actions
|
|
46
|
+
kinenix --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.10 or newer.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
kinenix list # list flows found in the current project
|
|
55
|
+
kinenix run path/to/flow.json # run a flow
|
|
56
|
+
kinenix compile path/to/flow.md # compile flow.md into flow.json
|
|
57
|
+
kinenix version # version and environment details
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Related packages
|
|
61
|
+
|
|
62
|
+
The worker, Studio, and Orchestrator modules live in the same repository and are not yet published on PyPI.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [Project README](https://github.com/arttopix/Kinenix#readme)
|
|
67
|
+
- [Actions reference](https://github.com/arttopix/Kinenix/blob/main/docs/actions_reference.md)
|
|
68
|
+
- [Flow Markdown specification](https://github.com/arttopix/Kinenix/blob/main/docs/flow_markdown_spec.md)
|
|
69
|
+
- [CLI guide](https://github.com/arttopix/Kinenix/blob/main/docs/cli_guide.md)
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# kinenix
|
|
2
|
+
|
|
3
|
+
Core execution engine and `kinenix` CLI for **Kinenix**, an open-source, local AI-native RPA framework in Python.
|
|
4
|
+
|
|
5
|
+
> **Status: beta.** Flow schemas and action parameters may still change between releases.
|
|
6
|
+
|
|
7
|
+
- Runs automation flows written as `flow.json` or `flow.md`
|
|
8
|
+
- Built-in actions for web automation (Playwright), Excel and CSV (no Microsoft Office required), files, HTTP, email, and flow control
|
|
9
|
+
- Local AI actions through Ollama, so business data stays on your machine
|
|
10
|
+
- Retries, fallbacks, failure screenshots, and structured JSON execution logs
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install kinenix
|
|
16
|
+
kinenix install-browsers # Playwright Chromium, needed for web actions
|
|
17
|
+
kinenix --version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10 or newer.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
kinenix list # list flows found in the current project
|
|
26
|
+
kinenix run path/to/flow.json # run a flow
|
|
27
|
+
kinenix compile path/to/flow.md # compile flow.md into flow.json
|
|
28
|
+
kinenix version # version and environment details
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Related packages
|
|
32
|
+
|
|
33
|
+
The worker, Studio, and Orchestrator modules live in the same repository and are not yet published on PyPI.
|
|
34
|
+
|
|
35
|
+
## Documentation
|
|
36
|
+
|
|
37
|
+
- [Project README](https://github.com/arttopix/Kinenix#readme)
|
|
38
|
+
- [Actions reference](https://github.com/arttopix/Kinenix/blob/main/docs/actions_reference.md)
|
|
39
|
+
- [Flow Markdown specification](https://github.com/arttopix/Kinenix/blob/main/docs/flow_markdown_spec.md)
|
|
40
|
+
- [CLI guide](https://github.com/arttopix/Kinenix/blob/main/docs/cli_guide.md)
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Actions registry and standard action implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .base import BaseAction
|
|
6
|
+
from .registry import ActionRegistry, register_action
|
|
7
|
+
from . import logic, data_excel, web_playwright, http_api, flow_control, email_smtp, ai_ollama, file_system, ai_systemone
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseAction",
|
|
11
|
+
"ActionRegistry",
|
|
12
|
+
"register_action",
|
|
13
|
+
"logic",
|
|
14
|
+
"data_excel",
|
|
15
|
+
"web_playwright",
|
|
16
|
+
"http_api",
|
|
17
|
+
"flow_control",
|
|
18
|
+
"email_smtp",
|
|
19
|
+
"ai_ollama",
|
|
20
|
+
"file_system",
|
|
21
|
+
"ai_systemone",
|
|
22
|
+
]
|
|
23
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .base import BaseAction
|
|
10
|
+
from .registry import register_action
|
|
11
|
+
from ..models.context import ExecutionContext
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("kinenix")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_file_path(file_path: str, context: ExecutionContext) -> Path:
|
|
17
|
+
target_path = Path(file_path)
|
|
18
|
+
if not target_path.is_absolute():
|
|
19
|
+
flow_dir_str = context.get_variable("__flow_dir__")
|
|
20
|
+
if flow_dir_str:
|
|
21
|
+
target_path = Path(flow_dir_str) / file_path
|
|
22
|
+
return target_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _clean_json_text(text: str) -> str:
|
|
26
|
+
"""
|
|
27
|
+
Strips markdown code fences (e.g. ```json ... ```) from LLM output.
|
|
28
|
+
"""
|
|
29
|
+
stripped = text.strip()
|
|
30
|
+
match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", stripped, re.IGNORECASE)
|
|
31
|
+
if match:
|
|
32
|
+
return match.group(1).strip()
|
|
33
|
+
return stripped
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register_action("ai.prompt")
|
|
37
|
+
class OllamaPromptAction(BaseAction):
|
|
38
|
+
"""
|
|
39
|
+
Executes a prompt against a local Ollama service.
|
|
40
|
+
Supports structured JSON enforcement and image inputs (multimodal).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
|
|
44
|
+
prompt = str(parameters.get("prompt", "")).strip()
|
|
45
|
+
system = parameters.get("system")
|
|
46
|
+
model = str(parameters.get("model", "qwen2.5:1.5b"))
|
|
47
|
+
output_format = parameters.get("format")
|
|
48
|
+
base_url = str(parameters.get("base_url", "http://localhost:11434")).rstrip("/")
|
|
49
|
+
timeout = float(parameters.get("timeout", 60))
|
|
50
|
+
temperature = float(parameters.get("temperature", 0.1))
|
|
51
|
+
|
|
52
|
+
if not prompt:
|
|
53
|
+
raise ValueError("Parameter 'prompt' is required for action 'ai.prompt'.")
|
|
54
|
+
|
|
55
|
+
payload: Dict[str, Any] = {
|
|
56
|
+
"model": model,
|
|
57
|
+
"prompt": prompt,
|
|
58
|
+
"stream": False,
|
|
59
|
+
"options": {
|
|
60
|
+
"temperature": temperature
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if system:
|
|
65
|
+
payload["system"] = str(system)
|
|
66
|
+
|
|
67
|
+
if output_format:
|
|
68
|
+
payload["format"] = output_format
|
|
69
|
+
|
|
70
|
+
# Handle optional image path for multimodal / vision models
|
|
71
|
+
image_path_str = parameters.get("image_path")
|
|
72
|
+
if image_path_str:
|
|
73
|
+
target_image = _resolve_file_path(image_path_str, context)
|
|
74
|
+
if not target_image.is_file():
|
|
75
|
+
raise FileNotFoundError(f"Image file not found: {image_path_str}")
|
|
76
|
+
with open(target_image, "rb") as img_f:
|
|
77
|
+
b64_img = base64.b64encode(img_f.read()).decode("utf-8")
|
|
78
|
+
payload["images"] = [b64_img]
|
|
79
|
+
|
|
80
|
+
endpoint = f"{base_url}/api/generate"
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
response = requests.post(endpoint, json=payload, timeout=timeout)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
data = response.json()
|
|
86
|
+
except requests.exceptions.ConnectionError as conn_err:
|
|
87
|
+
raise ConnectionError(
|
|
88
|
+
f"Failed to connect to Ollama service at '{endpoint}'. "
|
|
89
|
+
"Ensure Ollama is running (e.g. 'ollama serve' or running in the background)."
|
|
90
|
+
) from conn_err
|
|
91
|
+
except Exception as req_err:
|
|
92
|
+
raise RuntimeError(f"Ollama API request error: {req_err}") from req_err
|
|
93
|
+
|
|
94
|
+
raw_response = data.get("response", "")
|
|
95
|
+
parsed_output: Any = raw_response
|
|
96
|
+
|
|
97
|
+
if output_format == "json":
|
|
98
|
+
cleaned = _clean_json_text(raw_response)
|
|
99
|
+
try:
|
|
100
|
+
parsed_output = json.loads(cleaned)
|
|
101
|
+
except Exception as json_err:
|
|
102
|
+
logger.warning(f"Could not parse Ollama response as JSON: {json_err}. Returning raw text.")
|
|
103
|
+
parsed_output = raw_response
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"response": parsed_output,
|
|
107
|
+
"raw_text": raw_response,
|
|
108
|
+
"model": model,
|
|
109
|
+
"total_duration": data.get("total_duration")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@register_action("ai.extract")
|
|
114
|
+
class OllamaExtractAction(BaseAction):
|
|
115
|
+
"""
|
|
116
|
+
Extracts structured data entities from text or an image using Ollama.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
|
|
120
|
+
schema: Optional[Any] = parameters.get("schema")
|
|
121
|
+
instructions = parameters.get("instructions", "Extract all key invoice fields into JSON.")
|
|
122
|
+
text = parameters.get("text", "")
|
|
123
|
+
image_path_str = parameters.get("image_path")
|
|
124
|
+
model = str(parameters.get("model", "qwen2.5:1.5b"))
|
|
125
|
+
base_url = str(parameters.get("base_url", "http://localhost:11434")).rstrip("/")
|
|
126
|
+
timeout = float(parameters.get("timeout", 90))
|
|
127
|
+
|
|
128
|
+
prompt_parts: List[str] = [str(instructions)]
|
|
129
|
+
|
|
130
|
+
if schema:
|
|
131
|
+
prompt_parts.append(
|
|
132
|
+
f"Required JSON schema or fields: {json.dumps(schema) if isinstance(schema, (dict, list)) else schema}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
prompt_parts.append("Respond strictly with a valid JSON object. Do not include explanatory conversational text.")
|
|
136
|
+
|
|
137
|
+
if text:
|
|
138
|
+
prompt_parts.append(f"Content to extract from:\n{text}")
|
|
139
|
+
|
|
140
|
+
prompt = "\n\n".join(prompt_parts)
|
|
141
|
+
|
|
142
|
+
action = OllamaPromptAction()
|
|
143
|
+
action_params = {
|
|
144
|
+
"prompt": prompt,
|
|
145
|
+
"model": model,
|
|
146
|
+
"format": "json",
|
|
147
|
+
"base_url": base_url,
|
|
148
|
+
"timeout": timeout,
|
|
149
|
+
"temperature": 0.0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if image_path_str:
|
|
153
|
+
action_params["image_path"] = image_path_str
|
|
154
|
+
|
|
155
|
+
result = action.execute(action_params, context)
|
|
156
|
+
return result.get("response")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from .base import BaseAction
|
|
7
|
+
from .registry import register_action
|
|
8
|
+
from ..models.context import ExecutionContext
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("kinenix.ai_systemone")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def call_systemone_api(
|
|
14
|
+
state: Union[str, Dict[str, Any]],
|
|
15
|
+
questions: Dict[str, Any],
|
|
16
|
+
base_url: str = "http://localhost:8000",
|
|
17
|
+
timeout: float = 30.0,
|
|
18
|
+
fallback_to_ollama: bool = False
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Sends a structured decision request to a local or remote OpenThai-SystemOne API server.
|
|
22
|
+
Converts friendly parameter formats (e.g. options list) to the model's required format.
|
|
23
|
+
"""
|
|
24
|
+
clean_url = str(base_url).rstrip("/")
|
|
25
|
+
endpoint = f"{clean_url}/v1/systemone"
|
|
26
|
+
|
|
27
|
+
# Normalize state to string if dict or primitive
|
|
28
|
+
normalized_state: str = state if isinstance(state, str) else json.dumps(state, ensure_ascii=False)
|
|
29
|
+
|
|
30
|
+
# Normalize questions payload
|
|
31
|
+
normalized_questions: Dict[str, Any] = {}
|
|
32
|
+
for q_key, q_val in questions.items():
|
|
33
|
+
if not isinstance(q_val, dict):
|
|
34
|
+
continue
|
|
35
|
+
|
|
36
|
+
q_type = str(q_val.get("type", "choice")).lower()
|
|
37
|
+
instructions = q_val.get("instructions") or q_val.get("instruction") or f"Select the best match for {q_key}"
|
|
38
|
+
criteria = q_val.get("criteria")
|
|
39
|
+
|
|
40
|
+
if criteria is None:
|
|
41
|
+
# Convert list of options to criteria dict (option -> None)
|
|
42
|
+
options_list = q_val.get("options") or []
|
|
43
|
+
if isinstance(options_list, list):
|
|
44
|
+
criteria = {str(opt): None for opt in options_list}
|
|
45
|
+
elif isinstance(options_list, dict):
|
|
46
|
+
criteria = options_list
|
|
47
|
+
else:
|
|
48
|
+
criteria = {}
|
|
49
|
+
|
|
50
|
+
if q_type == "choice":
|
|
51
|
+
normalized_questions[q_key] = {
|
|
52
|
+
"type": "choice",
|
|
53
|
+
"instructions": instructions,
|
|
54
|
+
"criteria": criteria
|
|
55
|
+
}
|
|
56
|
+
elif q_type == "score":
|
|
57
|
+
normalized_questions[q_key] = {
|
|
58
|
+
"type": "score",
|
|
59
|
+
"instructions": instructions,
|
|
60
|
+
"criteria": criteria or {"1": "low", "2": "medium", "3": "high"}
|
|
61
|
+
}
|
|
62
|
+
elif q_type in ("noul", "yes_no", "boolean"):
|
|
63
|
+
normalized_questions[q_key] = {
|
|
64
|
+
"type": "noul",
|
|
65
|
+
"instructions": instructions
|
|
66
|
+
}
|
|
67
|
+
else:
|
|
68
|
+
normalized_questions[q_key] = q_val
|
|
69
|
+
|
|
70
|
+
payload = {
|
|
71
|
+
"state": normalized_state,
|
|
72
|
+
"questions": normalized_questions
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
response = requests.post(
|
|
77
|
+
endpoint,
|
|
78
|
+
json=payload,
|
|
79
|
+
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
80
|
+
timeout=timeout
|
|
81
|
+
)
|
|
82
|
+
response.raise_for_status()
|
|
83
|
+
return response.json()
|
|
84
|
+
except requests.exceptions.ConnectionError as conn_err:
|
|
85
|
+
if fallback_to_ollama:
|
|
86
|
+
logger.warning(
|
|
87
|
+
f"OpenThai-SystemOne server at '{endpoint}' unreachable. Attempting fallback to local Ollama..."
|
|
88
|
+
)
|
|
89
|
+
return _fallback_ollama_decision(normalized_state, normalized_questions, timeout=timeout)
|
|
90
|
+
raise ConnectionError(
|
|
91
|
+
f"Failed to connect to OpenThai-SystemOne server at '{endpoint}'. "
|
|
92
|
+
"Ensure the server is running (e.g. '$env:OPENTHAI_SYSTEMONE_MODEL=\"iapp/OpenThai-SystemOne\"; uvicorn openthai_systemone.server:app --port 8000')."
|
|
93
|
+
) from conn_err
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise RuntimeError(f"OpenThai-SystemOne request error: {e}") from e
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _fallback_ollama_decision(state: str, questions: Dict[str, Any], timeout: float = 60.0) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Fallback resolver using Ollama if OpenThai-SystemOne server is offline.
|
|
101
|
+
"""
|
|
102
|
+
ollama_url = "http://localhost:11434/api/generate"
|
|
103
|
+
answers = {}
|
|
104
|
+
|
|
105
|
+
for q_key, q_val in questions.items():
|
|
106
|
+
q_type = q_val.get("type", "choice")
|
|
107
|
+
instructions = q_val.get("instructions", "")
|
|
108
|
+
criteria = q_val.get("criteria", {})
|
|
109
|
+
options = list(criteria.keys())
|
|
110
|
+
|
|
111
|
+
prompt = (
|
|
112
|
+
f"Context: {state}\n"
|
|
113
|
+
f"Question: {instructions}\n"
|
|
114
|
+
f"Options: {json.dumps(options, ensure_ascii=False)}\n"
|
|
115
|
+
f"Return JSON: {{\"choice\": \"<selected option>\"}}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
res = requests.post(
|
|
120
|
+
ollama_url,
|
|
121
|
+
json={"model": "qwen2.5:1.5b", "prompt": prompt, "format": "json", "stream": False},
|
|
122
|
+
timeout=timeout
|
|
123
|
+
)
|
|
124
|
+
res.raise_for_status()
|
|
125
|
+
res_data = res.json()
|
|
126
|
+
parsed = json.loads(res_data.get("response", "{}"))
|
|
127
|
+
answers[q_key] = {
|
|
128
|
+
"type": q_type,
|
|
129
|
+
"choice": parsed.get("choice") or (options[0] if options else None),
|
|
130
|
+
"confidence": 0.5,
|
|
131
|
+
"fallback": "ollama"
|
|
132
|
+
}
|
|
133
|
+
except Exception as ex:
|
|
134
|
+
logger.warning(f"Ollama fallback failed for question '{q_key}': {ex}")
|
|
135
|
+
answers[q_key] = {
|
|
136
|
+
"type": q_type,
|
|
137
|
+
"choice": options[0] if options else None,
|
|
138
|
+
"confidence": 0.0,
|
|
139
|
+
"error": str(ex)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {"model": "ollama-fallback", "answers": answers, "usage": {"input_tokens": 0, "output_tokens": 0}}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@register_action("ai.decide")
|
|
146
|
+
class AiDecideAction(BaseAction):
|
|
147
|
+
"""
|
|
148
|
+
Executes fast semantic decisions (Choice, Score, Yes/No) using OpenThai-SystemOne (0.8B).
|
|
149
|
+
Computes calibrated probabilities in a single forward pass without text generation hallucination.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
|
|
153
|
+
state = parameters.get("state")
|
|
154
|
+
if state is None:
|
|
155
|
+
raise ValueError("Parameter 'state' is required for action 'ai.decide'.")
|
|
156
|
+
|
|
157
|
+
base_url = str(parameters.get("base_url", "http://localhost:8000"))
|
|
158
|
+
timeout = float(parameters.get("timeout", 30.0))
|
|
159
|
+
fallback_to_ollama = bool(parameters.get("fallback_to_ollama", True))
|
|
160
|
+
|
|
161
|
+
# Accept either 'question' (singular) or 'questions' (plural dictionary)
|
|
162
|
+
single_question = parameters.get("question")
|
|
163
|
+
questions_dict = parameters.get("questions")
|
|
164
|
+
|
|
165
|
+
if single_question and isinstance(single_question, dict):
|
|
166
|
+
q_name = single_question.get("name", "decision")
|
|
167
|
+
questions = {q_name: single_question}
|
|
168
|
+
is_single = True
|
|
169
|
+
elif questions_dict and isinstance(questions_dict, dict):
|
|
170
|
+
questions = questions_dict
|
|
171
|
+
is_single = False
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError("Either 'question' (dict) or 'questions' (dict) parameter is required for 'ai.decide'.")
|
|
174
|
+
|
|
175
|
+
response = call_systemone_api(
|
|
176
|
+
state=state,
|
|
177
|
+
questions=questions,
|
|
178
|
+
base_url=base_url,
|
|
179
|
+
timeout=timeout,
|
|
180
|
+
fallback_to_ollama=fallback_to_ollama
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
answers = response.get("answers", {})
|
|
184
|
+
|
|
185
|
+
if is_single and len(answers) == 1:
|
|
186
|
+
# For convenient single-question calls, unwrap the direct answer
|
|
187
|
+
single_res = list(answers.values())[0]
|
|
188
|
+
return {
|
|
189
|
+
"choice": single_res.get("choice"),
|
|
190
|
+
"confidence": single_res.get("confidence"),
|
|
191
|
+
"probabilities": single_res.get("probabilities", {}),
|
|
192
|
+
"type": single_res.get("type"),
|
|
193
|
+
"model": response.get("model", "openthai-systemone")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
"answers": answers,
|
|
198
|
+
"model": response.get("model", "openthai-systemone"),
|
|
199
|
+
"usage": response.get("usage", {})
|
|
200
|
+
}
|
|
201
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from ..models.context import ExecutionContext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseAction(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Abstract base class for all Kinenix Actions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
action_type: str = "base"
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def execute(self, parameters: Dict[str, Any], context: ExecutionContext) -> Any:
|
|
16
|
+
"""
|
|
17
|
+
Executes the action with given parameters and context.
|
|
18
|
+
Returns output data to be stored or used by downstream steps.
|
|
19
|
+
"""
|
|
20
|
+
pass
|