kweaver-dolphin 0.1.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.
- DolphinLanguageSDK/__init__.py +58 -0
- dolphin/__init__.py +62 -0
- dolphin/cli/__init__.py +20 -0
- dolphin/cli/args/__init__.py +9 -0
- dolphin/cli/args/parser.py +567 -0
- dolphin/cli/builtin_agents/__init__.py +22 -0
- dolphin/cli/commands/__init__.py +4 -0
- dolphin/cli/interrupt/__init__.py +8 -0
- dolphin/cli/interrupt/handler.py +205 -0
- dolphin/cli/interrupt/keyboard.py +82 -0
- dolphin/cli/main.py +49 -0
- dolphin/cli/multimodal/__init__.py +34 -0
- dolphin/cli/multimodal/clipboard.py +327 -0
- dolphin/cli/multimodal/handler.py +249 -0
- dolphin/cli/multimodal/image_processor.py +214 -0
- dolphin/cli/multimodal/input_parser.py +149 -0
- dolphin/cli/runner/__init__.py +8 -0
- dolphin/cli/runner/runner.py +989 -0
- dolphin/cli/ui/__init__.py +10 -0
- dolphin/cli/ui/console.py +2795 -0
- dolphin/cli/ui/input.py +340 -0
- dolphin/cli/ui/layout.py +425 -0
- dolphin/cli/ui/stream_renderer.py +302 -0
- dolphin/cli/utils/__init__.py +8 -0
- dolphin/cli/utils/helpers.py +135 -0
- dolphin/cli/utils/version.py +49 -0
- dolphin/core/__init__.py +107 -0
- dolphin/core/agent/__init__.py +10 -0
- dolphin/core/agent/agent_state.py +69 -0
- dolphin/core/agent/base_agent.py +970 -0
- dolphin/core/code_block/__init__.py +0 -0
- dolphin/core/code_block/agent_init_block.py +0 -0
- dolphin/core/code_block/assign_block.py +98 -0
- dolphin/core/code_block/basic_code_block.py +1865 -0
- dolphin/core/code_block/explore_block.py +1327 -0
- dolphin/core/code_block/explore_block_v2.py +712 -0
- dolphin/core/code_block/explore_strategy.py +672 -0
- dolphin/core/code_block/judge_block.py +220 -0
- dolphin/core/code_block/prompt_block.py +32 -0
- dolphin/core/code_block/skill_call_deduplicator.py +291 -0
- dolphin/core/code_block/tool_block.py +129 -0
- dolphin/core/common/__init__.py +17 -0
- dolphin/core/common/constants.py +176 -0
- dolphin/core/common/enums.py +1173 -0
- dolphin/core/common/exceptions.py +133 -0
- dolphin/core/common/multimodal.py +539 -0
- dolphin/core/common/object_type.py +165 -0
- dolphin/core/common/output_format.py +432 -0
- dolphin/core/common/types.py +36 -0
- dolphin/core/config/__init__.py +16 -0
- dolphin/core/config/global_config.py +1289 -0
- dolphin/core/config/ontology_config.py +133 -0
- dolphin/core/context/__init__.py +12 -0
- dolphin/core/context/context.py +1580 -0
- dolphin/core/context/context_manager.py +161 -0
- dolphin/core/context/var_output.py +82 -0
- dolphin/core/context/variable_pool.py +356 -0
- dolphin/core/context_engineer/__init__.py +41 -0
- dolphin/core/context_engineer/config/__init__.py +5 -0
- dolphin/core/context_engineer/config/settings.py +402 -0
- dolphin/core/context_engineer/core/__init__.py +7 -0
- dolphin/core/context_engineer/core/budget_manager.py +327 -0
- dolphin/core/context_engineer/core/context_assembler.py +583 -0
- dolphin/core/context_engineer/core/context_manager.py +637 -0
- dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
- dolphin/core/context_engineer/example/incremental_example.py +267 -0
- dolphin/core/context_engineer/example/traditional_example.py +334 -0
- dolphin/core/context_engineer/services/__init__.py +5 -0
- dolphin/core/context_engineer/services/compressor.py +399 -0
- dolphin/core/context_engineer/utils/__init__.py +6 -0
- dolphin/core/context_engineer/utils/context_utils.py +441 -0
- dolphin/core/context_engineer/utils/message_formatter.py +270 -0
- dolphin/core/context_engineer/utils/token_utils.py +139 -0
- dolphin/core/coroutine/__init__.py +15 -0
- dolphin/core/coroutine/context_snapshot.py +154 -0
- dolphin/core/coroutine/context_snapshot_profile.py +922 -0
- dolphin/core/coroutine/context_snapshot_store.py +268 -0
- dolphin/core/coroutine/execution_frame.py +145 -0
- dolphin/core/coroutine/execution_state_registry.py +161 -0
- dolphin/core/coroutine/resume_handle.py +101 -0
- dolphin/core/coroutine/step_result.py +101 -0
- dolphin/core/executor/__init__.py +18 -0
- dolphin/core/executor/debug_controller.py +630 -0
- dolphin/core/executor/dolphin_executor.py +1063 -0
- dolphin/core/executor/executor.py +624 -0
- dolphin/core/flags/__init__.py +27 -0
- dolphin/core/flags/definitions.py +49 -0
- dolphin/core/flags/manager.py +113 -0
- dolphin/core/hook/__init__.py +95 -0
- dolphin/core/hook/expression_evaluator.py +499 -0
- dolphin/core/hook/hook_dispatcher.py +380 -0
- dolphin/core/hook/hook_types.py +248 -0
- dolphin/core/hook/isolated_variable_pool.py +284 -0
- dolphin/core/interfaces.py +53 -0
- dolphin/core/llm/__init__.py +0 -0
- dolphin/core/llm/llm.py +495 -0
- dolphin/core/llm/llm_call.py +100 -0
- dolphin/core/llm/llm_client.py +1285 -0
- dolphin/core/llm/message_sanitizer.py +120 -0
- dolphin/core/logging/__init__.py +20 -0
- dolphin/core/logging/logger.py +526 -0
- dolphin/core/message/__init__.py +8 -0
- dolphin/core/message/compressor.py +749 -0
- dolphin/core/parser/__init__.py +8 -0
- dolphin/core/parser/parser.py +405 -0
- dolphin/core/runtime/__init__.py +10 -0
- dolphin/core/runtime/runtime_graph.py +926 -0
- dolphin/core/runtime/runtime_instance.py +446 -0
- dolphin/core/skill/__init__.py +14 -0
- dolphin/core/skill/context_retention.py +157 -0
- dolphin/core/skill/skill_function.py +686 -0
- dolphin/core/skill/skill_matcher.py +282 -0
- dolphin/core/skill/skillkit.py +700 -0
- dolphin/core/skill/skillset.py +72 -0
- dolphin/core/trajectory/__init__.py +10 -0
- dolphin/core/trajectory/recorder.py +189 -0
- dolphin/core/trajectory/trajectory.py +522 -0
- dolphin/core/utils/__init__.py +9 -0
- dolphin/core/utils/cache_kv.py +212 -0
- dolphin/core/utils/tools.py +340 -0
- dolphin/lib/__init__.py +93 -0
- dolphin/lib/debug/__init__.py +8 -0
- dolphin/lib/debug/visualizer.py +409 -0
- dolphin/lib/memory/__init__.py +28 -0
- dolphin/lib/memory/async_processor.py +220 -0
- dolphin/lib/memory/llm_calls.py +195 -0
- dolphin/lib/memory/manager.py +78 -0
- dolphin/lib/memory/sandbox.py +46 -0
- dolphin/lib/memory/storage.py +245 -0
- dolphin/lib/memory/utils.py +51 -0
- dolphin/lib/ontology/__init__.py +12 -0
- dolphin/lib/ontology/basic/__init__.py +0 -0
- dolphin/lib/ontology/basic/base.py +102 -0
- dolphin/lib/ontology/basic/concept.py +130 -0
- dolphin/lib/ontology/basic/object.py +11 -0
- dolphin/lib/ontology/basic/relation.py +63 -0
- dolphin/lib/ontology/datasource/__init__.py +27 -0
- dolphin/lib/ontology/datasource/datasource.py +66 -0
- dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
- dolphin/lib/ontology/datasource/sql.py +845 -0
- dolphin/lib/ontology/mapping.py +177 -0
- dolphin/lib/ontology/ontology.py +733 -0
- dolphin/lib/ontology/ontology_context.py +16 -0
- dolphin/lib/ontology/ontology_manager.py +107 -0
- dolphin/lib/skill_results/__init__.py +31 -0
- dolphin/lib/skill_results/cache_backend.py +559 -0
- dolphin/lib/skill_results/result_processor.py +181 -0
- dolphin/lib/skill_results/result_reference.py +179 -0
- dolphin/lib/skill_results/skillkit_hook.py +324 -0
- dolphin/lib/skill_results/strategies.py +328 -0
- dolphin/lib/skill_results/strategy_registry.py +150 -0
- dolphin/lib/skillkits/__init__.py +44 -0
- dolphin/lib/skillkits/agent_skillkit.py +155 -0
- dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
- dolphin/lib/skillkits/env_skillkit.py +250 -0
- dolphin/lib/skillkits/mcp_adapter.py +616 -0
- dolphin/lib/skillkits/mcp_skillkit.py +771 -0
- dolphin/lib/skillkits/memory_skillkit.py +650 -0
- dolphin/lib/skillkits/noop_skillkit.py +31 -0
- dolphin/lib/skillkits/ontology_skillkit.py +89 -0
- dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
- dolphin/lib/skillkits/resource/__init__.py +52 -0
- dolphin/lib/skillkits/resource/models/__init__.py +6 -0
- dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
- dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
- dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
- dolphin/lib/skillkits/resource/skill_cache.py +215 -0
- dolphin/lib/skillkits/resource/skill_loader.py +395 -0
- dolphin/lib/skillkits/resource/skill_validator.py +406 -0
- dolphin/lib/skillkits/resource_skillkit.py +11 -0
- dolphin/lib/skillkits/search_skillkit.py +163 -0
- dolphin/lib/skillkits/sql_skillkit.py +274 -0
- dolphin/lib/skillkits/system_skillkit.py +509 -0
- dolphin/lib/skillkits/vm_skillkit.py +65 -0
- dolphin/lib/utils/__init__.py +9 -0
- dolphin/lib/utils/data_process.py +207 -0
- dolphin/lib/utils/handle_progress.py +178 -0
- dolphin/lib/utils/security.py +139 -0
- dolphin/lib/utils/text_retrieval.py +462 -0
- dolphin/lib/vm/__init__.py +11 -0
- dolphin/lib/vm/env_executor.py +895 -0
- dolphin/lib/vm/python_session_manager.py +453 -0
- dolphin/lib/vm/vm.py +610 -0
- dolphin/sdk/__init__.py +60 -0
- dolphin/sdk/agent/__init__.py +12 -0
- dolphin/sdk/agent/agent_factory.py +236 -0
- dolphin/sdk/agent/dolphin_agent.py +1106 -0
- dolphin/sdk/api/__init__.py +4 -0
- dolphin/sdk/runtime/__init__.py +8 -0
- dolphin/sdk/runtime/env.py +363 -0
- dolphin/sdk/skill/__init__.py +10 -0
- dolphin/sdk/skill/global_skills.py +706 -0
- dolphin/sdk/skill/traditional_toolkit.py +260 -0
- kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
- kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
- kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
- kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
- kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
- kweaver_dolphin-0.1.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interrupt Token - Thread-safe signal bridge between CLI and Agent layers.
|
|
3
|
+
|
|
4
|
+
This module provides the InterruptToken class which bridges UI threads (prompt_toolkit)
|
|
5
|
+
and asyncio event loops, enabling safe communication of user interrupt signals.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Thread-safe interrupt signaling using threading.Event
|
|
9
|
+
- Bridges UI thread to asyncio event loop via run_coroutine_threadsafe
|
|
10
|
+
- Idempotent trigger_interrupt() to handle multiple key presses
|
|
11
|
+
- Clear separation from asyncio.Event used in Agent/Block layers
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
token = InterruptToken()
|
|
15
|
+
token.bind(agent, asyncio.get_running_loop())
|
|
16
|
+
|
|
17
|
+
# In UI thread (e.g., ESC key handler):
|
|
18
|
+
token.trigger_interrupt()
|
|
19
|
+
|
|
20
|
+
# After handling interrupt:
|
|
21
|
+
token.clear()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import threading
|
|
26
|
+
from typing import TYPE_CHECKING, Optional
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from dolphin.core.agent.base_agent import BaseAgent
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class InterruptToken:
|
|
33
|
+
"""Thread-safe user interrupt token (CLI -> Agent signal bridge).
|
|
34
|
+
|
|
35
|
+
Bridges UI threads (prompt_toolkit) and asyncio event loops,
|
|
36
|
+
transmitting interrupt signals to the execution layer via agent.interrupt().
|
|
37
|
+
|
|
38
|
+
Terminology Alignment:
|
|
39
|
+
- CLI Layer: InterruptToken.trigger_interrupt()
|
|
40
|
+
- Agent Layer: agent.interrupt() / agent.resume_with_input()
|
|
41
|
+
- Block Layer: context.check_user_interrupt() / raise UserInterrupt
|
|
42
|
+
|
|
43
|
+
Thread Safety:
|
|
44
|
+
- Uses threading.Event internally (thread-safe)
|
|
45
|
+
- Schedules agent.interrupt() via run_coroutine_threadsafe
|
|
46
|
+
|
|
47
|
+
Warning:
|
|
48
|
+
- NEVER directly access agent._interrupt_event from UI thread
|
|
49
|
+
- asyncio.Event is NOT thread-safe, always use this bridge
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
_interrupted: Thread-safe event flag
|
|
53
|
+
_agent: Bound agent instance (optional)
|
|
54
|
+
_loop: Bound event loop (optional)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self):
|
|
58
|
+
"""Initialize the interrupt token."""
|
|
59
|
+
self._interrupted = threading.Event()
|
|
60
|
+
self._agent: Optional["BaseAgent"] = None
|
|
61
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
62
|
+
self._pending_input: Optional[str] = None
|
|
63
|
+
self._realtime_input_buffer: str = "" # Real-time typing buffer
|
|
64
|
+
|
|
65
|
+
def bind(self, agent: "BaseAgent", loop: asyncio.AbstractEventLoop) -> None:
|
|
66
|
+
"""Bind agent instance and event loop.
|
|
67
|
+
|
|
68
|
+
Must be called before trigger_interrupt() to enable signal transmission.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
agent: The agent instance to interrupt
|
|
72
|
+
loop: The asyncio event loop running the agent
|
|
73
|
+
"""
|
|
74
|
+
self._agent = agent
|
|
75
|
+
self._loop = loop
|
|
76
|
+
|
|
77
|
+
def unbind(self) -> None:
|
|
78
|
+
"""Unbind agent and event loop.
|
|
79
|
+
|
|
80
|
+
Call this when the agent execution ends to prevent stale references.
|
|
81
|
+
"""
|
|
82
|
+
self._agent = None
|
|
83
|
+
self._loop = None
|
|
84
|
+
|
|
85
|
+
def trigger_interrupt(self) -> None:
|
|
86
|
+
"""Trigger user interrupt (called from UI thread, thread-safe).
|
|
87
|
+
|
|
88
|
+
This method is idempotent - calling it multiple times has no additional effect.
|
|
89
|
+
|
|
90
|
+
The interrupt signal is transmitted to the agent layer by scheduling
|
|
91
|
+
agent.interrupt() on the bound event loop using run_coroutine_threadsafe.
|
|
92
|
+
"""
|
|
93
|
+
if self._interrupted.is_set():
|
|
94
|
+
return # Idempotent
|
|
95
|
+
|
|
96
|
+
self._interrupted.set()
|
|
97
|
+
|
|
98
|
+
# Cross-thread scheduling of agent.interrupt()
|
|
99
|
+
if self._agent and self._loop:
|
|
100
|
+
try:
|
|
101
|
+
asyncio.run_coroutine_threadsafe(
|
|
102
|
+
self._agent.interrupt(),
|
|
103
|
+
self._loop
|
|
104
|
+
)
|
|
105
|
+
except Exception:
|
|
106
|
+
# Ignore errors if loop is closed or agent is unavailable
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
def is_interrupted(self) -> bool:
|
|
110
|
+
"""Check if interrupt has been triggered.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if interrupt was triggered, False otherwise
|
|
114
|
+
"""
|
|
115
|
+
return self._interrupted.is_set()
|
|
116
|
+
|
|
117
|
+
def clear(self) -> None:
|
|
118
|
+
"""Clear interrupt state (call before starting a new execution round).
|
|
119
|
+
|
|
120
|
+
This resets the token for the next user input cycle.
|
|
121
|
+
Also clears any pending user input and real-time buffer.
|
|
122
|
+
"""
|
|
123
|
+
self._interrupted.clear()
|
|
124
|
+
self._pending_input = None
|
|
125
|
+
self._realtime_input_buffer = ""
|
|
126
|
+
|
|
127
|
+
def append_realtime_input(self, char: str) -> str:
|
|
128
|
+
"""Append a character to the real-time input buffer.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
char: Character to append
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The full current buffer
|
|
135
|
+
"""
|
|
136
|
+
if char == "\x1b": # ESC
|
|
137
|
+
return self._realtime_input_buffer
|
|
138
|
+
|
|
139
|
+
if char in ("\x7f", "\x08"): # Backspace
|
|
140
|
+
self._realtime_input_buffer = self._realtime_input_buffer[:-1]
|
|
141
|
+
else:
|
|
142
|
+
self._realtime_input_buffer += char
|
|
143
|
+
return self._realtime_input_buffer
|
|
144
|
+
|
|
145
|
+
def get_realtime_input(self, consume: bool = True) -> str:
|
|
146
|
+
"""Get the current real-time input buffer.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
consume: Whether to clear the buffer after reading
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The current buffer string
|
|
153
|
+
"""
|
|
154
|
+
res = self._realtime_input_buffer
|
|
155
|
+
if consume:
|
|
156
|
+
self._realtime_input_buffer = ""
|
|
157
|
+
return res
|
|
158
|
+
|
|
159
|
+
def set_pending_input(self, user_input: Optional[str]) -> None:
|
|
160
|
+
"""Store pending user input for resume.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
user_input: The user's new instruction after interrupt
|
|
164
|
+
"""
|
|
165
|
+
self._pending_input = user_input
|
|
166
|
+
|
|
167
|
+
def get_pending_input(self) -> Optional[str]:
|
|
168
|
+
"""Get and clear pending user input.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The pending user input, or None if no input is pending
|
|
172
|
+
"""
|
|
173
|
+
result = self._pending_input
|
|
174
|
+
self._pending_input = None
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
async def wait_for_interrupt(self, timeout: Optional[float] = None) -> bool:
|
|
178
|
+
"""Async wait for interrupt signal (for use in asyncio code).
|
|
179
|
+
|
|
180
|
+
This is a convenience method for asyncio code that needs to wait
|
|
181
|
+
for an interrupt signal without blocking the event loop.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
timeout: Maximum time to wait in seconds, None for indefinite
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
True if interrupted, False if timeout
|
|
188
|
+
"""
|
|
189
|
+
loop = asyncio.get_running_loop()
|
|
190
|
+
|
|
191
|
+
def check_interrupted():
|
|
192
|
+
return self._interrupted.wait(timeout=0.1)
|
|
193
|
+
|
|
194
|
+
start_time = loop.time()
|
|
195
|
+
while True:
|
|
196
|
+
if self._interrupted.is_set():
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
if timeout is not None:
|
|
200
|
+
elapsed = loop.time() - start_time
|
|
201
|
+
if elapsed >= timeout:
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# Yield control to allow other coroutines to run
|
|
205
|
+
await asyncio.sleep(0.05)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
import sys
|
|
5
|
+
import select
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
async def _monitor_interrupt(token, stop_event: threading.Event):
|
|
9
|
+
"""Monitor stdin for ESC key in a separate thread."""
|
|
10
|
+
loop = asyncio.get_running_loop()
|
|
11
|
+
try:
|
|
12
|
+
await loop.run_in_executor(None, _blocking_stdin_monitor, token, stop_event)
|
|
13
|
+
except:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def _blocking_stdin_monitor(token, stop_event: threading.Event):
|
|
17
|
+
"""Blocking monitor for ESC key using select/termios."""
|
|
18
|
+
import tty
|
|
19
|
+
import termios
|
|
20
|
+
|
|
21
|
+
if not sys.stdin.isatty():
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
fd = sys.stdin.fileno()
|
|
25
|
+
try:
|
|
26
|
+
old_settings = termios.tcgetattr(fd)
|
|
27
|
+
except:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# Set to cbreak mode to read keys without waiting for newline
|
|
32
|
+
tty.setcbreak(fd)
|
|
33
|
+
while not stop_event.is_set():
|
|
34
|
+
# Check if input is available (timeout 0.1s)
|
|
35
|
+
if select.select([sys.stdin], [], [], 0.1)[0]:
|
|
36
|
+
try:
|
|
37
|
+
key = sys.stdin.read(1)
|
|
38
|
+
if key == '\x1b': # ESC code
|
|
39
|
+
token.trigger_interrupt()
|
|
40
|
+
# Once triggered, we can stop
|
|
41
|
+
break
|
|
42
|
+
elif key == '\x03': # Ctrl-C
|
|
43
|
+
# Also trigger interrupt on Ctrl-C
|
|
44
|
+
token.trigger_interrupt()
|
|
45
|
+
break
|
|
46
|
+
elif key in ('\r', '\n'): # Enter
|
|
47
|
+
# Treat Enter as an interrupt signal only if there is buffered text
|
|
48
|
+
# This makes the UI feel like a real-time chat
|
|
49
|
+
if token._realtime_input_buffer:
|
|
50
|
+
token.trigger_interrupt()
|
|
51
|
+
break
|
|
52
|
+
# Otherwise ignore empty Enter
|
|
53
|
+
else:
|
|
54
|
+
# Append to buffer and ECHO to the fixed input line
|
|
55
|
+
buffer = token.append_realtime_input(key)
|
|
56
|
+
|
|
57
|
+
# Echoing with cursor preservation
|
|
58
|
+
try:
|
|
59
|
+
import shutil
|
|
60
|
+
height = shutil.get_terminal_size().lines
|
|
61
|
+
# Save cursor, move to bottom line, clear, draw prompt + buffer, restore
|
|
62
|
+
# We use atomic write to minimize flickering
|
|
63
|
+
echo_output = (
|
|
64
|
+
f"\0337" # Save cursor
|
|
65
|
+
f"\033[{height};1H" # Move to bottom line
|
|
66
|
+
f"\033[K> {buffer}█" # Clear and draw with block cursor
|
|
67
|
+
f"\0338" # Restore cursor
|
|
68
|
+
)
|
|
69
|
+
sys.stdout.write(echo_output)
|
|
70
|
+
sys.stdout.flush()
|
|
71
|
+
except:
|
|
72
|
+
pass
|
|
73
|
+
except:
|
|
74
|
+
break
|
|
75
|
+
except:
|
|
76
|
+
pass
|
|
77
|
+
finally:
|
|
78
|
+
# ABSOLUTELY ESSENTIAL: Restore original terminal settings
|
|
79
|
+
try:
|
|
80
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
81
|
+
except:
|
|
82
|
+
pass
|
dolphin/cli/main.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main entry point for Dolphin CLI
|
|
3
|
+
|
|
4
|
+
This module provides the main() function that serves as the CLI entry point.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from dolphin.cli.args.parser import parseArgs
|
|
11
|
+
from dolphin.cli.runner.runner import runDolphin
|
|
12
|
+
from dolphin.cli.utils.helpers import setupFlagsFromArgs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main() -> None:
|
|
16
|
+
"""Main CLI entry point
|
|
17
|
+
|
|
18
|
+
This function:
|
|
19
|
+
1. Parses command-line arguments
|
|
20
|
+
2. Sets up logging
|
|
21
|
+
3. Configures feature flags
|
|
22
|
+
4. Runs the dolphin program
|
|
23
|
+
"""
|
|
24
|
+
args = parseArgs()
|
|
25
|
+
|
|
26
|
+
# Setup logging
|
|
27
|
+
from dolphin.core.logging.logger import setup_default_logger, set_log_level
|
|
28
|
+
|
|
29
|
+
setup_default_logger(args.logSuffix)
|
|
30
|
+
|
|
31
|
+
if args.logLevel:
|
|
32
|
+
logLevelMap = {
|
|
33
|
+
"DEBUG": logging.DEBUG,
|
|
34
|
+
"INFO": logging.INFO,
|
|
35
|
+
"WARNING": logging.WARNING,
|
|
36
|
+
"ERROR": logging.ERROR,
|
|
37
|
+
}
|
|
38
|
+
set_log_level(logLevelMap.get(args.logLevel, logging.INFO))
|
|
39
|
+
|
|
40
|
+
# Setup feature flags
|
|
41
|
+
setupFlagsFromArgs(args)
|
|
42
|
+
|
|
43
|
+
# Run
|
|
44
|
+
asyncio.run(runDolphin(args))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
49
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multimodal input module for Dolphin CLI.
|
|
3
|
+
|
|
4
|
+
This module provides support for multimodal input in the CLI, including:
|
|
5
|
+
- Clipboard image reading (@paste)
|
|
6
|
+
- File path references (@image:<path>)
|
|
7
|
+
- URL references (@url:<url>)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dolphin.cli.multimodal.input_parser import (
|
|
11
|
+
MultimodalInputParser,
|
|
12
|
+
ParsedMultimodalInput,
|
|
13
|
+
ImageReference,
|
|
14
|
+
ImageSourceType,
|
|
15
|
+
)
|
|
16
|
+
from dolphin.cli.multimodal.clipboard import ClipboardImageReader
|
|
17
|
+
from dolphin.cli.multimodal.image_processor import ImageProcessor, ImageProcessConfig
|
|
18
|
+
from dolphin.cli.multimodal.handler import (
|
|
19
|
+
MultimodalInputHandler,
|
|
20
|
+
process_multimodal_input,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"MultimodalInputParser",
|
|
25
|
+
"ParsedMultimodalInput",
|
|
26
|
+
"ImageReference",
|
|
27
|
+
"ImageSourceType",
|
|
28
|
+
"ClipboardImageReader",
|
|
29
|
+
"ImageProcessor",
|
|
30
|
+
"ImageProcessConfig",
|
|
31
|
+
"MultimodalInputHandler",
|
|
32
|
+
"process_multimodal_input",
|
|
33
|
+
]
|
|
34
|
+
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clipboard image reader for multimodal CLI input.
|
|
3
|
+
|
|
4
|
+
Provides cross-platform clipboard image reading functionality.
|
|
5
|
+
|
|
6
|
+
Platform Support (Linux-first for commercial deployments):
|
|
7
|
+
- Linux X11: Primary target, requires PIL and xclip/xsel
|
|
8
|
+
- macOS: Secondary platform, requires pyobjc (AppKit)
|
|
9
|
+
- macOS: Secondary platform, requires pyobjc (AppKit)
|
|
10
|
+
- Windows: Fallback, requires PIL
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
import base64
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from dolphin.core.common.multimodal import ClipboardEmptyError
|
|
20
|
+
from dolphin.core.logging.logger import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger("clipboard")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ClipboardImageReader:
|
|
26
|
+
"""Cross-platform clipboard image reader.
|
|
27
|
+
|
|
28
|
+
Platform priority (Linux-first for commercial deployments):
|
|
29
|
+
- Linux X11: Primary target, uses Pillow's ImageGrab
|
|
30
|
+
- Linux Wayland: Quick fail with clear message
|
|
31
|
+
- macOS: Secondary platform, uses AppKit/Cocoa APIs
|
|
32
|
+
- Windows: Fallback, uses Pillow's ImageGrab
|
|
33
|
+
reader = ClipboardImageReader()
|
|
34
|
+
data = reader.read()
|
|
35
|
+
if data:
|
|
36
|
+
url = reader.to_base64_url(data)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def read(self) -> Optional[bytes]:
|
|
40
|
+
"""Read image data from clipboard.
|
|
41
|
+
|
|
42
|
+
Platform detection order (Linux-first for commercial deployments):
|
|
43
|
+
1. Linux (X11) - Primary target for commercial versions
|
|
44
|
+
2. Linux (Wayland) - Quick fail with clear message
|
|
45
|
+
3. macOS - Secondary platform
|
|
46
|
+
4. Windows - Fallback
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
PNG image data as bytes, or None if no image in clipboard
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
On failure, logs a warning with platform-specific guidance.
|
|
53
|
+
"""
|
|
54
|
+
platform = sys.platform
|
|
55
|
+
import os
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# Linux-first: Commercial versions primarily run on Linux
|
|
59
|
+
if platform.startswith("linux"):
|
|
60
|
+
# Quick check for Wayland (not supported)
|
|
61
|
+
if "WAYLAND_DISPLAY" in os.environ:
|
|
62
|
+
logger.warning(
|
|
63
|
+
"Clipboard image read not supported on Wayland. "
|
|
64
|
+
"Please use file upload instead: /image path/to/image.png"
|
|
65
|
+
)
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
# Try Linux X11 method (Pillow ImageGrab)
|
|
69
|
+
data = self._read_linux()
|
|
70
|
+
if data is not None:
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
# macOS fallback
|
|
74
|
+
elif platform == "darwin":
|
|
75
|
+
data = self._read_macos()
|
|
76
|
+
if data is not None:
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
# Windows fallback
|
|
80
|
+
elif platform == "win32":
|
|
81
|
+
data = self._read_windows()
|
|
82
|
+
if data is not None:
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
# No image found - this is normal, not an error
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
# Log platform-specific guidance
|
|
90
|
+
self._log_platform_error(platform, e)
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def _log_platform_error(self, platform: str, error: Exception) -> None:
|
|
94
|
+
"""Log platform-specific error message with helpful guidance.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
platform: sys.platform value
|
|
98
|
+
error: The exception that occurred
|
|
99
|
+
"""
|
|
100
|
+
if platform == "darwin":
|
|
101
|
+
logger.warning(
|
|
102
|
+
f"Clipboard image read failed on macOS: {error}. "
|
|
103
|
+
f"Install pyobjc: pip install pyobjc-framework-Cocoa"
|
|
104
|
+
)
|
|
105
|
+
elif platform.startswith("linux"):
|
|
106
|
+
# Detect Wayland
|
|
107
|
+
wayland_display = sys.platform.startswith("linux") and "WAYLAND_DISPLAY" in __import__("os").environ
|
|
108
|
+
if wayland_display:
|
|
109
|
+
logger.warning(
|
|
110
|
+
f"Clipboard image read not supported on Wayland. "
|
|
111
|
+
f"Please use file upload instead: /image path/to/image.png"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
logger.warning(
|
|
115
|
+
f"Clipboard image read failed on Linux: {error}. "
|
|
116
|
+
f"Install dependencies: pip install pillow && sudo apt-get install xclip"
|
|
117
|
+
)
|
|
118
|
+
elif platform == "win32":
|
|
119
|
+
logger.warning(
|
|
120
|
+
f"Clipboard image read failed on Windows: {error}. "
|
|
121
|
+
f"Install Pillow: pip install pillow"
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
logger.warning(
|
|
125
|
+
f"Clipboard image read failed on {platform}: {error}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _read_macos(self) -> Optional[bytes]:
|
|
129
|
+
"""Read clipboard on macOS using AppKit.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Image data as PNG bytes, or None if not available
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ImportError: If pyobjc is not installed
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
from AppKit import NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeTIFF
|
|
139
|
+
|
|
140
|
+
pb = NSPasteboard.generalPasteboard()
|
|
141
|
+
|
|
142
|
+
# Try PNG first
|
|
143
|
+
data = pb.dataForType_(NSPasteboardTypePNG)
|
|
144
|
+
if data:
|
|
145
|
+
return bytes(data)
|
|
146
|
+
|
|
147
|
+
# Try TIFF (macOS screenshots are often TIFF)
|
|
148
|
+
data = pb.dataForType_(NSPasteboardTypeTIFF)
|
|
149
|
+
if data:
|
|
150
|
+
return self._convert_to_png(bytes(data))
|
|
151
|
+
|
|
152
|
+
return None
|
|
153
|
+
except ImportError as e:
|
|
154
|
+
# AppKit not available - re-raise for proper error handling
|
|
155
|
+
raise ImportError(
|
|
156
|
+
"pyobjc not installed. Install with: pip install pyobjc-framework-Cocoa"
|
|
157
|
+
) from e
|
|
158
|
+
|
|
159
|
+
def _convert_to_png(self, image_data: bytes) -> bytes:
|
|
160
|
+
"""Convert image data to PNG format.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
image_data: Image data in any format supported by Pillow
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
PNG image data
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
from PIL import Image
|
|
170
|
+
|
|
171
|
+
img = Image.open(io.BytesIO(image_data))
|
|
172
|
+
output = io.BytesIO()
|
|
173
|
+
|
|
174
|
+
# Handle RGBA to RGB conversion if needed
|
|
175
|
+
if img.mode in ('RGBA', 'LA') or (img.mode == 'P' and 'transparency' in img.info):
|
|
176
|
+
# Keep as PNG to preserve transparency
|
|
177
|
+
img.save(output, format='PNG')
|
|
178
|
+
else:
|
|
179
|
+
img.save(output, format='PNG')
|
|
180
|
+
|
|
181
|
+
return output.getvalue()
|
|
182
|
+
except Exception as e:
|
|
183
|
+
raise RuntimeError(f"Failed to convert image to PNG: {e}")
|
|
184
|
+
|
|
185
|
+
def _read_linux(self) -> Optional[bytes]:
|
|
186
|
+
"""Read clipboard on Linux X11 using Pillow's ImageGrab.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Image data as PNG bytes, or None if not available
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ImportError: If PIL is not installed
|
|
193
|
+
RuntimeError: If clipboard access fails (e.g., no X server)
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
from PIL import ImageGrab
|
|
197
|
+
|
|
198
|
+
img = ImageGrab.grabclipboard()
|
|
199
|
+
if img is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
output = io.BytesIO()
|
|
203
|
+
img.save(output, format='PNG')
|
|
204
|
+
return output.getvalue()
|
|
205
|
+
except ImportError as e:
|
|
206
|
+
# Pillow not installed - re-raise for proper error handling
|
|
207
|
+
raise ImportError(
|
|
208
|
+
"Pillow not installed. Install with: pip install pillow"
|
|
209
|
+
) from e
|
|
210
|
+
|
|
211
|
+
def _read_windows(self) -> Optional[bytes]:
|
|
212
|
+
"""Read clipboard on Windows using Pillow's ImageGrab.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Image data as PNG bytes, or None if not available
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ImportError: If PIL is not installed
|
|
219
|
+
RuntimeError: If clipboard access fails
|
|
220
|
+
"""
|
|
221
|
+
try:
|
|
222
|
+
from PIL import ImageGrab
|
|
223
|
+
|
|
224
|
+
img = ImageGrab.grabclipboard()
|
|
225
|
+
if img is None:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
output = io.BytesIO()
|
|
229
|
+
img.save(output, format='PNG')
|
|
230
|
+
return output.getvalue()
|
|
231
|
+
except ImportError as e:
|
|
232
|
+
# Pillow not installed - re-raise for proper error handling
|
|
233
|
+
raise ImportError(
|
|
234
|
+
"Pillow not installed. Install with: pip install pillow"
|
|
235
|
+
) from e
|
|
236
|
+
|
|
237
|
+
def to_base64_url(self, image_data: bytes, mime_type: str = "image/png") -> str:
|
|
238
|
+
"""Convert image data to base64 data URL.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
image_data: Raw image bytes
|
|
242
|
+
mime_type: MIME type of the image (default: image/png)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Data URL string (e.g., "data:image/png;base64,...")
|
|
246
|
+
"""
|
|
247
|
+
b64 = base64.b64encode(image_data).decode('utf-8')
|
|
248
|
+
return f"data:{mime_type};base64,{b64}"
|
|
249
|
+
|
|
250
|
+
def read_as_base64_url(self) -> str:
|
|
251
|
+
"""Read clipboard image and return as base64 data URL.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Data URL string
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ClipboardEmptyError: If no image in clipboard
|
|
258
|
+
"""
|
|
259
|
+
data = self.read()
|
|
260
|
+
if data is None:
|
|
261
|
+
raise ClipboardEmptyError("No image found in clipboard")
|
|
262
|
+
return self.to_base64_url(data)
|
|
263
|
+
|
|
264
|
+
def has_image(self) -> bool:
|
|
265
|
+
"""Check if clipboard contains an image.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
True if clipboard has an image
|
|
269
|
+
"""
|
|
270
|
+
return self.read() is not None
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def get_platform_support_info() -> dict:
|
|
274
|
+
"""Get platform-specific clipboard support information.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Dictionary with platform support details
|
|
278
|
+
"""
|
|
279
|
+
platform = sys.platform
|
|
280
|
+
import os
|
|
281
|
+
|
|
282
|
+
info = {
|
|
283
|
+
"platform": platform,
|
|
284
|
+
"supported": True,
|
|
285
|
+
"requirements": [],
|
|
286
|
+
"notes": []
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if platform == "darwin":
|
|
290
|
+
info["requirements"] = ["pyobjc-framework-Cocoa"]
|
|
291
|
+
info["notes"] = ["May require clipboard access permission in System Preferences"]
|
|
292
|
+
try:
|
|
293
|
+
import AppKit
|
|
294
|
+
info["installed"] = True
|
|
295
|
+
except ImportError:
|
|
296
|
+
info["installed"] = False
|
|
297
|
+
|
|
298
|
+
elif platform.startswith("linux"):
|
|
299
|
+
wayland = "WAYLAND_DISPLAY" in os.environ
|
|
300
|
+
if wayland:
|
|
301
|
+
info["supported"] = False
|
|
302
|
+
info["notes"] = [
|
|
303
|
+
"Wayland display server detected",
|
|
304
|
+
"Clipboard image reading not supported",
|
|
305
|
+
"Use file upload instead: /image path/to/file.png"
|
|
306
|
+
]
|
|
307
|
+
else:
|
|
308
|
+
info["requirements"] = ["pillow", "xclip or xsel (system package)"]
|
|
309
|
+
info["notes"] = ["Requires X11 display server"]
|
|
310
|
+
try:
|
|
311
|
+
from PIL import ImageGrab
|
|
312
|
+
info["installed"] = True
|
|
313
|
+
except ImportError:
|
|
314
|
+
info["installed"] = False
|
|
315
|
+
|
|
316
|
+
elif platform == "win32":
|
|
317
|
+
info["requirements"] = ["pillow"]
|
|
318
|
+
try:
|
|
319
|
+
from PIL import ImageGrab
|
|
320
|
+
info["installed"] = True
|
|
321
|
+
except ImportError:
|
|
322
|
+
info["installed"] = False
|
|
323
|
+
else:
|
|
324
|
+
info["supported"] = False
|
|
325
|
+
info["notes"] = [f"Platform '{platform}' not explicitly supported"]
|
|
326
|
+
|
|
327
|
+
return info
|