sourcebot 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 (110) hide show
  1. sourcebot/__init__.py +9 -0
  2. sourcebot/__main__.py +17 -0
  3. sourcebot/bus/__init__.py +4 -0
  4. sourcebot/bus/channel_adapter.py +21 -0
  5. sourcebot/bus/event_bus.py +15 -0
  6. sourcebot/bus/message_models.py +33 -0
  7. sourcebot/bus/outbound_dispatcher.py +15 -0
  8. sourcebot/bus/session_manager.py +20 -0
  9. sourcebot/cli/commands/core/__init__.py +3 -0
  10. sourcebot/cli/commands/core/command_line.py +26 -0
  11. sourcebot/cli/commands/init_commands/__init__.py +3 -0
  12. sourcebot/cli/commands/init_commands/init_global_config.py +30 -0
  13. sourcebot/cli/commands/init_commands/init_workspace_config.py +18 -0
  14. sourcebot/cli/commands/run_commands/__init__.py +3 -0
  15. sourcebot/cli/commands/run_commands/command_line_tool.py +345 -0
  16. sourcebot/cli/commands/run_commands/safe_runner.py +47 -0
  17. sourcebot/cli/main.py +28 -0
  18. sourcebot/config/__init__.py +15 -0
  19. sourcebot/config/base.py +13 -0
  20. sourcebot/config/config_manager.py +367 -0
  21. sourcebot/config/exceptions.py +4 -0
  22. sourcebot/config/global_config.py +55 -0
  23. sourcebot/config/provider_config.py +62 -0
  24. sourcebot/config/workspace_config.py +106 -0
  25. sourcebot/context/__init__.py +5 -0
  26. sourcebot/context/context_builder.py +78 -0
  27. sourcebot/context/identity.py +19 -0
  28. sourcebot/context/message_builder.py +154 -0
  29. sourcebot/context/skill/__init__.py +7 -0
  30. sourcebot/context/skill/skill.py +11 -0
  31. sourcebot/context/skill/skill_context.py +10 -0
  32. sourcebot/context/skill/skill_loader.py +57 -0
  33. sourcebot/context/skill/skill_metadata.py +27 -0
  34. sourcebot/context/skill/skill_requirements.py +25 -0
  35. sourcebot/context/skill/skill_summary.py +31 -0
  36. sourcebot/conversation/__init__.py +2 -0
  37. sourcebot/conversation/service.py +191 -0
  38. sourcebot/docker_sandbox/__init__.py +3 -0
  39. sourcebot/docker_sandbox/docker_sandbox.py +113 -0
  40. sourcebot/llm/__init__.py +3 -0
  41. sourcebot/llm/anthropic/__init__.py +2 -0
  42. sourcebot/llm/anthropic/adapter.py +30 -0
  43. sourcebot/llm/anthropic/anthropic_llm_client.py +38 -0
  44. sourcebot/llm/anthropic/converter.py +59 -0
  45. sourcebot/llm/core/adapter.py +16 -0
  46. sourcebot/llm/core/client.py +16 -0
  47. sourcebot/llm/core/delta.py +12 -0
  48. sourcebot/llm/core/message.py +53 -0
  49. sourcebot/llm/core/message_converter.py +33 -0
  50. sourcebot/llm/core/response.py +30 -0
  51. sourcebot/llm/core/tool.py +7 -0
  52. sourcebot/llm/core/tool_converter.py +30 -0
  53. sourcebot/llm/core/tool_delta_aggregator.py +38 -0
  54. sourcebot/llm/llm_client_factory.py +13 -0
  55. sourcebot/llm/openai/__init__.py +2 -0
  56. sourcebot/llm/openai/adapter.py +27 -0
  57. sourcebot/llm/openai/converter.py +53 -0
  58. sourcebot/llm/openai/openai_llm_client.py +47 -0
  59. sourcebot/logging/__init__.py +3 -0
  60. sourcebot/logging/setup.py +33 -0
  61. sourcebot/memory/__init__.py +5 -0
  62. sourcebot/memory/file_store.py +23 -0
  63. sourcebot/memory/llm_consolidator.py +79 -0
  64. sourcebot/memory/service.py +116 -0
  65. sourcebot/memory/window_policy.py +36 -0
  66. sourcebot/prompt/__init__.py +4 -0
  67. sourcebot/prompt/deeomposer_prompt.py +420 -0
  68. sourcebot/prompt/identity_prompt.py +98 -0
  69. sourcebot/prompt/subagent_prompt.py +25 -0
  70. sourcebot/runtime/__init__.py +3 -0
  71. sourcebot/runtime/agent/__init__.py +3 -0
  72. sourcebot/runtime/agent/agent.py +130 -0
  73. sourcebot/runtime/agent/agent_factory.py +83 -0
  74. sourcebot/runtime/dag/planner/__init__.py +3 -0
  75. sourcebot/runtime/dag/planner/dag_planner.py +26 -0
  76. sourcebot/runtime/dag/planner/execution_scheduler.py +35 -0
  77. sourcebot/runtime/dag/planner/parallelism_optimizer.py +44 -0
  78. sourcebot/runtime/dag/planner/task_decomposer.py +37 -0
  79. sourcebot/runtime/dag/scheduler/__init__.py +3 -0
  80. sourcebot/runtime/dag/scheduler/dag_scheduler.py +319 -0
  81. sourcebot/runtime/dag/scheduler/retry_policy.py +27 -0
  82. sourcebot/runtime/dag/scheduler/run_store.py +58 -0
  83. sourcebot/runtime/dag/scheduler/state_store.py +40 -0
  84. sourcebot/runtime/dag/scheduler/task_graph.py +29 -0
  85. sourcebot/runtime/init_system.py +182 -0
  86. sourcebot/runtime/tool_executor.py +30 -0
  87. sourcebot/security/policy.py +23 -0
  88. sourcebot/session/__init__.py +4 -0
  89. sourcebot/session/jsonl_repository.py +142 -0
  90. sourcebot/session/repository.py +19 -0
  91. sourcebot/session/service.py +44 -0
  92. sourcebot/session/session.py +53 -0
  93. sourcebot/storage/__init__.py +3 -0
  94. sourcebot/storage/rules_loader.py +72 -0
  95. sourcebot/storage/skill_storage.py +51 -0
  96. sourcebot/tools/__init__.py +7 -0
  97. sourcebot/tools/base.py +182 -0
  98. sourcebot/tools/registry.py +81 -0
  99. sourcebot/tools/rule_detail.py +70 -0
  100. sourcebot/tools/rule_list.py +57 -0
  101. sourcebot/tools/shell.py +93 -0
  102. sourcebot/tools/skill_detail.py +61 -0
  103. sourcebot/tools/skill_list.py +68 -0
  104. sourcebot/utils/__init__.py +2 -0
  105. sourcebot/utils/output.py +79 -0
  106. sourcebot-0.1.0.dist-info/METADATA +318 -0
  107. sourcebot-0.1.0.dist-info/RECORD +110 -0
  108. sourcebot-0.1.0.dist-info/WHEEL +5 -0
  109. sourcebot-0.1.0.dist-info/entry_points.txt +2 -0
  110. sourcebot-0.1.0.dist-info/top_level.txt +1 -0
sourcebot/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+ __name__ = """ _ _
3
+ | | | |
4
+ ___ ___ _ _ _ __ ___ ___ | |__ ___ | |_
5
+ / __|/ _ \| | | | '__/ __/ _ \ | '_ \ / _ \| __|
6
+ \__ \ (_) | |_| | | | (_| __/ | |_) | (_) | |_
7
+ |___/\___/ \__,_|_| \___\___| |_.__/ \___/ \__|
8
+
9
+ """
sourcebot/__main__.py ADDED
@@ -0,0 +1,17 @@
1
+ # src/sourcebot/__main__.py
2
+ import sys
3
+ from sourcebot.cli.main import app
4
+ from sourcebot.logging import setup_logging
5
+
6
+
7
+ def main():
8
+ """Entry point for console script"""
9
+ setup_logging()
10
+ try:
11
+ app()
12
+ except KeyboardInterrupt:
13
+ print("\n👋 Bye")
14
+ sys.exit(0)
15
+
16
+ if __name__ == "__main__":
17
+ main()
@@ -0,0 +1,4 @@
1
+ from sourcebot.bus.message_models import InboundMessage, OutboundMessage
2
+ from sourcebot.bus.event_bus import EventBus
3
+
4
+ __all__ = ["EventBus", "InboundMessage", "OutboundMessage"]
@@ -0,0 +1,21 @@
1
+ # sourcebot/bus/channel_adapter.py
2
+ from .message_models from OutboundMessage, InboundMessage
3
+ from .event_bus from EventBus
4
+ from typing import Any
5
+ from abc import ABC, abstractmethod
6
+
7
+ class ChannelAdapter(ABC):
8
+ def __init__(self, bus: EventBus):
9
+ self.bus = bus
10
+
11
+ @abstractmethod
12
+ async def start(self):
13
+ pass
14
+
15
+ @abstractmethod
16
+ async def send(self, msg: OutboundMessage):
17
+ pass
18
+
19
+ @abstractmethod
20
+ def normalize(self, raw_event: Any) -> InboundMessage:
21
+ pass
@@ -0,0 +1,15 @@
1
+ # sourcebot/bus/event_bus.py
2
+ import asyncio
3
+ from typing import Any, Callable, Dict, List, Type, Coroutine
4
+ from collections import defaultdict
5
+
6
+ class EventBus:
7
+ def __init__(self):
8
+ self.handlers: Dict[Type, List[Callable[[Any], Coroutine]]] = defaultdict(list)
9
+
10
+ def subscribe(self, event_type: Type, handler: Callable[[Any], Coroutine]):
11
+ self.handlers[event_type].append(handler)
12
+
13
+ async def publish(self, event: Any):
14
+ if type(event) in self.handlers:
15
+ await asyncio.gather(*(h(event) for h in self.handlers[type(event)]))
@@ -0,0 +1,33 @@
1
+ # sourcebot/bus/message_models.py
2
+ import uuid
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import Any, Dict, Optional
6
+
7
+ @dataclass
8
+ class InboundMessage:
9
+ channel: str
10
+ sender_id: str
11
+ conversation_id: str
12
+ content: str
13
+ message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
14
+ timestamp: datetime = field(default_factory=datetime.utcnow)
15
+ metadata: Dict[str, Any] = field(default_factory=dict)
16
+ correlation_id: Optional[str] = None
17
+ type: str = "text"
18
+
19
+ @property
20
+ def session_key(self) -> str:
21
+ return f"{self.channel}:{self.conversation_id}:{self.sender_id}"
22
+
23
+
24
+ @dataclass
25
+ class OutboundMessage:
26
+ channel: str
27
+ conversation_id: str
28
+ content: str
29
+ message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
30
+ reply_to: Optional[str] = None
31
+ metadata: Dict[str, Any] = field(default_factory=dict)
32
+ correlation_id: Optional[str] = None
33
+ type: str = "text"
@@ -0,0 +1,15 @@
1
+ # sourcebot/bus/outbound_dispatcher.py
2
+ from .message_models from OutboundMessage
3
+ from .channel_adapter from ChannelAdapter
4
+ from typing import Dict
5
+
6
+ class OutboundDispatcher:
7
+ def __init__(self, adapters: Dict[str, ChannelAdapter]):
8
+ self.adapters = adapters
9
+
10
+ async def dispatch(self, msg: OutboundMessage):
11
+ adapter = self.adapters.get(msg.channel)
12
+ if not adapter:
13
+ print(f"[WARN] No adapter for channel {msg.channel}")
14
+ return
15
+ await adapter.send(msg)
@@ -0,0 +1,20 @@
1
+ # sourcebot/bus/session_manager.py
2
+ from datetime import datetime
3
+ from typing import Any, Dict
4
+
5
+ class SessionManager:
6
+ def __init__(self):
7
+ self.sessions: Dict[str, Dict[str, Any]] = {}
8
+
9
+ def get(self, session_key: str) -> Dict[str, Any]:
10
+ if session_key not in self.sessions:
11
+ self.sessions[session_key] = {
12
+ "created_at": datetime.utcnow(),
13
+ "state": {},
14
+ "history": []
15
+ }
16
+ return self.sessions[session_key]
17
+
18
+ def append_history(self, session_key: str, message: InboundMessage):
19
+ session = self.get(session_key)
20
+ session["history"].append(message)
@@ -0,0 +1,3 @@
1
+ from sourcebot.cli.commands.core.command_line import command_line
2
+
3
+ __all__ = ["command_line"]
@@ -0,0 +1,26 @@
1
+ # sourcebot/cli/commands/core/command_line.py
2
+ from sourcebot.cli.commands.run_commands import CommandLineTool
3
+ from sourcebot.runtime import InitSystem
4
+ from datetime import datetime
5
+ import logging
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ async def command_line():
10
+
11
+ init_system = InitSystem()
12
+ console = init_system.console
13
+
14
+ command_line_tool = CommandLineTool(
15
+ console = console,
16
+ docker_sandbox = init_system.docker_sandbox,
17
+ dag_planner = init_system.dag_planner,
18
+ dag_scheduler = init_system.dag_scheduler,
19
+ conversation_service = init_system.conversation_service
20
+ )
21
+ try:
22
+ from sourcebot.cli.commands.run_commands import SafeRunner
23
+ runner = SafeRunner(command_line_tool)
24
+ await runner.run()
25
+ except EOFError:
26
+ console.print("\nGoodbye!")
@@ -0,0 +1,3 @@
1
+ from sourcebot.cli.commands.init_commands.init_global_config import init_global_config
2
+ from sourcebot.cli.commands.init_commands.init_workspace_config import init_workspace_config
3
+ __all__ = ["init_global_config", "init_workspace_config"]
@@ -0,0 +1,30 @@
1
+ # sourcebot/cli/commands/init_global_config.py
2
+ from sourcebot.config import ConfigManager
3
+ from rich.console import Console
4
+ from rich.prompt import Confirm
5
+ console = Console()
6
+
7
+ def init_global_config():
8
+
9
+
10
+ config_manager = ConfigManager()
11
+ global_config_path = config_manager.global_config_path
12
+
13
+
14
+ if global_config_path.exists():
15
+
16
+
17
+ console.print(f"[yellow]Config already exists at {global_config_path}[/yellow]")
18
+ console.print(" [bold]y[/bold] = overwrite with defaults (existing values will be lost)")
19
+ console.print(" [bold]N[/bold] = refresh config, keeping existing values and adding new fields")
20
+
21
+ confirm = Confirm.ask("[bold]Execute?[/bold]", default=True)
22
+
23
+ if confirm:
24
+ config_manager.init_global_config(True)
25
+ console.print(f"[green]✓[/green] Config reset to defaults at {global_config_path}")
26
+ else:
27
+ console.print(f"[green]✓[/green] Config refreshed at {global_config_path} (existing values preserved)")
28
+ else:
29
+ config_manager.init_global_config()
30
+ console.print(f"[green]✓[/green] Created config at {global_config_path}")
@@ -0,0 +1,18 @@
1
+ from sourcebot.config import ConfigManager
2
+ from rich.console import Console
3
+ from pathlib import Path
4
+
5
+ def init_workspace_config():
6
+ console = Console()
7
+ config_manager = ConfigManager()
8
+ project_path = Path.cwd()
9
+ project_name = project_path.name
10
+
11
+ if config_manager.load_workspace_config():
12
+ current_configuration = config_manager.find_workspace_config()
13
+ console.print(f"[yellow]Workspace config already exists at {current_configuration}[/yellow]")
14
+ else:
15
+ config_manager.init_workspace_config(
16
+ project_name = project_name,
17
+ project_path = project_path
18
+ )
@@ -0,0 +1,3 @@
1
+ from sourcebot.cli.commands.run_commands.command_line_tool import CommandLineTool
2
+ from sourcebot.cli.commands.run_commands.safe_runner import SafeRunner
3
+ __all__ = ["command_line", "CommandLineTool"]
@@ -0,0 +1,345 @@
1
+ # sourcebot/cli/commands/command_line_tool.py
2
+ import asyncio
3
+ import shlex
4
+ from typing import Dict, Any, Optional, List
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.panel import Panel
10
+ from rich.syntax import Syntax
11
+ from rich.prompt import Confirm
12
+ from rich import print as rprint
13
+ from rich.markdown import Markdown
14
+ import cmd2
15
+ from sourcebot.bus import InboundMessage
16
+ from sourcebot import __name__
17
+ class CommandType(Enum):
18
+ SHELL = "shell" # Shell commands
19
+ PYTHON = "python" # Python code execution
20
+ SYSTEM = "system" # System commands
21
+
22
+ @dataclass
23
+ class Command:
24
+ """Command data class"""
25
+ type: CommandType
26
+ name: str
27
+ content: str
28
+ args: Optional[Dict[str, Any]] = None
29
+
30
+ class CommandLineTool:
31
+ """Command line branching tool main class"""
32
+
33
+ def __init__(
34
+ self,
35
+ console,
36
+ docker_sandbox,
37
+ dag_scheduler,
38
+ dag_planner,
39
+ conversation_service
40
+ ):
41
+ self.console = console
42
+ self.docker_sandbox = docker_sandbox
43
+ self.dag_planner = dag_planner
44
+ self.dag_scheduler = dag_scheduler
45
+
46
+ self.command_history = []
47
+ self.last_plan_tasks = None # Store recently generated plans
48
+ self.conversation_service = conversation_service
49
+ self._running = True
50
+
51
+ async def run(self):
52
+ try:
53
+ await self._startup()
54
+ await self._loop()
55
+ except Exception as e:
56
+ self.console.print(f"[red]Fatal: {e}[/red]")
57
+ finally:
58
+ await self._shutdown()
59
+
60
+ async def _startup(self):
61
+ await self.docker_sandbox.start()
62
+ self.console.print(Panel.fit(
63
+ f"{__name__}\n\n"
64
+ "[bold cyan]AI Assistant Command Line Interface[/bold cyan]\n"
65
+ "Type [bold]help[/bold] for available commands\n"
66
+ "Type [bold]exit[/bold] or [bold]quit[/bold] to exit\n"
67
+ "\n"
68
+ "[dim]• Chat mode: Just type anything[/dim]\n"
69
+ "[dim]• Tasks: [bold]task <description>[/bold][/dim]\n"
70
+ "[dim]• Commands: [bold]shell|python|system <command>[/bold][/dim]",
71
+ title="Welcome",
72
+ border_style="blue"
73
+ ))
74
+
75
+ async def _shutdown(self):
76
+ await self._cleanup()
77
+
78
+ async def _loop(self):
79
+ while self._running:
80
+ try:
81
+ user_input = await self._read_input()
82
+ if not user_input:
83
+ continue
84
+ result = await self._process_command(user_input)
85
+ if result == "exit":
86
+ self._running = False
87
+ except KeyboardInterrupt:
88
+ self.console.print("\n[yellow]Interrupted[/yellow]")
89
+ break
90
+ except Exception as e:
91
+ self.console.print(f"[red]Error: {e}[/red]")
92
+
93
+ async def _read_input(self):
94
+ try:
95
+ text = self.console.input("[bold blue]>>> [/bold blue]")
96
+ except (EOFError, KeyboardInterrupt):
97
+ return "exit"
98
+ return text.strip()
99
+
100
+ async def _process_command(self, user_input: str):
101
+ """Processing user input commands"""
102
+
103
+ parts = shlex.split(user_input)
104
+ cmd = parts[0].lower()
105
+ args = parts[1:] if len(parts) > 1 else []
106
+ # Command distribution
107
+ if cmd == "help":
108
+ self._show_help()
109
+ elif cmd in {"exit", "quit"}:
110
+ self.console.print("[yellow]Goodbye![/yellow]")
111
+ return "exit"
112
+ elif cmd == "shell":
113
+ await self._handle_shell_command(args)
114
+ elif cmd == "python":
115
+ await self._handle_python_command(args)
116
+ elif cmd == "system":
117
+ await self._handle_system_command(args)
118
+ elif cmd == "task":
119
+ await self._plan_and_confirm(' '.join(args))
120
+ elif cmd == "resume_run":
121
+ await self._handle_resume_run(run_id = args[0])
122
+ elif cmd == "replay_failed":
123
+ await self._handle_replay_failed(run_id = args[0])
124
+ elif cmd == "clear":
125
+ self._handle_clear_command()
126
+ elif cmd == "history":
127
+ self._show_history()
128
+ else:
129
+ await self._chat(user_input)
130
+ async def _chat(self, user_input: str):
131
+ msg = InboundMessage(
132
+ channel = "cli",
133
+ sender_id = "user",
134
+ conversation_id = "main",
135
+ content = user_input
136
+ )
137
+ response = await self.conversation_service.handle_message(msg)
138
+ content = Markdown(response.content)
139
+ self.console.print()
140
+ self.console.print(content)
141
+ self.console.print()
142
+
143
+
144
+ async def _plan_and_confirm(self, user_input: str):
145
+ """
146
+ Planning-Confirmation-Execution Process
147
+ This is the core function of a planned task.
148
+ """
149
+ # 1. Generate plan
150
+ self.console.print("[bold cyan]📝 Generating execution plan...[/bold cyan]")
151
+ plan = await self.dag_planner.plan(user_input)
152
+ plan_tasks = plan["tasks"]
153
+
154
+ if not plan_tasks:
155
+ self.console.print("[red]❌ Could not generate a plan for your input[/red]")
156
+ return
157
+
158
+ self.last_plan_tasks = plan_tasks
159
+
160
+ # 2. Display plan
161
+ self.console.print("\n[bold]Generated Plan:[/bold]")
162
+ self.console.print(self.last_plan_tasks)
163
+ # 3. Request user confirmation
164
+ self.console.print("\n[yellow]Do you want to execute this plan?[/yellow]")
165
+ confirm = Confirm.ask("[bold]Execute?[/bold]", default=True)
166
+
167
+ if confirm:
168
+ # 4. Execute after user confirmation
169
+ self.console.print("[green]✓ Plan confirmed, starting execution...[/green]")
170
+ await self._execute_plan(plan_tasks)
171
+ else:
172
+ # 5. The user has not confirmed; modifications can be made before execution.
173
+ self.console.print("[yellow]⏸️ Plan execution cancelled[/yellow]")
174
+ run_id, run_dir = self.dag_scheduler.save_dag(plan_tasks)
175
+ self.console.print("[cyan]run_id[/cyan]:", run_id)
176
+ self.console.print("[cyan]run_dir[cyan]:", run_dir)
177
+ self.console.print("You can:")
178
+ self.console.print("Modify the task plan in [cyan]dag.json[/cyan]")
179
+ self.console.print(f"Then restart the task with [cyan]resume_run {run_id}[/cyan]")
180
+
181
+
182
+ async def _execute_plan(self, plan_tasks):
183
+ """Execute planned tasks"""
184
+ result = await self.dag_scheduler.run(plan_tasks)
185
+ self.console.print("[bold green]✅ Plan execution completed![/bold green]")
186
+
187
+
188
+
189
+ async def _handle_resume_run(self, run_id):
190
+ self.console.print("[bold green]Plan resume run start![/bold green]")
191
+ await self.dag_scheduler.resume(run_id = run_id)
192
+ self.console.print("[bold green]✅ Plan resume run completed![/bold green]")
193
+
194
+ async def _handle_replay_failed(self, run_id):
195
+ self.console.print("[bold green]✅ Plan replay failed start![/bold green]")
196
+ await self.dag_scheduler.replay_failed(run_id = run_id)
197
+ self.console.print("[bold green]✅ Plan replay failed completed![/bold green]")
198
+
199
+ async def _handle_shell_command(self, args: List[str]):
200
+ """Processing Shell commands"""
201
+ if not args:
202
+ self.console.print("[red]Usage: shell <command>[/red]")
203
+ return
204
+
205
+ command = " ".join(args)
206
+ cmd = Command(
207
+ type=CommandType.SHELL,
208
+ name=f"shell_{len(self.command_history)}",
209
+ content=command
210
+ )
211
+ await self._execute_command(cmd)
212
+
213
+ async def _handle_python_command(self, args: List[str]):
214
+ """Processing Python commands"""
215
+ if not args:
216
+ self.console.print("[red]Usage: python <code>[/red]")
217
+ self.console.print(" python -m <module>")
218
+ self.console.print(" python -f <file>")
219
+ return
220
+
221
+ if args[0] == "-m":
222
+ if len(args) < 2:
223
+ self.console.print("[red]Usage: python -m <module>[/red]")
224
+ return
225
+ content = f"import {args[1]}"
226
+ elif args[0] == "-f":
227
+ if len(args) < 2:
228
+ self.console.print("[red]Usage: python -f <file>[/red]")
229
+ return
230
+ try:
231
+ with open(args[1], 'r') as f:
232
+ content = f.read()
233
+ except FileNotFoundError:
234
+ self.console.print(f"[red]File not found: {args[1]}[/red]")
235
+ return
236
+ else:
237
+
238
+ content = " ".join(args)
239
+
240
+ cmd = Command(
241
+ type=CommandType.PYTHON,
242
+ name=f"python_{len(self.command_history)}",
243
+ content=content
244
+ )
245
+ await self._execute_command(cmd)
246
+
247
+ async def _handle_system_command(self, args: List[str]):
248
+ """Processing system commands"""
249
+ if not args:
250
+ self.console.print("[red]Usage: system <command>[/red]")
251
+ return
252
+
253
+ command = " ".join(args)
254
+ cmd = Command(
255
+ type=CommandType.SYSTEM,
256
+ name=f"system_{len(self.command_history)}",
257
+ content=command
258
+ )
259
+ await self._execute_command(cmd)
260
+
261
+
262
+ def _handle_clear_command(self):
263
+ """clear screen"""
264
+ self.console.clear()
265
+
266
+ def _show_history(self):
267
+ """Show command history"""
268
+ if not self.command_history:
269
+ self.console.print("[yellow]No command history[/yellow]")
270
+ return
271
+
272
+ table = Table(title="Command History")
273
+ table.add_column("#", style="cyan")
274
+ table.add_column("Command", style="green")
275
+
276
+ start = max(0, len(self.command_history) - 10)
277
+ for i in range(start, len(self.command_history)):
278
+ table.add_row(str(i + 1), self.command_history[i])
279
+
280
+ self.console.print(table)
281
+
282
+
283
+ def _show_help(self):
284
+ """Display help information"""
285
+ help_text = """
286
+ [bold cyan]Available Commands:[/bold cyan]
287
+
288
+ [bold]Basic Commands:[/bold]
289
+ [green]help[/green] - Show this help message
290
+ [green]exit[/green] / [green]quit[/green] - Exit the tool
291
+ [green]clear[/green] - Clear screen
292
+ [green]history[/green] - Show command history
293
+
294
+ [bold]Execution Commands:[/bold]
295
+ [green]shell <command>[/green] - Execute shell command (non-blocking)
296
+ [green]python <code>[/green] - Execute Python code
297
+ [green]system <command>[/green] - Execute system command (blocking)
298
+ [green]<any text>[/green] - Chat mode (default behavior)
299
+
300
+ [bold]Task Planning:[/bold]
301
+ [green]task <description>[/green] - Generate and execute a plan for the task
302
+ [green]resume_run <run_id>[/green] - Resume execution of a specific task run
303
+ [green]replay_failed <run_id>[/green] - Replay failed tasks from a specific run
304
+
305
+ [bold]Workflow Example:[/bold]
306
+ [cyan]>>> task create a backup of all .txt files[/cyan]
307
+ [dim]# System generates plan and executes it automatically[/dim]
308
+ [cyan]>>> replay_failed run_123[/cyan]
309
+ [dim]# Replays only the failed tasks from run_123[/dim]
310
+ """
311
+ self.console.print(Panel(help_text, title="Help", border_style="blue"))
312
+
313
+ async def _execute_command(self, command: Command):
314
+ """Execute commands in the sandbox"""
315
+ self.command_history.append(command.content)
316
+
317
+ self.console.print(f"[cyan]Executing: {command.name}[/cyan]")
318
+ self.console.print(f"[dim]Command: {command.content}[/dim]")
319
+
320
+ try:
321
+ result = await self.docker_sandbox.execute(command.content)
322
+
323
+
324
+ if result:
325
+ self.console.print("[bold green]Output:[/bold green]")
326
+
327
+ lines = result.split('\n')
328
+ for line in lines:
329
+ if line.strip():
330
+ self.console.print(f" [white]{line}[/white]")
331
+ else:
332
+ self.console.print("")
333
+ else:
334
+ self.console.print("[yellow]Command executed successfully (no output)[/yellow]")
335
+
336
+ except Exception as e:
337
+ self.console.print(f"[red]Error executing command: {str(e)}[/red]")
338
+ async def _cleanup(self):
339
+ """Clean up resources"""
340
+ try:
341
+ await self.docker_sandbox.stop()
342
+ except:
343
+ pass
344
+
345
+
@@ -0,0 +1,47 @@
1
+ # sourcebot/cli/commands/safe_runner.py
2
+ import inspect
3
+ import asyncio
4
+
5
+ class SafeRunner:
6
+ def __init__(self, obj):
7
+ self.obj = obj
8
+
9
+ # case 1: async function
10
+ if inspect.iscoroutinefunction(obj):
11
+ self.mode = "async_function"
12
+ return
13
+
14
+ # case 2: coroutine
15
+ if inspect.iscoroutine(obj):
16
+ self.mode = "coroutine"
17
+ return
18
+
19
+ # case 3: factory function
20
+ if callable(obj) and not inspect.isclass(obj):
21
+ obj = obj()
22
+
23
+ self.obj = obj
24
+
25
+ # case 4: class instance with run()
26
+ if hasattr(self.obj, "run") and callable(self.obj.run):
27
+ self.mode = "runner"
28
+ self.is_async = inspect.iscoroutinefunction(self.obj.run)
29
+ return
30
+
31
+ raise TypeError(f"{type(obj)} is not runnable")
32
+
33
+ async def run(self, *args, **kwargs):
34
+ # async function
35
+ if self.mode == "async_function":
36
+ return await self.obj(*args, **kwargs)
37
+
38
+ # coroutine
39
+ if self.mode == "coroutine":
40
+ return await self.obj
41
+
42
+ # class with run()
43
+ if self.mode == "runner":
44
+ if self.is_async:
45
+ return await self.obj.run(*args, **kwargs)
46
+ loop = asyncio.get_event_loop()
47
+ return await loop.run_in_executor(None, lambda: self.obj.run(*args, **kwargs))
sourcebot/cli/main.py ADDED
@@ -0,0 +1,28 @@
1
+ # src/sourcebot/cli/main.py
2
+ import typer
3
+ import sys
4
+
5
+ app = typer.Typer(
6
+ name = "sourcebot",
7
+ no_args_is_help = True,
8
+ add_completion = False
9
+ )
10
+ @app.command("init")
11
+ def init():
12
+ """Init global config"""
13
+ from sourcebot.cli.commands.init_commands import init_global_config
14
+ init_global_config()
15
+ @app.command("init_workspace")
16
+ def agent():
17
+ """Init workspace config"""
18
+ from sourcebot.cli.commands.init_commands import init_workspace_config
19
+ init_workspace_config()
20
+ @app.command("cli")
21
+ def test_init_agent_system():
22
+ """Start SourceBot AI Assistant CLI"""
23
+ from sourcebot.cli.commands.run_commands import SafeRunner
24
+ from sourcebot.cli.commands.core import command_line
25
+ import asyncio
26
+ runner = SafeRunner(command_line)
27
+ asyncio.run(runner.run())
28
+
@@ -0,0 +1,15 @@
1
+ from .config_manager import ConfigManager
2
+ from .global_config import GlobalConfig
3
+ from .workspace_config import WorkspaceConfig, ModelConfig
4
+ from .provider_config import ProviderConfig, ProvidersConfig
5
+ from .exceptions import ConfigError
6
+
7
+ __all__ = [
8
+ 'ConfigManager',
9
+ 'GlobalConfig',
10
+ 'WorkspaceConfig',
11
+ 'ModelConfig',
12
+ 'ProviderConfig',
13
+ 'ProvidersConfig',
14
+ 'ConfigError',
15
+ ]