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.
- sourcebot/__init__.py +9 -0
- sourcebot/__main__.py +17 -0
- sourcebot/bus/__init__.py +4 -0
- sourcebot/bus/channel_adapter.py +21 -0
- sourcebot/bus/event_bus.py +15 -0
- sourcebot/bus/message_models.py +33 -0
- sourcebot/bus/outbound_dispatcher.py +15 -0
- sourcebot/bus/session_manager.py +20 -0
- sourcebot/cli/commands/core/__init__.py +3 -0
- sourcebot/cli/commands/core/command_line.py +26 -0
- sourcebot/cli/commands/init_commands/__init__.py +3 -0
- sourcebot/cli/commands/init_commands/init_global_config.py +30 -0
- sourcebot/cli/commands/init_commands/init_workspace_config.py +18 -0
- sourcebot/cli/commands/run_commands/__init__.py +3 -0
- sourcebot/cli/commands/run_commands/command_line_tool.py +345 -0
- sourcebot/cli/commands/run_commands/safe_runner.py +47 -0
- sourcebot/cli/main.py +28 -0
- sourcebot/config/__init__.py +15 -0
- sourcebot/config/base.py +13 -0
- sourcebot/config/config_manager.py +367 -0
- sourcebot/config/exceptions.py +4 -0
- sourcebot/config/global_config.py +55 -0
- sourcebot/config/provider_config.py +62 -0
- sourcebot/config/workspace_config.py +106 -0
- sourcebot/context/__init__.py +5 -0
- sourcebot/context/context_builder.py +78 -0
- sourcebot/context/identity.py +19 -0
- sourcebot/context/message_builder.py +154 -0
- sourcebot/context/skill/__init__.py +7 -0
- sourcebot/context/skill/skill.py +11 -0
- sourcebot/context/skill/skill_context.py +10 -0
- sourcebot/context/skill/skill_loader.py +57 -0
- sourcebot/context/skill/skill_metadata.py +27 -0
- sourcebot/context/skill/skill_requirements.py +25 -0
- sourcebot/context/skill/skill_summary.py +31 -0
- sourcebot/conversation/__init__.py +2 -0
- sourcebot/conversation/service.py +191 -0
- sourcebot/docker_sandbox/__init__.py +3 -0
- sourcebot/docker_sandbox/docker_sandbox.py +113 -0
- sourcebot/llm/__init__.py +3 -0
- sourcebot/llm/anthropic/__init__.py +2 -0
- sourcebot/llm/anthropic/adapter.py +30 -0
- sourcebot/llm/anthropic/anthropic_llm_client.py +38 -0
- sourcebot/llm/anthropic/converter.py +59 -0
- sourcebot/llm/core/adapter.py +16 -0
- sourcebot/llm/core/client.py +16 -0
- sourcebot/llm/core/delta.py +12 -0
- sourcebot/llm/core/message.py +53 -0
- sourcebot/llm/core/message_converter.py +33 -0
- sourcebot/llm/core/response.py +30 -0
- sourcebot/llm/core/tool.py +7 -0
- sourcebot/llm/core/tool_converter.py +30 -0
- sourcebot/llm/core/tool_delta_aggregator.py +38 -0
- sourcebot/llm/llm_client_factory.py +13 -0
- sourcebot/llm/openai/__init__.py +2 -0
- sourcebot/llm/openai/adapter.py +27 -0
- sourcebot/llm/openai/converter.py +53 -0
- sourcebot/llm/openai/openai_llm_client.py +47 -0
- sourcebot/logging/__init__.py +3 -0
- sourcebot/logging/setup.py +33 -0
- sourcebot/memory/__init__.py +5 -0
- sourcebot/memory/file_store.py +23 -0
- sourcebot/memory/llm_consolidator.py +79 -0
- sourcebot/memory/service.py +116 -0
- sourcebot/memory/window_policy.py +36 -0
- sourcebot/prompt/__init__.py +4 -0
- sourcebot/prompt/deeomposer_prompt.py +420 -0
- sourcebot/prompt/identity_prompt.py +98 -0
- sourcebot/prompt/subagent_prompt.py +25 -0
- sourcebot/runtime/__init__.py +3 -0
- sourcebot/runtime/agent/__init__.py +3 -0
- sourcebot/runtime/agent/agent.py +130 -0
- sourcebot/runtime/agent/agent_factory.py +83 -0
- sourcebot/runtime/dag/planner/__init__.py +3 -0
- sourcebot/runtime/dag/planner/dag_planner.py +26 -0
- sourcebot/runtime/dag/planner/execution_scheduler.py +35 -0
- sourcebot/runtime/dag/planner/parallelism_optimizer.py +44 -0
- sourcebot/runtime/dag/planner/task_decomposer.py +37 -0
- sourcebot/runtime/dag/scheduler/__init__.py +3 -0
- sourcebot/runtime/dag/scheduler/dag_scheduler.py +319 -0
- sourcebot/runtime/dag/scheduler/retry_policy.py +27 -0
- sourcebot/runtime/dag/scheduler/run_store.py +58 -0
- sourcebot/runtime/dag/scheduler/state_store.py +40 -0
- sourcebot/runtime/dag/scheduler/task_graph.py +29 -0
- sourcebot/runtime/init_system.py +182 -0
- sourcebot/runtime/tool_executor.py +30 -0
- sourcebot/security/policy.py +23 -0
- sourcebot/session/__init__.py +4 -0
- sourcebot/session/jsonl_repository.py +142 -0
- sourcebot/session/repository.py +19 -0
- sourcebot/session/service.py +44 -0
- sourcebot/session/session.py +53 -0
- sourcebot/storage/__init__.py +3 -0
- sourcebot/storage/rules_loader.py +72 -0
- sourcebot/storage/skill_storage.py +51 -0
- sourcebot/tools/__init__.py +7 -0
- sourcebot/tools/base.py +182 -0
- sourcebot/tools/registry.py +81 -0
- sourcebot/tools/rule_detail.py +70 -0
- sourcebot/tools/rule_list.py +57 -0
- sourcebot/tools/shell.py +93 -0
- sourcebot/tools/skill_detail.py +61 -0
- sourcebot/tools/skill_list.py +68 -0
- sourcebot/utils/__init__.py +2 -0
- sourcebot/utils/output.py +79 -0
- sourcebot-0.1.0.dist-info/METADATA +318 -0
- sourcebot-0.1.0.dist-info/RECORD +110 -0
- sourcebot-0.1.0.dist-info/WHEEL +5 -0
- sourcebot-0.1.0.dist-info/entry_points.txt +2 -0
- sourcebot-0.1.0.dist-info/top_level.txt +1 -0
sourcebot/__init__.py
ADDED
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,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,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,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,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
|
+
]
|