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.
Files changed (199) hide show
  1. DolphinLanguageSDK/__init__.py +58 -0
  2. dolphin/__init__.py +62 -0
  3. dolphin/cli/__init__.py +20 -0
  4. dolphin/cli/args/__init__.py +9 -0
  5. dolphin/cli/args/parser.py +567 -0
  6. dolphin/cli/builtin_agents/__init__.py +22 -0
  7. dolphin/cli/commands/__init__.py +4 -0
  8. dolphin/cli/interrupt/__init__.py +8 -0
  9. dolphin/cli/interrupt/handler.py +205 -0
  10. dolphin/cli/interrupt/keyboard.py +82 -0
  11. dolphin/cli/main.py +49 -0
  12. dolphin/cli/multimodal/__init__.py +34 -0
  13. dolphin/cli/multimodal/clipboard.py +327 -0
  14. dolphin/cli/multimodal/handler.py +249 -0
  15. dolphin/cli/multimodal/image_processor.py +214 -0
  16. dolphin/cli/multimodal/input_parser.py +149 -0
  17. dolphin/cli/runner/__init__.py +8 -0
  18. dolphin/cli/runner/runner.py +989 -0
  19. dolphin/cli/ui/__init__.py +10 -0
  20. dolphin/cli/ui/console.py +2795 -0
  21. dolphin/cli/ui/input.py +340 -0
  22. dolphin/cli/ui/layout.py +425 -0
  23. dolphin/cli/ui/stream_renderer.py +302 -0
  24. dolphin/cli/utils/__init__.py +8 -0
  25. dolphin/cli/utils/helpers.py +135 -0
  26. dolphin/cli/utils/version.py +49 -0
  27. dolphin/core/__init__.py +107 -0
  28. dolphin/core/agent/__init__.py +10 -0
  29. dolphin/core/agent/agent_state.py +69 -0
  30. dolphin/core/agent/base_agent.py +970 -0
  31. dolphin/core/code_block/__init__.py +0 -0
  32. dolphin/core/code_block/agent_init_block.py +0 -0
  33. dolphin/core/code_block/assign_block.py +98 -0
  34. dolphin/core/code_block/basic_code_block.py +1865 -0
  35. dolphin/core/code_block/explore_block.py +1327 -0
  36. dolphin/core/code_block/explore_block_v2.py +712 -0
  37. dolphin/core/code_block/explore_strategy.py +672 -0
  38. dolphin/core/code_block/judge_block.py +220 -0
  39. dolphin/core/code_block/prompt_block.py +32 -0
  40. dolphin/core/code_block/skill_call_deduplicator.py +291 -0
  41. dolphin/core/code_block/tool_block.py +129 -0
  42. dolphin/core/common/__init__.py +17 -0
  43. dolphin/core/common/constants.py +176 -0
  44. dolphin/core/common/enums.py +1173 -0
  45. dolphin/core/common/exceptions.py +133 -0
  46. dolphin/core/common/multimodal.py +539 -0
  47. dolphin/core/common/object_type.py +165 -0
  48. dolphin/core/common/output_format.py +432 -0
  49. dolphin/core/common/types.py +36 -0
  50. dolphin/core/config/__init__.py +16 -0
  51. dolphin/core/config/global_config.py +1289 -0
  52. dolphin/core/config/ontology_config.py +133 -0
  53. dolphin/core/context/__init__.py +12 -0
  54. dolphin/core/context/context.py +1580 -0
  55. dolphin/core/context/context_manager.py +161 -0
  56. dolphin/core/context/var_output.py +82 -0
  57. dolphin/core/context/variable_pool.py +356 -0
  58. dolphin/core/context_engineer/__init__.py +41 -0
  59. dolphin/core/context_engineer/config/__init__.py +5 -0
  60. dolphin/core/context_engineer/config/settings.py +402 -0
  61. dolphin/core/context_engineer/core/__init__.py +7 -0
  62. dolphin/core/context_engineer/core/budget_manager.py +327 -0
  63. dolphin/core/context_engineer/core/context_assembler.py +583 -0
  64. dolphin/core/context_engineer/core/context_manager.py +637 -0
  65. dolphin/core/context_engineer/core/tokenizer_service.py +260 -0
  66. dolphin/core/context_engineer/example/incremental_example.py +267 -0
  67. dolphin/core/context_engineer/example/traditional_example.py +334 -0
  68. dolphin/core/context_engineer/services/__init__.py +5 -0
  69. dolphin/core/context_engineer/services/compressor.py +399 -0
  70. dolphin/core/context_engineer/utils/__init__.py +6 -0
  71. dolphin/core/context_engineer/utils/context_utils.py +441 -0
  72. dolphin/core/context_engineer/utils/message_formatter.py +270 -0
  73. dolphin/core/context_engineer/utils/token_utils.py +139 -0
  74. dolphin/core/coroutine/__init__.py +15 -0
  75. dolphin/core/coroutine/context_snapshot.py +154 -0
  76. dolphin/core/coroutine/context_snapshot_profile.py +922 -0
  77. dolphin/core/coroutine/context_snapshot_store.py +268 -0
  78. dolphin/core/coroutine/execution_frame.py +145 -0
  79. dolphin/core/coroutine/execution_state_registry.py +161 -0
  80. dolphin/core/coroutine/resume_handle.py +101 -0
  81. dolphin/core/coroutine/step_result.py +101 -0
  82. dolphin/core/executor/__init__.py +18 -0
  83. dolphin/core/executor/debug_controller.py +630 -0
  84. dolphin/core/executor/dolphin_executor.py +1063 -0
  85. dolphin/core/executor/executor.py +624 -0
  86. dolphin/core/flags/__init__.py +27 -0
  87. dolphin/core/flags/definitions.py +49 -0
  88. dolphin/core/flags/manager.py +113 -0
  89. dolphin/core/hook/__init__.py +95 -0
  90. dolphin/core/hook/expression_evaluator.py +499 -0
  91. dolphin/core/hook/hook_dispatcher.py +380 -0
  92. dolphin/core/hook/hook_types.py +248 -0
  93. dolphin/core/hook/isolated_variable_pool.py +284 -0
  94. dolphin/core/interfaces.py +53 -0
  95. dolphin/core/llm/__init__.py +0 -0
  96. dolphin/core/llm/llm.py +495 -0
  97. dolphin/core/llm/llm_call.py +100 -0
  98. dolphin/core/llm/llm_client.py +1285 -0
  99. dolphin/core/llm/message_sanitizer.py +120 -0
  100. dolphin/core/logging/__init__.py +20 -0
  101. dolphin/core/logging/logger.py +526 -0
  102. dolphin/core/message/__init__.py +8 -0
  103. dolphin/core/message/compressor.py +749 -0
  104. dolphin/core/parser/__init__.py +8 -0
  105. dolphin/core/parser/parser.py +405 -0
  106. dolphin/core/runtime/__init__.py +10 -0
  107. dolphin/core/runtime/runtime_graph.py +926 -0
  108. dolphin/core/runtime/runtime_instance.py +446 -0
  109. dolphin/core/skill/__init__.py +14 -0
  110. dolphin/core/skill/context_retention.py +157 -0
  111. dolphin/core/skill/skill_function.py +686 -0
  112. dolphin/core/skill/skill_matcher.py +282 -0
  113. dolphin/core/skill/skillkit.py +700 -0
  114. dolphin/core/skill/skillset.py +72 -0
  115. dolphin/core/trajectory/__init__.py +10 -0
  116. dolphin/core/trajectory/recorder.py +189 -0
  117. dolphin/core/trajectory/trajectory.py +522 -0
  118. dolphin/core/utils/__init__.py +9 -0
  119. dolphin/core/utils/cache_kv.py +212 -0
  120. dolphin/core/utils/tools.py +340 -0
  121. dolphin/lib/__init__.py +93 -0
  122. dolphin/lib/debug/__init__.py +8 -0
  123. dolphin/lib/debug/visualizer.py +409 -0
  124. dolphin/lib/memory/__init__.py +28 -0
  125. dolphin/lib/memory/async_processor.py +220 -0
  126. dolphin/lib/memory/llm_calls.py +195 -0
  127. dolphin/lib/memory/manager.py +78 -0
  128. dolphin/lib/memory/sandbox.py +46 -0
  129. dolphin/lib/memory/storage.py +245 -0
  130. dolphin/lib/memory/utils.py +51 -0
  131. dolphin/lib/ontology/__init__.py +12 -0
  132. dolphin/lib/ontology/basic/__init__.py +0 -0
  133. dolphin/lib/ontology/basic/base.py +102 -0
  134. dolphin/lib/ontology/basic/concept.py +130 -0
  135. dolphin/lib/ontology/basic/object.py +11 -0
  136. dolphin/lib/ontology/basic/relation.py +63 -0
  137. dolphin/lib/ontology/datasource/__init__.py +27 -0
  138. dolphin/lib/ontology/datasource/datasource.py +66 -0
  139. dolphin/lib/ontology/datasource/oracle_datasource.py +338 -0
  140. dolphin/lib/ontology/datasource/sql.py +845 -0
  141. dolphin/lib/ontology/mapping.py +177 -0
  142. dolphin/lib/ontology/ontology.py +733 -0
  143. dolphin/lib/ontology/ontology_context.py +16 -0
  144. dolphin/lib/ontology/ontology_manager.py +107 -0
  145. dolphin/lib/skill_results/__init__.py +31 -0
  146. dolphin/lib/skill_results/cache_backend.py +559 -0
  147. dolphin/lib/skill_results/result_processor.py +181 -0
  148. dolphin/lib/skill_results/result_reference.py +179 -0
  149. dolphin/lib/skill_results/skillkit_hook.py +324 -0
  150. dolphin/lib/skill_results/strategies.py +328 -0
  151. dolphin/lib/skill_results/strategy_registry.py +150 -0
  152. dolphin/lib/skillkits/__init__.py +44 -0
  153. dolphin/lib/skillkits/agent_skillkit.py +155 -0
  154. dolphin/lib/skillkits/cognitive_skillkit.py +82 -0
  155. dolphin/lib/skillkits/env_skillkit.py +250 -0
  156. dolphin/lib/skillkits/mcp_adapter.py +616 -0
  157. dolphin/lib/skillkits/mcp_skillkit.py +771 -0
  158. dolphin/lib/skillkits/memory_skillkit.py +650 -0
  159. dolphin/lib/skillkits/noop_skillkit.py +31 -0
  160. dolphin/lib/skillkits/ontology_skillkit.py +89 -0
  161. dolphin/lib/skillkits/plan_act_skillkit.py +452 -0
  162. dolphin/lib/skillkits/resource/__init__.py +52 -0
  163. dolphin/lib/skillkits/resource/models/__init__.py +6 -0
  164. dolphin/lib/skillkits/resource/models/skill_config.py +109 -0
  165. dolphin/lib/skillkits/resource/models/skill_meta.py +127 -0
  166. dolphin/lib/skillkits/resource/resource_skillkit.py +393 -0
  167. dolphin/lib/skillkits/resource/skill_cache.py +215 -0
  168. dolphin/lib/skillkits/resource/skill_loader.py +395 -0
  169. dolphin/lib/skillkits/resource/skill_validator.py +406 -0
  170. dolphin/lib/skillkits/resource_skillkit.py +11 -0
  171. dolphin/lib/skillkits/search_skillkit.py +163 -0
  172. dolphin/lib/skillkits/sql_skillkit.py +274 -0
  173. dolphin/lib/skillkits/system_skillkit.py +509 -0
  174. dolphin/lib/skillkits/vm_skillkit.py +65 -0
  175. dolphin/lib/utils/__init__.py +9 -0
  176. dolphin/lib/utils/data_process.py +207 -0
  177. dolphin/lib/utils/handle_progress.py +178 -0
  178. dolphin/lib/utils/security.py +139 -0
  179. dolphin/lib/utils/text_retrieval.py +462 -0
  180. dolphin/lib/vm/__init__.py +11 -0
  181. dolphin/lib/vm/env_executor.py +895 -0
  182. dolphin/lib/vm/python_session_manager.py +453 -0
  183. dolphin/lib/vm/vm.py +610 -0
  184. dolphin/sdk/__init__.py +60 -0
  185. dolphin/sdk/agent/__init__.py +12 -0
  186. dolphin/sdk/agent/agent_factory.py +236 -0
  187. dolphin/sdk/agent/dolphin_agent.py +1106 -0
  188. dolphin/sdk/api/__init__.py +4 -0
  189. dolphin/sdk/runtime/__init__.py +8 -0
  190. dolphin/sdk/runtime/env.py +363 -0
  191. dolphin/sdk/skill/__init__.py +10 -0
  192. dolphin/sdk/skill/global_skills.py +706 -0
  193. dolphin/sdk/skill/traditional_toolkit.py +260 -0
  194. kweaver_dolphin-0.1.0.dist-info/METADATA +521 -0
  195. kweaver_dolphin-0.1.0.dist-info/RECORD +199 -0
  196. kweaver_dolphin-0.1.0.dist-info/WHEEL +5 -0
  197. kweaver_dolphin-0.1.0.dist-info/entry_points.txt +27 -0
  198. kweaver_dolphin-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
  199. 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