siada-cli 0.0.1__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.
- agent_config.yaml +34 -0
- siada/__init__.py +13 -0
- siada/agent_hub/__init__.py +10 -0
- siada/agent_hub/coder/bug_fix_agent.py +398 -0
- siada/agent_hub/coder/code_gen_agent.py +235 -0
- siada/agent_hub/coder/fe_gen_agent.py +37 -0
- siada/agent_hub/coder/issue_review_agent.py +68 -0
- siada/agent_hub/coder/prompt/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/__init__.py +0 -0
- siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
- siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
- siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
- siada/agent_hub/coder/prompt/base/rules.py +40 -0
- siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
- siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
- siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
- siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
- siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
- siada/agent_hub/coder/tracing/__init__.py +22 -0
- siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
- siada/agent_hub/hooks/__init__.py +0 -0
- siada/agent_hub/hooks/io_hooks.py +30 -0
- siada/agent_hub/siada_agent.py +214 -0
- siada/config/__init__.py +0 -0
- siada/config/config_loader.py +67 -0
- siada/entrypoint/__init__.py +3 -0
- siada/entrypoint/args_parser/__init__.py +0 -0
- siada/entrypoint/args_parser/args.py +195 -0
- siada/entrypoint/interaction/__init__.py +0 -0
- siada/entrypoint/interaction/base_turn.py +107 -0
- siada/entrypoint/interaction/config.py +30 -0
- siada/entrypoint/interaction/controller.py +122 -0
- siada/entrypoint/interaction/nointeractive_controller.py +27 -0
- siada/entrypoint/interaction/turn/interface.py +116 -0
- siada/entrypoint/interaction/turn/models.py +34 -0
- siada/entrypoint/interaction/turn/run_turn.py +641 -0
- siada/entrypoint/interaction/turn_factory.py +35 -0
- siada/entrypoint/interaction/turn_models.py +28 -0
- siada/entrypoint/siadahub.py +413 -0
- siada/foundation/__init__.py +0 -0
- siada/foundation/code_agent_context.py +48 -0
- siada/foundation/config.py +59 -0
- siada/foundation/constants.py +2 -0
- siada/foundation/context.py +76 -0
- siada/foundation/logging.py +341 -0
- siada/foundation/tools/__init__.py +0 -0
- siada/foundation/tools/get_git_diff.py +105 -0
- siada/io/__init__.py +0 -0
- siada/io/banner.py +148 -0
- siada/io/color_settings.py +207 -0
- siada/io/color_utils.py +16 -0
- siada/io/components/__init__.py +0 -0
- siada/io/components/mdstream.py +204 -0
- siada/io/console_printer.py +68 -0
- siada/io/io.py +661 -0
- siada/io/key_bindings.py +129 -0
- siada/io/notification_command.py +40 -0
- siada/io/test_banner.py +64 -0
- siada/models/__init__.py +0 -0
- siada/models/agent.py +47 -0
- siada/models/coder/__init__.py +0 -0
- siada/models/converter.py +27 -0
- siada/models/model_base_config.py +121 -0
- siada/models/model_run_config.py +153 -0
- siada/provider/__init__.py +0 -0
- siada/provider/client_factory.py +112 -0
- siada/provider/llm_client.py +11 -0
- siada/provider/openrouter/__init__.py +0 -0
- siada/provider/openrouter/coverter.py +17 -0
- siada/provider/openrouter/openroute_provider.py +33 -0
- siada/provider/provider_factory.py +53 -0
- siada/queries/tree-sitter-language-pack/README.md +7 -0
- siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- siada/queries/tree-sitter-languages/README.md +23 -0
- siada/queries/tree-sitter-languages/c-tags.scm +9 -0
- siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
- siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
- siada/queries/tree-sitter-languages/go-tags.scm +30 -0
- siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- siada/queries/tree-sitter-languages/java-tags.scm +20 -0
- siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- siada/queries/tree-sitter-languages/php-tags.scm +26 -0
- siada/queries/tree-sitter-languages/python-tags.scm +12 -0
- siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
- siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
- siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
- siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- siada/services/__init__.py +30 -0
- siada/services/at_file_recommendation_design_document.md +1213 -0
- siada/services/code_context_manager.py +136 -0
- siada/services/enhanced_fix_result_check.py +546 -0
- siada/services/execution_trace_collector.py +207 -0
- siada/services/file_recommendation/__init__.py +136 -0
- siada/services/file_recommendation/core/__init__.py +17 -0
- siada/services/file_recommendation/core/completion_engine.py +351 -0
- siada/services/file_recommendation/core/config.py +62 -0
- siada/services/file_recommendation/core/file_discovery.py +353 -0
- siada/services/file_recommendation/core/suggestion.py +110 -0
- siada/services/file_recommendation/utils/__init__.py +22 -0
- siada/services/file_recommendation/utils/path_utils.py +138 -0
- siada/services/file_recommendation/utils/text_utils.py +253 -0
- siada/services/fix_result_check.py +377 -0
- siada/services/handle_at_command/README.md +247 -0
- siada/services/handle_at_command/__init__.py +115 -0
- siada/services/handle_at_command/exceptions.py +46 -0
- siada/services/handle_at_command/models.py +92 -0
- siada/services/handle_at_command/parser.py +189 -0
- siada/services/handle_at_command/processor.py +321 -0
- siada/services/handle_at_command/resolver.py +318 -0
- siada/services/handle_at_command/utils.py +345 -0
- siada/services/model_info_service.py +103 -0
- siada/services/siada_runner.py +170 -0
- siada/session/__init__.py +29 -0
- siada/session/session_manager.py +66 -0
- siada/session/session_models.py +43 -0
- siada/support/__init__.py +0 -0
- siada/support/completer.py +204 -0
- siada/support/editor.py +146 -0
- siada/support/envprocessor.py +54 -0
- siada/support/repo.py +51 -0
- siada/support/slash_commands.py +410 -0
- siada/support/spinner.py +320 -0
- siada/tools/__init__.py +0 -0
- siada/tools/ast/README.md +345 -0
- siada/tools/ast/ast_tool.py +341 -0
- siada/tools/ast/models.py +11 -0
- siada/tools/coder/__init__.py +0 -0
- siada/tools/coder/ask_followup_question.py +70 -0
- siada/tools/coder/cmd_runner.py +132 -0
- siada/tools/coder/file_operator.py +284 -0
- siada/tools/coder/file_search/README.md +223 -0
- siada/tools/coder/file_search/__init__.py +10 -0
- siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
- siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
- siada/tools/coder/file_search/bin/rg.exe +0 -0
- siada/tools/coder/file_search/search.py +437 -0
- siada/tools/coder/file_search/setup_package.py +150 -0
- siada/tools/coder/file_search/test_search.py +142 -0
- siada/tools/coder/files.py +147 -0
- siada/tools/coder/fix_attempt_completion.py +58 -0
- siada/tools/coder/issue_review_completion.py +29 -0
- siada/tools/coder/observation/__init__.py +0 -0
- siada/tools/coder/observation/error.py +22 -0
- siada/tools/coder/observation/file_observation.py +216 -0
- siada/tools/coder/observation/observation.py +71 -0
- siada/tools/coder/repo_map/__init__.py +157 -0
- siada/tools/coder/repo_map/dump.py +29 -0
- siada/tools/coder/repo_map/io.py +226 -0
- siada/tools/coder/repo_map/repo_map.py +881 -0
- siada/tools/coder/repo_map/special.py +203 -0
- siada/tools/coder/repo_map/token_counter.py +154 -0
- siada/tools/coder/repo_map/waiting.py +221 -0
- siada/tools/coder/reproduce_completion.py +39 -0
- siada/tools/coder/run_cmd.py +60 -0
- siada/tools/coder/test_completion.py +25 -0
- siada/tools/coder/tool_docs.py +44 -0
- siada/tools/compression_tool.py +200 -0
- siada/tools/read_many_files/README.md +220 -0
- siada/tools/read_many_files/__init__.py +25 -0
- siada/tools/read_many_files/file_processor.py +486 -0
- siada/tools/read_many_files/filters.py +326 -0
- siada/tools/read_many_files/formatters.py +204 -0
- siada/tools/read_many_files/models.py +189 -0
- siada/tools/read_many_files_tool.py +233 -0
- siada/tools/tool_call_format/__init__.py +55 -0
- siada/tools/tool_call_format/file_to_language.py +231 -0
- siada/tools/tool_call_format/formatter_factory.py +80 -0
- siada/tools/tool_call_format/formatters.py +282 -0
- siada/tools/tool_call_format/tool_call_formatter.py +54 -0
- siada/tools/web/__init__.py +0 -0
- siada/tools/web/web_crawl.py +208 -0
- siada/utils.py +194 -0
- siada_cli-0.0.1.dist-info/LICENSE +201 -0
- siada_cli-0.0.1.dist-info/METADATA +457 -0
- siada_cli-0.0.1.dist-info/RECORD +215 -0
- siada_cli-0.0.1.dist-info/WHEEL +4 -0
- siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Run Turn Module
|
|
3
|
+
|
|
4
|
+
Manages individual interaction turns between user and AI, including command processing
|
|
5
|
+
and model conversations. Encapsulates the logic for a single interaction cycle.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import grep_ast
|
|
9
|
+
from siada.foundation.logging import logger
|
|
10
|
+
import re
|
|
11
|
+
import siada.io.components.mdstream
|
|
12
|
+
from typing import Optional, Dict, Any, Tuple
|
|
13
|
+
|
|
14
|
+
from agents import (
|
|
15
|
+
RawResponsesStreamEvent,
|
|
16
|
+
RunItemStreamEvent,
|
|
17
|
+
RunResultStreaming,
|
|
18
|
+
ToolCallOutputItem,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from siada.support.spinner import WaitingSpinner
|
|
22
|
+
from siada.tools.coder.observation.observation import FunctionCallResult
|
|
23
|
+
from siada.tools.tool_call_format.formatter_factory import ToolCallFormatterFactory
|
|
24
|
+
|
|
25
|
+
# Import existing InteractionConfig
|
|
26
|
+
from ..config import RunningConfig
|
|
27
|
+
|
|
28
|
+
# Import models and interface from the same directory
|
|
29
|
+
from .models import TurnType, TurnInput, TurnOutput
|
|
30
|
+
from .interface import RunTurn
|
|
31
|
+
from rich.markdown import Markdown
|
|
32
|
+
from rich.text import Text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Standard tag identifier
|
|
36
|
+
REASONING_TAG = "thinking-content-" + "7bbeb8e1441453ad999a0bbba8a46d4b"
|
|
37
|
+
|
|
38
|
+
SPLIT_TAG = "\n--------------\n"
|
|
39
|
+
# Output formatting
|
|
40
|
+
|
|
41
|
+
REASONING_START = "► **THINKING**"
|
|
42
|
+
|
|
43
|
+
REASONING_END = "► **ANSWER**"
|
|
44
|
+
|
|
45
|
+
TOOL_CALL_START = "► **TOOL USE**"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ConversationTurn(RunTurn):
|
|
49
|
+
"""Handles regular AI conversation turns"""
|
|
50
|
+
|
|
51
|
+
mdstream: siada.io.components.mdstream.MarkdownRender = None
|
|
52
|
+
|
|
53
|
+
def _process_thinking_tags(self, text: str) -> Tuple[str, bool]:
|
|
54
|
+
"""
|
|
55
|
+
Process thinking tags and return (processed_text, should_render)
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
text (str): Streaming input text
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
tuple: (processed_text, should_render)
|
|
62
|
+
- If text ends with incomplete thinking tag, return (previous_safe_text, False)
|
|
63
|
+
- Otherwise remove all thinking tags (preserve content) and return (clean_text, True)
|
|
64
|
+
"""
|
|
65
|
+
# Define complete tags
|
|
66
|
+
thinking_start = '<thinking>'
|
|
67
|
+
thinking_end = '</thinking>'
|
|
68
|
+
|
|
69
|
+
# Check if text ends with incomplete thinking tag
|
|
70
|
+
def is_partial_tag_at_end(text: str, full_tag: str) -> bool:
|
|
71
|
+
"""Check if text ends with incomplete part of specified tag"""
|
|
72
|
+
for i in range(1, len(full_tag)):
|
|
73
|
+
if text.endswith(full_tag[:i]):
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# If text ends with incomplete <thinking> or </thinking> part, pause rendering
|
|
78
|
+
if is_partial_tag_at_end(text, thinking_start) or is_partial_tag_at_end(text, thinking_end):
|
|
79
|
+
# Find the last complete tag position and get safe part
|
|
80
|
+
safe_end = len(text)
|
|
81
|
+
for i in range(len(text) - 1, -1, -1):
|
|
82
|
+
if text[i] == '<':
|
|
83
|
+
# Check if this position might be start of incomplete thinking tag
|
|
84
|
+
remaining = text[i:]
|
|
85
|
+
if thinking_start.startswith(remaining) or thinking_end.startswith(remaining):
|
|
86
|
+
safe_end = i
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
safe_text = text[:safe_end]
|
|
90
|
+
clean_text = self._remove_thinking_content(safe_text)
|
|
91
|
+
return clean_text, False
|
|
92
|
+
|
|
93
|
+
# No incomplete tags, remove all thinking tags
|
|
94
|
+
clean_text = self._remove_thinking_content(text)
|
|
95
|
+
return clean_text, True
|
|
96
|
+
|
|
97
|
+
def _remove_thinking_content(self, text: str) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Remove thinking tags from text but preserve content inside
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
text (str): Input text
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str: Text with thinking tags removed
|
|
106
|
+
"""
|
|
107
|
+
# Remove start tag <thinking> and possible whitespace
|
|
108
|
+
text = re.sub(r'<thinking>\s?', '', text)
|
|
109
|
+
|
|
110
|
+
# Remove end tag </thinking> and possible surrounding whitespace
|
|
111
|
+
text = re.sub(r'\s?</thinking>', '', text)
|
|
112
|
+
|
|
113
|
+
return text
|
|
114
|
+
tool_calls: Dict[str, Dict[str, Any]] = None
|
|
115
|
+
tool_call_mdstreams: Dict[str, siada.io.components.mdstream.MarkdownRender] = None
|
|
116
|
+
response_content: str = None
|
|
117
|
+
current_active_call_id: Optional[str] = None
|
|
118
|
+
got_content_part: bool = False
|
|
119
|
+
got_reasoning_part: bool = False
|
|
120
|
+
got_tool_result_part: bool = False
|
|
121
|
+
got_function_call_part: bool = False
|
|
122
|
+
|
|
123
|
+
# Class-level dedicated event loop and thread, shared by all instances
|
|
124
|
+
_dedicated_loop = None
|
|
125
|
+
_dedicated_thread = None
|
|
126
|
+
_loop_ready = None
|
|
127
|
+
|
|
128
|
+
def __init__(self, config: RunningConfig, session: Any, slash_commands: Any):
|
|
129
|
+
super().__init__(config, session, slash_commands)
|
|
130
|
+
self.mdargs = dict(
|
|
131
|
+
style=self.config.running_color_settings.split_line_color,
|
|
132
|
+
code_theme=self.config.running_color_settings.code_theme,
|
|
133
|
+
inline_code_lexer="text",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def _ensure_dedicated_loop(cls):
|
|
138
|
+
"""Ensure dedicated event loop is started"""
|
|
139
|
+
if cls._dedicated_loop is None or cls._dedicated_loop.is_closed():
|
|
140
|
+
import threading
|
|
141
|
+
import asyncio
|
|
142
|
+
|
|
143
|
+
# Detect if main thread has a running event loop (prompt_toolkit might be using it)
|
|
144
|
+
try:
|
|
145
|
+
main_loop = asyncio.get_running_loop()
|
|
146
|
+
logger.info(f"📋 Detected main thread event loop: {id(main_loop)}")
|
|
147
|
+
except RuntimeError:
|
|
148
|
+
logger.info("📋 No event loop in main thread") # Normal case
|
|
149
|
+
|
|
150
|
+
# Create event for synchronization
|
|
151
|
+
cls._loop_ready = threading.Event()
|
|
152
|
+
|
|
153
|
+
def run_dedicated_loop():
|
|
154
|
+
"""Run event loop in dedicated thread"""
|
|
155
|
+
# Create new event loop
|
|
156
|
+
loop = asyncio.new_event_loop()
|
|
157
|
+
asyncio.set_event_loop(loop)
|
|
158
|
+
cls._dedicated_loop = loop
|
|
159
|
+
|
|
160
|
+
# Notify main thread that loop is ready
|
|
161
|
+
cls._loop_ready.set()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Run event loop until stopped
|
|
165
|
+
loop.run_forever()
|
|
166
|
+
finally:
|
|
167
|
+
# Cleanup
|
|
168
|
+
loop.close()
|
|
169
|
+
cls._dedicated_loop = None
|
|
170
|
+
|
|
171
|
+
# Start dedicated thread
|
|
172
|
+
cls._dedicated_thread = threading.Thread(
|
|
173
|
+
target=run_dedicated_loop,
|
|
174
|
+
daemon=True, # Daemon thread, auto-terminate when main program exits
|
|
175
|
+
name="ConversationTurn-AsyncLoop",
|
|
176
|
+
)
|
|
177
|
+
cls._dedicated_thread.start()
|
|
178
|
+
|
|
179
|
+
# Wait for event loop to be ready
|
|
180
|
+
cls._loop_ready.wait()
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def _cleanup_dedicated_loop(cls):
|
|
184
|
+
"""Cleanup dedicated event loop (optional, mainly for testing or graceful shutdown)"""
|
|
185
|
+
if cls._dedicated_loop and not cls._dedicated_loop.is_closed():
|
|
186
|
+
cls._dedicated_loop.call_soon_threadsafe(cls._dedicated_loop.stop)
|
|
187
|
+
if cls._dedicated_thread and cls._dedicated_thread.is_alive():
|
|
188
|
+
cls._dedicated_thread.join(timeout=5.0)
|
|
189
|
+
|
|
190
|
+
def get_turn_type(self) -> TurnType:
|
|
191
|
+
return TurnType.CONVERSATION
|
|
192
|
+
|
|
193
|
+
def can_handle(self, user_input: str) -> bool:
|
|
194
|
+
"""Handle non-command input"""
|
|
195
|
+
return not self.slash_commands.is_command(user_input)
|
|
196
|
+
|
|
197
|
+
async def output_stream_content(self, result: RunResultStreaming) -> None:
|
|
198
|
+
"""Process stream events and handle real-time output"""
|
|
199
|
+
from openai.types.responses import (
|
|
200
|
+
ResponseTextDeltaEvent,
|
|
201
|
+
ResponseReasoningSummaryTextDeltaEvent,
|
|
202
|
+
ResponseFunctionCallArgumentsDeltaEvent,
|
|
203
|
+
ResponseContentPartAddedEvent,
|
|
204
|
+
ResponseOutputItemAddedEvent,
|
|
205
|
+
ResponseCompletedEvent,
|
|
206
|
+
ResponseCreatedEvent,
|
|
207
|
+
ResponseReasoningSummaryPartAddedEvent,
|
|
208
|
+
ResponseFunctionToolCall,
|
|
209
|
+
ResponseOutputItemDoneEvent,
|
|
210
|
+
ResponseContentPartDoneEvent,
|
|
211
|
+
)
|
|
212
|
+
from openai.types.responses.response_input_item_param import FunctionCallOutput
|
|
213
|
+
|
|
214
|
+
stream_iterator = None
|
|
215
|
+
try:
|
|
216
|
+
stream_iterator = result.stream_events()
|
|
217
|
+
async for event in stream_iterator:
|
|
218
|
+
if isinstance(event, RawResponsesStreamEvent):
|
|
219
|
+
self._stop_waiting_spinner()
|
|
220
|
+
|
|
221
|
+
# Handle the raw response stream event
|
|
222
|
+
stream_data = event.data
|
|
223
|
+
|
|
224
|
+
# Handle different types of stream events
|
|
225
|
+
if isinstance(stream_data, ResponseCreatedEvent):
|
|
226
|
+
# Response started
|
|
227
|
+
self.response_content = ""
|
|
228
|
+
self.tool_calls = {}
|
|
229
|
+
self.tool_call_mdstreams = {}
|
|
230
|
+
self.got_content_part = False
|
|
231
|
+
self.got_reasoning_part = False
|
|
232
|
+
self.current_active_call_id = None
|
|
233
|
+
self.got_tool_result_part = False
|
|
234
|
+
self.got_function_call_part = False
|
|
235
|
+
|
|
236
|
+
elif isinstance(
|
|
237
|
+
stream_data, ResponseReasoningSummaryPartAddedEvent
|
|
238
|
+
):
|
|
239
|
+
if self.mdstream is None:
|
|
240
|
+
self.mdstream = (
|
|
241
|
+
self.get_response_mdstream()
|
|
242
|
+
if self.config.io.pretty
|
|
243
|
+
else None
|
|
244
|
+
)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
elif isinstance(
|
|
248
|
+
stream_data, ResponseReasoningSummaryTextDeltaEvent
|
|
249
|
+
):
|
|
250
|
+
if not self.got_reasoning_part and stream_data.delta:
|
|
251
|
+
self.got_reasoning_part = True
|
|
252
|
+
self.print_split_line()
|
|
253
|
+
delta_text = f"\n{REASONING_START}\n\n{stream_data.delta}"
|
|
254
|
+
self.response_content += delta_text
|
|
255
|
+
else:
|
|
256
|
+
delta_text = stream_data.delta
|
|
257
|
+
self.response_content += delta_text
|
|
258
|
+
self._live_incremental_response(
|
|
259
|
+
delta_text, self.response_content
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
elif isinstance(stream_data, ResponseContentPartAddedEvent):
|
|
263
|
+
if self.mdstream is None:
|
|
264
|
+
self.mdstream = (
|
|
265
|
+
self.get_response_mdstream()
|
|
266
|
+
if self.config.io.pretty
|
|
267
|
+
else None
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
elif isinstance(stream_data, ResponseTextDeltaEvent):
|
|
272
|
+
if not self.got_content_part and stream_data.delta:
|
|
273
|
+
self.got_content_part = True
|
|
274
|
+
if not self.got_reasoning_part:
|
|
275
|
+
self.print_split_line()
|
|
276
|
+
delta_text = f"\n\n{REASONING_END}\n\n{stream_data.delta}"
|
|
277
|
+
self.response_content += delta_text
|
|
278
|
+
else:
|
|
279
|
+
delta_text = stream_data.delta
|
|
280
|
+
self.response_content += delta_text
|
|
281
|
+
self._live_incremental_response(
|
|
282
|
+
delta_text, self.response_content
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
elif isinstance(stream_data, ResponseContentPartDoneEvent):
|
|
286
|
+
if not self.got_function_call_part:
|
|
287
|
+
# if not got function call part, flush the response content
|
|
288
|
+
self._live_incremental_response(
|
|
289
|
+
"\n", self.response_content, final=True
|
|
290
|
+
)
|
|
291
|
+
self.mdstream = None
|
|
292
|
+
|
|
293
|
+
elif isinstance(stream_data, ResponseOutputItemAddedEvent):
|
|
294
|
+
if isinstance(stream_data.item, ResponseFunctionToolCall):
|
|
295
|
+
# if have got the function call part, must flush the response before the tool call
|
|
296
|
+
if not self.got_function_call_part:
|
|
297
|
+
self.got_function_call_part = True
|
|
298
|
+
# flush the response content
|
|
299
|
+
self._live_incremental_response(
|
|
300
|
+
"\n", self.response_content, final=True
|
|
301
|
+
)
|
|
302
|
+
self.mdstream = None
|
|
303
|
+
|
|
304
|
+
call_id = stream_data.item.call_id
|
|
305
|
+
tool_name = stream_data.item.name
|
|
306
|
+
self.tool_calls[call_id] = {
|
|
307
|
+
"name": tool_name,
|
|
308
|
+
"arguments": "",
|
|
309
|
+
"arguments_render": "",
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
tool_call_formatter = ToolCallFormatterFactory.get_formatter(tool_name)
|
|
313
|
+
|
|
314
|
+
if (
|
|
315
|
+
self.config.io.pretty
|
|
316
|
+
and tool_call_formatter.supports_streaming()
|
|
317
|
+
):
|
|
318
|
+
self.tool_call_mdstreams[call_id] = (
|
|
319
|
+
self.get_response_mdstream()
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# process the previous tool call stream, stop the live
|
|
323
|
+
if (
|
|
324
|
+
self.current_active_call_id
|
|
325
|
+
and self.current_active_call_id
|
|
326
|
+
in self.tool_call_mdstreams
|
|
327
|
+
):
|
|
328
|
+
self.tool_call_mdstreams[
|
|
329
|
+
self.current_active_call_id
|
|
330
|
+
].update(
|
|
331
|
+
tool_call_formatter.format_input(
|
|
332
|
+
self.current_active_call_id,
|
|
333
|
+
self.tool_calls[self.current_active_call_id]["name"],
|
|
334
|
+
self.tool_calls[self.current_active_call_id][
|
|
335
|
+
"arguments"
|
|
336
|
+
],
|
|
337
|
+
)[0],
|
|
338
|
+
final=True,
|
|
339
|
+
)
|
|
340
|
+
if self.current_active_call_id in self.tool_call_mdstreams:
|
|
341
|
+
del self.tool_call_mdstreams[
|
|
342
|
+
self.current_active_call_id
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
self.current_active_call_id = call_id
|
|
346
|
+
self.print_split_line()
|
|
347
|
+
self.config.io.print_tool_call(
|
|
348
|
+
f"{TOOL_CALL_START}\n\nSiada wants to use the tool: {tool_name}\n"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
elif isinstance(
|
|
352
|
+
stream_data, ResponseFunctionCallArgumentsDeltaEvent
|
|
353
|
+
):
|
|
354
|
+
delta = stream_data.delta
|
|
355
|
+
if self.current_active_call_id:
|
|
356
|
+
self.tool_calls[self.current_active_call_id][
|
|
357
|
+
"arguments"
|
|
358
|
+
] += delta
|
|
359
|
+
|
|
360
|
+
tool_call_formatter = ToolCallFormatterFactory.get_formatter(
|
|
361
|
+
self.tool_calls[self.current_active_call_id]["name"]
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# if supports streaming, update the tool call mdstream
|
|
365
|
+
if tool_call_formatter.supports_streaming():
|
|
366
|
+
content, is_complete = tool_call_formatter.format_input(
|
|
367
|
+
self.current_active_call_id,
|
|
368
|
+
self.tool_calls[self.current_active_call_id][
|
|
369
|
+
"name"
|
|
370
|
+
],
|
|
371
|
+
self.tool_calls[self.current_active_call_id][
|
|
372
|
+
"arguments"
|
|
373
|
+
],
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
# compute the content_delta
|
|
377
|
+
arguments_delta = content[len(self.tool_calls[self.current_active_call_id]["arguments_render"]):]
|
|
378
|
+
self.tool_calls[self.current_active_call_id]["arguments_render"] = content
|
|
379
|
+
|
|
380
|
+
if self.current_active_call_id in self.tool_call_mdstreams:
|
|
381
|
+
self.tool_call_mdstreams[
|
|
382
|
+
self.current_active_call_id
|
|
383
|
+
].update(content, final=False)
|
|
384
|
+
else:
|
|
385
|
+
self.config.io.console.print(
|
|
386
|
+
arguments_delta, sep="", end=""
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
elif isinstance(stream_data, ResponseOutputItemDoneEvent):
|
|
390
|
+
if isinstance(stream_data.item, ResponseFunctionToolCall):
|
|
391
|
+
call_id = stream_data.item.call_id
|
|
392
|
+
if call_id in self.tool_calls:
|
|
393
|
+
tool_name = self.tool_calls[call_id]["name"]
|
|
394
|
+
full_arguments = self.tool_calls[call_id]["arguments"]
|
|
395
|
+
|
|
396
|
+
tool_call_formatter = (
|
|
397
|
+
ToolCallFormatterFactory.get_formatter(tool_name)
|
|
398
|
+
)
|
|
399
|
+
content, _ = tool_call_formatter.format_input(
|
|
400
|
+
call_id, tool_name, full_arguments
|
|
401
|
+
)
|
|
402
|
+
style = tool_call_formatter.get_style()
|
|
403
|
+
# if not streaming, only create the mdstream and update the final content
|
|
404
|
+
if tool_call_formatter.supports_streaming():
|
|
405
|
+
# process the last tool call stream, stop the live
|
|
406
|
+
if call_id in self.tool_call_mdstreams:
|
|
407
|
+
self.tool_call_mdstreams[call_id].update(
|
|
408
|
+
content,
|
|
409
|
+
final=True,
|
|
410
|
+
)
|
|
411
|
+
if call_id in self.tool_call_mdstreams:
|
|
412
|
+
del self.tool_call_mdstreams[call_id]
|
|
413
|
+
else:
|
|
414
|
+
if style == "markdown" and self.config.io.pretty:
|
|
415
|
+
self.tool_call_mdstreams[call_id] = (
|
|
416
|
+
self.get_response_mdstream()
|
|
417
|
+
)
|
|
418
|
+
self.tool_call_mdstreams[call_id].update(
|
|
419
|
+
content, final=True
|
|
420
|
+
)
|
|
421
|
+
if call_id in self.tool_call_mdstreams:
|
|
422
|
+
del self.tool_call_mdstreams[call_id]
|
|
423
|
+
else:
|
|
424
|
+
self.config.io.print_tool_call(content)
|
|
425
|
+
|
|
426
|
+
elif isinstance(stream_data, ResponseCompletedEvent):
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
elif isinstance(event, RunItemStreamEvent):
|
|
430
|
+
stream_data = event.item
|
|
431
|
+
if isinstance(stream_data, ToolCallOutputItem):
|
|
432
|
+
call_id = stream_data.raw_item.get("call_id", None)
|
|
433
|
+
if call_id:
|
|
434
|
+
if call_id in self.tool_calls:
|
|
435
|
+
tool_name = self.tool_calls[call_id]["name"]
|
|
436
|
+
self.print_split_line()
|
|
437
|
+
output = stream_data.output
|
|
438
|
+
if isinstance(output, FunctionCallResult):
|
|
439
|
+
self.config.io.print_tool_result(
|
|
440
|
+
output.format_for_display()
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
self.config.io.print_tool_result(str(output))
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
# Clean up MarkdownStream if it exists on stream error
|
|
447
|
+
if hasattr(self, "mdstream") and self.mdstream is not None:
|
|
448
|
+
try:
|
|
449
|
+
self.mdstream.close() # Direct close, no need to render on error
|
|
450
|
+
self.mdstream = None
|
|
451
|
+
except Exception:
|
|
452
|
+
pass # Ignore cleanup errors
|
|
453
|
+
|
|
454
|
+
# Clean up tool call streams on error
|
|
455
|
+
if hasattr(self, "tool_call_mdstreams") and self.tool_call_mdstreams:
|
|
456
|
+
try:
|
|
457
|
+
for stream in self.tool_call_mdstreams.values():
|
|
458
|
+
if stream:
|
|
459
|
+
try:
|
|
460
|
+
stream.close() # Direct close, no need to render on error
|
|
461
|
+
except Exception:
|
|
462
|
+
pass # Ignore individual stream cleanup errors
|
|
463
|
+
self.tool_call_mdstreams.clear()
|
|
464
|
+
except Exception:
|
|
465
|
+
pass # Ignore cleanup errors
|
|
466
|
+
raise e
|
|
467
|
+
|
|
468
|
+
def _live_incremental_response(
|
|
469
|
+
self,
|
|
470
|
+
delta_text: str,
|
|
471
|
+
response_content: str,
|
|
472
|
+
final: bool = False,
|
|
473
|
+
):
|
|
474
|
+
if self.mdstream:
|
|
475
|
+
# Process thinking tags
|
|
476
|
+
processed_content, should_render = self._process_thinking_tags(response_content)
|
|
477
|
+
|
|
478
|
+
if should_render or final:
|
|
479
|
+
self.mdstream.update(processed_content if processed_content else "", final)
|
|
480
|
+
# If should_render is False, pause mdstream.update temporarily
|
|
481
|
+
else:
|
|
482
|
+
if not self.config.io.pretty:
|
|
483
|
+
# For non-pretty mode, also need to process thinking tags
|
|
484
|
+
processed_delta, should_render = self._process_thinking_tags(delta_text)
|
|
485
|
+
if should_render or final:
|
|
486
|
+
self.config.io.console.print(processed_delta, sep="", end="")
|
|
487
|
+
|
|
488
|
+
def execute(self, turn_input: TurnInput) -> TurnOutput:
|
|
489
|
+
"""Execute AI conversation turn
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
turn_input: User input for conversation
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
TurnOutput: AI response
|
|
496
|
+
"""
|
|
497
|
+
self.input_data = turn_input
|
|
498
|
+
self.start_time = self._get_timestamp()
|
|
499
|
+
self.spinner = None
|
|
500
|
+
if self.config.io.pretty:
|
|
501
|
+
self.spinner = WaitingSpinner(f"Waiting for Agent {self.config.agent_name}...")
|
|
502
|
+
self.spinner.start()
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
# Import here to avoid circular imports
|
|
506
|
+
from siada.services.siada_runner import SiadaRunner
|
|
507
|
+
import asyncio
|
|
508
|
+
|
|
509
|
+
# Define async execution logic
|
|
510
|
+
async def _async_execute():
|
|
511
|
+
# Run agent for conversation
|
|
512
|
+
result: RunResultStreaming = await SiadaRunner.run_agent(
|
|
513
|
+
agent_name=self.config.agent_name,
|
|
514
|
+
user_input=turn_input.use_input,
|
|
515
|
+
workspace=self.config.workspace,
|
|
516
|
+
session=self.session,
|
|
517
|
+
stream=True,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
await self.output_stream_content(result)
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
# Use dedicated event loop to execute async tasks (reuse loop, maintain connection pool advantages)
|
|
524
|
+
self._ensure_dedicated_loop()
|
|
525
|
+
|
|
526
|
+
# Execute async task in dedicated loop
|
|
527
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
528
|
+
_async_execute(), self._dedicated_loop
|
|
529
|
+
)
|
|
530
|
+
result = future.result()
|
|
531
|
+
|
|
532
|
+
self.end_time = self._get_timestamp()
|
|
533
|
+
|
|
534
|
+
output = TurnOutput(
|
|
535
|
+
output=result.final_output,
|
|
536
|
+
metadata={
|
|
537
|
+
"agent_used": self.config.agent_name,
|
|
538
|
+
"execution_time": self.end_time - self.start_time,
|
|
539
|
+
},
|
|
540
|
+
next_action=None,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
self.output_data = output
|
|
544
|
+
return output
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
self.end_time = self._get_timestamp()
|
|
548
|
+
return self.handle_error(e)
|
|
549
|
+
|
|
550
|
+
def print_split_line(self):
|
|
551
|
+
if self.config.io.pretty:
|
|
552
|
+
self.config.io.rule(color=self.config.running_color_settings.split_line_color)
|
|
553
|
+
else:
|
|
554
|
+
self.config.io.console.print(SPLIT_TAG, end="")
|
|
555
|
+
|
|
556
|
+
def get_response_mdstream(self):
|
|
557
|
+
mdargs = dict(
|
|
558
|
+
style=self.config.running_color_settings.assistant_output_color,
|
|
559
|
+
code_theme=self.config.running_color_settings.code_theme,
|
|
560
|
+
inline_code_lexer="text",
|
|
561
|
+
)
|
|
562
|
+
mdStream = siada.io.components.mdstream.MarkdownRender(mdargs=mdargs)
|
|
563
|
+
return mdStream
|
|
564
|
+
|
|
565
|
+
def _stop_waiting_spinner(self):
|
|
566
|
+
"""Stop and clear the waiting spinner if it is running."""
|
|
567
|
+
spinner = getattr(self, "spinner", None)
|
|
568
|
+
if spinner:
|
|
569
|
+
try:
|
|
570
|
+
spinner.stop()
|
|
571
|
+
finally:
|
|
572
|
+
self.spinner = None
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class CommandTurn(RunTurn):
|
|
576
|
+
"""Handles slash command turns"""
|
|
577
|
+
|
|
578
|
+
def get_turn_type(self) -> TurnType:
|
|
579
|
+
return TurnType.COMMAND
|
|
580
|
+
|
|
581
|
+
def can_handle(self, user_input: str) -> bool:
|
|
582
|
+
"""Handle slash commands"""
|
|
583
|
+
return self.slash_commands.is_command(user_input)
|
|
584
|
+
|
|
585
|
+
def execute(self, turn_input: TurnInput) -> TurnOutput:
|
|
586
|
+
"""Execute slash command
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
turn_input: Command input
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
TurnOutput: Command result
|
|
593
|
+
"""
|
|
594
|
+
self.input_data = turn_input
|
|
595
|
+
self.start_time = self._get_timestamp()
|
|
596
|
+
|
|
597
|
+
try:
|
|
598
|
+
result = self.slash_commands.run(self.session, turn_input.use_input)
|
|
599
|
+
self.end_time = self._get_timestamp()
|
|
600
|
+
|
|
601
|
+
output = TurnOutput(
|
|
602
|
+
output=result,
|
|
603
|
+
metadata={"execution_time": self.end_time - self.start_time},
|
|
604
|
+
next_action=None,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
return output
|
|
608
|
+
|
|
609
|
+
except Exception as e:
|
|
610
|
+
self.end_time = self._get_timestamp()
|
|
611
|
+
return self.handle_error(e)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
class TurnFactory:
|
|
615
|
+
"""Factory for creating appropriate turn instances"""
|
|
616
|
+
|
|
617
|
+
@staticmethod
|
|
618
|
+
def create_turn(
|
|
619
|
+
config: RunningConfig, session: Any, slash_commands: Any, user_input: str
|
|
620
|
+
) -> RunTurn:
|
|
621
|
+
"""Create appropriate turn for user input
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
user_input: Raw user input
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
RunTurn: Appropriate turn handler
|
|
628
|
+
"""
|
|
629
|
+
# Always create new instances to avoid state pollution
|
|
630
|
+
turn_types = [
|
|
631
|
+
CommandTurn,
|
|
632
|
+
ConversationTurn,
|
|
633
|
+
]
|
|
634
|
+
|
|
635
|
+
for turn_class in turn_types:
|
|
636
|
+
# Create a temporary instance to test if it can handle the input
|
|
637
|
+
temp_turn = turn_class(config, session, slash_commands)
|
|
638
|
+
if temp_turn.can_handle(user_input):
|
|
639
|
+
return temp_turn
|
|
640
|
+
|
|
641
|
+
raise ValueError(f"No turn can handle the input: {user_input}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from siada.entrypoint.interaction.base_turn import RunTurn
|
|
4
|
+
from siada.entrypoint.interaction.config import RunningConfig
|
|
5
|
+
from siada.entrypoint.interaction.turn.run_turn import CommandTurn, ConversationTurn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TurnFactory:
|
|
9
|
+
"""Factory for creating appropriate turn instances"""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def create_turn(
|
|
13
|
+
config: RunningConfig, session: Any, slash_commands: Any, user_input: str
|
|
14
|
+
) -> RunTurn:
|
|
15
|
+
"""Create appropriate turn for user input
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
user_input: Raw user input
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
RunTurn: Appropriate turn handler
|
|
22
|
+
"""
|
|
23
|
+
# Always create new instances to avoid state pollution
|
|
24
|
+
turn_types = [
|
|
25
|
+
CommandTurn,
|
|
26
|
+
ConversationTurn,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
for turn_class in turn_types:
|
|
30
|
+
# Create a temporary instance to test if it can handle the input
|
|
31
|
+
temp_turn = turn_class(config, session, slash_commands)
|
|
32
|
+
if temp_turn.can_handle(user_input):
|
|
33
|
+
return temp_turn
|
|
34
|
+
|
|
35
|
+
raise ValueError(f"No turn can handle the input: {user_input}")
|