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.
Files changed (41) hide show
  1. kinenix-0.1.0b1/LICENSE +21 -0
  2. kinenix-0.1.0b1/PKG-INFO +73 -0
  3. kinenix-0.1.0b1/README.md +44 -0
  4. kinenix-0.1.0b1/kinenix/__init__.py +5 -0
  5. kinenix-0.1.0b1/kinenix/actions/__init__.py +23 -0
  6. kinenix-0.1.0b1/kinenix/actions/ai_ollama.py +156 -0
  7. kinenix-0.1.0b1/kinenix/actions/ai_systemone.py +201 -0
  8. kinenix-0.1.0b1/kinenix/actions/base.py +20 -0
  9. kinenix-0.1.0b1/kinenix/actions/data_excel.py +143 -0
  10. kinenix-0.1.0b1/kinenix/actions/email_smtp.py +131 -0
  11. kinenix-0.1.0b1/kinenix/actions/file_system.py +121 -0
  12. kinenix-0.1.0b1/kinenix/actions/flow_control.py +37 -0
  13. kinenix-0.1.0b1/kinenix/actions/http_api.py +73 -0
  14. kinenix-0.1.0b1/kinenix/actions/logic.py +59 -0
  15. kinenix-0.1.0b1/kinenix/actions/registry.py +32 -0
  16. kinenix-0.1.0b1/kinenix/actions/web_playwright.py +537 -0
  17. kinenix-0.1.0b1/kinenix/cli.py +345 -0
  18. kinenix-0.1.0b1/kinenix/engine/__init__.py +13 -0
  19. kinenix-0.1.0b1/kinenix/engine/evaluator.py +55 -0
  20. kinenix-0.1.0b1/kinenix/engine/interpreter.py +796 -0
  21. kinenix-0.1.0b1/kinenix/engine/logger.py +155 -0
  22. kinenix-0.1.0b1/kinenix/engine/markdown.py +544 -0
  23. kinenix-0.1.0b1/kinenix/models/__init__.py +17 -0
  24. kinenix-0.1.0b1/kinenix/models/context.py +55 -0
  25. kinenix-0.1.0b1/kinenix/models/flow.py +46 -0
  26. kinenix-0.1.0b1/kinenix.egg-info/PKG-INFO +73 -0
  27. kinenix-0.1.0b1/kinenix.egg-info/SOURCES.txt +39 -0
  28. kinenix-0.1.0b1/kinenix.egg-info/dependency_links.txt +1 -0
  29. kinenix-0.1.0b1/kinenix.egg-info/entry_points.txt +2 -0
  30. kinenix-0.1.0b1/kinenix.egg-info/requires.txt +8 -0
  31. kinenix-0.1.0b1/kinenix.egg-info/top_level.txt +1 -0
  32. kinenix-0.1.0b1/pyproject.toml +48 -0
  33. kinenix-0.1.0b1/setup.cfg +4 -0
  34. kinenix-0.1.0b1/tests/test_ai_action.py +120 -0
  35. kinenix-0.1.0b1/tests/test_ai_systemone.py +249 -0
  36. kinenix-0.1.0b1/tests/test_engine.py +788 -0
  37. kinenix-0.1.0b1/tests/test_markdown.py +220 -0
  38. kinenix-0.1.0b1/tests/test_new_actions.py +413 -0
  39. kinenix-0.1.0b1/tests/test_resilience.py +261 -0
  40. kinenix-0.1.0b1/tests/test_telemetry_auth.py +45 -0
  41. kinenix-0.1.0b1/tests/test_zombie_cleanup.py +237 -0
@@ -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.
@@ -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,5 @@
1
+ """
2
+ kinenix-core: Core Execution Engine for Kinenix RPA Platform
3
+ """
4
+
5
+ __version__ = "0.1.0b1"
@@ -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