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/config/base.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# src/sourcebot/config/base.py
|
|
2
|
+
from pydantic import BaseModel, ConfigDict
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Base(BaseModel):
|
|
7
|
+
"""Basic configuration class"""
|
|
8
|
+
model_config = ConfigDict(
|
|
9
|
+
extra = "forbid",
|
|
10
|
+
validate_assignment = True,
|
|
11
|
+
arbitrary_types_allowed = False,
|
|
12
|
+
populate_by_name = True,
|
|
13
|
+
)
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# sourcebot/config/config_manager.py
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any, Union
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from .exceptions import ConfigError
|
|
9
|
+
from .global_config import GlobalConfig
|
|
10
|
+
from .workspace_config import WorkspaceConfig, ModelConfig
|
|
11
|
+
from .provider_config import ProvidersConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ConfigManager:
|
|
15
|
+
"""
|
|
16
|
+
Configuration Manager - Separate global configuration and workspace configuration
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, app_name: str = "sourcebot"):
|
|
20
|
+
"""
|
|
21
|
+
Initialize Configuration Manager
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
app_name: Application Name
|
|
25
|
+
"""
|
|
26
|
+
self.app_name = app_name
|
|
27
|
+
self.app_root_path = Path.home() / f".{app_name}"
|
|
28
|
+
self.global_config_path = self.app_root_path / "config.json"
|
|
29
|
+
self.workspace_config_name = f"{app_name}.workspace.json"
|
|
30
|
+
|
|
31
|
+
self.app_root_path.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
self._global_config: Optional[GlobalConfig] = None
|
|
34
|
+
self._workspace_config: Optional[WorkspaceConfig] = None
|
|
35
|
+
self._current_workspace_path: Optional[Path] = None
|
|
36
|
+
|
|
37
|
+
# Global Configuration Management
|
|
38
|
+
|
|
39
|
+
def init_global_config(self, force: bool = False) -> GlobalConfig:
|
|
40
|
+
"""
|
|
41
|
+
Initialize global configuration (only once)
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
force: Whether to force re-initialization
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Global configuration object
|
|
48
|
+
"""
|
|
49
|
+
if not force and self.global_config_path.exists():
|
|
50
|
+
return self.load_global_config()
|
|
51
|
+
|
|
52
|
+
config = GlobalConfig()
|
|
53
|
+
|
|
54
|
+
self.save_global_config(config)
|
|
55
|
+
print(f"✅ Global configuration has been initialized: {self.global_config_path}")
|
|
56
|
+
|
|
57
|
+
self._global_config = config
|
|
58
|
+
return config
|
|
59
|
+
|
|
60
|
+
def load_global_config(self) -> GlobalConfig:
|
|
61
|
+
"""
|
|
62
|
+
Load global configuration
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Global configuration object
|
|
66
|
+
"""
|
|
67
|
+
if self._global_config is not None:
|
|
68
|
+
return self._global_config
|
|
69
|
+
|
|
70
|
+
if not self.global_config_path.exists():
|
|
71
|
+
return self.init_global_config()
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
with open(self.global_config_path, 'r', encoding='utf-8') as f:
|
|
75
|
+
data = json.load(f)
|
|
76
|
+
|
|
77
|
+
config = GlobalConfig.model_validate(data)
|
|
78
|
+
self._global_config = config
|
|
79
|
+
return config
|
|
80
|
+
|
|
81
|
+
except Exception as e:
|
|
82
|
+
raise ConfigError(f"Failed to load global configuration: {e}")
|
|
83
|
+
|
|
84
|
+
def save_global_config(self, config: Optional[GlobalConfig] = None):
|
|
85
|
+
"""
|
|
86
|
+
Save global configuration
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
config: Configuration object; saves cached configuration when None is selected.
|
|
90
|
+
"""
|
|
91
|
+
config = config or self._global_config
|
|
92
|
+
if config is None:
|
|
93
|
+
raise ConfigError("No global configuration can be saved")
|
|
94
|
+
|
|
95
|
+
config.update_timestamp()
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
self.global_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
|
|
100
|
+
with open(self.global_config_path, 'w', encoding='utf-8') as f:
|
|
101
|
+
json.dump(config.model_dump(exclude_none=True), f, indent=2, ensure_ascii=False)
|
|
102
|
+
|
|
103
|
+
self._global_config = config
|
|
104
|
+
print(f"✅ Global configuration has been saved")
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise ConfigError(f"Failed to save global configuration: {e}")
|
|
108
|
+
|
|
109
|
+
# Provider Configuration Management
|
|
110
|
+
|
|
111
|
+
def set_provider_config(self, provider: str, api_key: str, api_base: Optional[str] = None, **kwargs):
|
|
112
|
+
"""
|
|
113
|
+
Configure Provider
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
Provider: Provider name
|
|
117
|
+
API_key: API key
|
|
118
|
+
API_base: API base URL
|
|
119
|
+
**Kwargs: Other configurations
|
|
120
|
+
"""
|
|
121
|
+
global_config = self.load_global_config()
|
|
122
|
+
|
|
123
|
+
provider_config = getattr(global_config.providers, provider, None)
|
|
124
|
+
if provider_config is None:
|
|
125
|
+
raise ConfigError(f"Unsupported providers: {provider}")
|
|
126
|
+
|
|
127
|
+
provider_config.api_key = api_key
|
|
128
|
+
if api_base:
|
|
129
|
+
provider_config.api_base = api_base
|
|
130
|
+
for key, value in kwargs.items():
|
|
131
|
+
if hasattr(provider_config, key):
|
|
132
|
+
setattr(provider_config, key, value)
|
|
133
|
+
|
|
134
|
+
self.save_global_config(global_config)
|
|
135
|
+
print(f"✅ {provider} configuration has been updated")
|
|
136
|
+
|
|
137
|
+
def get_provider_config(self, provider: str):
|
|
138
|
+
"""Get provider configuration"""
|
|
139
|
+
global_config = self.load_global_config()
|
|
140
|
+
return getattr(global_config.providers, provider, None)
|
|
141
|
+
|
|
142
|
+
def list_providers(self, only_configured: bool = False) -> Dict[str, Any]:
|
|
143
|
+
"""List all providers"""
|
|
144
|
+
global_config = self.load_global_config()
|
|
145
|
+
|
|
146
|
+
if only_configured:
|
|
147
|
+
return {name: config for name, config in global_config.providers if config.is_configured()}
|
|
148
|
+
else:
|
|
149
|
+
return {name: config for name, config in global_config.providers}
|
|
150
|
+
|
|
151
|
+
# Workspace Configuration Management
|
|
152
|
+
|
|
153
|
+
def find_workspace_config(self, start_path: Optional[Path] = None) -> Optional[Path]:
|
|
154
|
+
"""
|
|
155
|
+
Locate workspace configuration file
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
start_path: Starting path
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Configuration file path
|
|
162
|
+
"""
|
|
163
|
+
if start_path is None:
|
|
164
|
+
start_path = Path.cwd()
|
|
165
|
+
|
|
166
|
+
start_path = start_path.absolute()
|
|
167
|
+
|
|
168
|
+
current = start_path
|
|
169
|
+
while current != current.parent:
|
|
170
|
+
config_file = current / self.workspace_config_name
|
|
171
|
+
if config_file.exists():
|
|
172
|
+
return config_file
|
|
173
|
+
current = current.parent
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def load_workspace_config(self, workspace_path: Optional[Path] = None) -> Optional[WorkspaceConfig]:
|
|
178
|
+
"""
|
|
179
|
+
Load workspace configuration
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
workspace_path: Workspace path
|
|
183
|
+
Returns:
|
|
184
|
+
Workspace configuration object; returns None if not found.
|
|
185
|
+
"""
|
|
186
|
+
config_path = self.find_workspace_config(workspace_path)
|
|
187
|
+
if not config_path:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
192
|
+
data = json.load(f)
|
|
193
|
+
|
|
194
|
+
config = WorkspaceConfig.model_validate(data)
|
|
195
|
+
self._workspace_config = config
|
|
196
|
+
self._current_workspace_path = config_path.parent
|
|
197
|
+
return config
|
|
198
|
+
|
|
199
|
+
except Exception as e:
|
|
200
|
+
raise ConfigError(f"Failed to load workspace configuration: {e}")
|
|
201
|
+
|
|
202
|
+
def init_workspace_config(self, project_name: str, project_path: Optional[Path] = None,
|
|
203
|
+
default_model: Optional[str] = None) -> WorkspaceConfig:
|
|
204
|
+
"""
|
|
205
|
+
Initialize workspace configuration
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
project_name: Project name
|
|
209
|
+
project_path: Project path
|
|
210
|
+
default_model: Default model name
|
|
211
|
+
Returns:
|
|
212
|
+
Workspace configuration object
|
|
213
|
+
"""
|
|
214
|
+
if project_path is None:
|
|
215
|
+
project_path = Path.cwd()
|
|
216
|
+
else:
|
|
217
|
+
project_path = Path(project_path).expanduser().resolve()
|
|
218
|
+
|
|
219
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
config_path = project_path / self.workspace_config_name
|
|
221
|
+
if config_path.exists():
|
|
222
|
+
response = input(f"The workspace configuration already exists: {config_path}\nShould it be overwritten? (y/N): ")
|
|
223
|
+
if response.lower() != 'y':
|
|
224
|
+
return self.load_workspace_config(project_path)
|
|
225
|
+
|
|
226
|
+
config = WorkspaceConfig(
|
|
227
|
+
project_name=project_name,
|
|
228
|
+
project_root=str(project_path)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if default_model:
|
|
232
|
+
global_config = self.load_global_config()
|
|
233
|
+
config.add_model(
|
|
234
|
+
name="default",
|
|
235
|
+
provider=global_config.default_provider,
|
|
236
|
+
model=default_model
|
|
237
|
+
)
|
|
238
|
+
config.default_model = "default"
|
|
239
|
+
|
|
240
|
+
self.save_workspace_config(config, project_path)
|
|
241
|
+
|
|
242
|
+
self._workspace_config = config
|
|
243
|
+
self._current_workspace_path = project_path
|
|
244
|
+
|
|
245
|
+
return config
|
|
246
|
+
|
|
247
|
+
def save_workspace_config(self, config: WorkspaceConfig, project_path: Optional[Path] = None):
|
|
248
|
+
"""
|
|
249
|
+
Save workspace configuration
|
|
250
|
+
Args:
|
|
251
|
+
config: Workspace configuration
|
|
252
|
+
project_path: Project path
|
|
253
|
+
"""
|
|
254
|
+
if project_path is None:
|
|
255
|
+
project_path = Path(config.project_root)
|
|
256
|
+
else:
|
|
257
|
+
project_path = Path(project_path).expanduser().resolve()
|
|
258
|
+
|
|
259
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
|
|
261
|
+
config_path = project_path / self.workspace_config_name
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
265
|
+
json.dump(config.model_dump(exclude_none=True), f, indent=2, ensure_ascii=False)
|
|
266
|
+
|
|
267
|
+
print(f"✅ Workspace configuration has been saved: {config_path}")
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
raise ConfigError(f"Failed to save workspace configuration: {e}")
|
|
271
|
+
|
|
272
|
+
# Model Configuration Management
|
|
273
|
+
|
|
274
|
+
def add_model_to_workspace(self, name: str, provider: str, model: str,
|
|
275
|
+
workspace_path: Optional[Path] = None, **kwargs):
|
|
276
|
+
"""
|
|
277
|
+
Add a model to the workspace configuration
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
name: Model name
|
|
281
|
+
provider: Provider name
|
|
282
|
+
model: Model name
|
|
283
|
+
workspace_path: Workspace path
|
|
284
|
+
**kwargs: Other model parameters
|
|
285
|
+
"""
|
|
286
|
+
global_config = self.load_global_config()
|
|
287
|
+
provider_config = getattr(global_config.providers, provider, None)
|
|
288
|
+
if not provider_config or not provider_config.is_configured():
|
|
289
|
+
print(f"Warning: Provider {provider} has not set the API key in the global configuration.")
|
|
290
|
+
|
|
291
|
+
workspace_config = self.load_workspace_config(workspace_path)
|
|
292
|
+
if not workspace_config:
|
|
293
|
+
if workspace_path is None:
|
|
294
|
+
workspace_path = Path.cwd()
|
|
295
|
+
workspace_config = self.init_workspace_config(
|
|
296
|
+
project_name=workspace_path.name,
|
|
297
|
+
project_path=workspace_path
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
workspace_config.add_model(name, provider, model, **kwargs)
|
|
301
|
+
|
|
302
|
+
if workspace_config.default_model == "default" and name != "default":
|
|
303
|
+
workspace_config.default_model = name
|
|
304
|
+
|
|
305
|
+
self.save_workspace_config(workspace_config)
|
|
306
|
+
print(f"✅ The model {name} ({provider}/{model}) has been added to the workspace.")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def get_model_for_workspace(self, model_name: Optional[str] = None,
|
|
310
|
+
workspace_path: Optional[Path] = None) -> Dict[str, Any]:
|
|
311
|
+
"""
|
|
312
|
+
Retrieves the workspace model configuration (including the global API key)
|
|
313
|
+
Args:
|
|
314
|
+
model_name: Model name; default model is used if None
|
|
315
|
+
workspace_path: Workspace path
|
|
316
|
+
Returns:
|
|
317
|
+
A dictionary containing the complete configuration (model parameters + API key, etc.)
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
workspace_config = self.load_workspace_config(workspace_path)
|
|
321
|
+
if not workspace_config:
|
|
322
|
+
raise ConfigError("Workspace configuration not found, please initialize first.")
|
|
323
|
+
|
|
324
|
+
model_config = workspace_config.get_model_config(model_name)
|
|
325
|
+
if not model_config:
|
|
326
|
+
raise ConfigError(f"The model does not exist: {model_name or workspace_config.default_model}")
|
|
327
|
+
|
|
328
|
+
global_config = self.load_global_config()
|
|
329
|
+
provider_config = global_config.get_provider_config(model_config.provider)
|
|
330
|
+
|
|
331
|
+
if not provider_config or not provider_config.is_configured():
|
|
332
|
+
raise ConfigError(f"The provider {model_config.provider} does not have an API key configured.")
|
|
333
|
+
|
|
334
|
+
result = {
|
|
335
|
+
"provider": model_config.provider,
|
|
336
|
+
"model": model_config.model,
|
|
337
|
+
"api_key": provider_config.api_key,
|
|
338
|
+
"api_base": provider_config.api_base,
|
|
339
|
+
"temperature": model_config.temperature,
|
|
340
|
+
"max_tokens": model_config.max_tokens,
|
|
341
|
+
"top_p": model_config.top_p,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if provider_config.extra_headers:
|
|
345
|
+
result["extra_headers"] = provider_config.extra_headers
|
|
346
|
+
|
|
347
|
+
return result
|
|
348
|
+
|
|
349
|
+
# Tools and Methods
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def current_workspace(self) -> Optional[WorkspaceConfig]:
|
|
353
|
+
"""Get current workspace configuration"""
|
|
354
|
+
if self._workspace_config is None:
|
|
355
|
+
self._workspace_config = self.load_workspace_config()
|
|
356
|
+
return self._workspace_config
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def in_workspace(self) -> bool:
|
|
360
|
+
return self.current_workspace is not None
|
|
361
|
+
|
|
362
|
+
def get_workspace_path(self) -> Optional[Path]:
|
|
363
|
+
"""Get the current workspace path"""
|
|
364
|
+
if self._current_workspace_path:
|
|
365
|
+
return self._current_workspace_path
|
|
366
|
+
config_path = self.find_workspace_config()
|
|
367
|
+
return config_path.parent if config_path else None
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# sourcebot/config/global_config.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .base import Base
|
|
8
|
+
from .provider_config import ProvidersConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalConfig(Base):
|
|
12
|
+
"""Global configuration - initialized only once, saved in ~/.sourcebot/config.json"""
|
|
13
|
+
|
|
14
|
+
version: str = Field(default="1.0.0", description="Configuration version")
|
|
15
|
+
created_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Creation time")
|
|
16
|
+
updated_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Update time")
|
|
17
|
+
|
|
18
|
+
providers: ProvidersConfig = Field(default_factory=ProvidersConfig, description="LLM provider configuration")
|
|
19
|
+
|
|
20
|
+
default_provider: str = Field(default="dashscope", description="Default provider")
|
|
21
|
+
default_model: str = Field(default="qwen3.5-plus", description="Default model")
|
|
22
|
+
|
|
23
|
+
cache_dir: str = Field(default=str(Path.home() / ".cache" / "sourcebot"), description="Cache directory")
|
|
24
|
+
log_level: str = Field(default="info", description="Log levels")
|
|
25
|
+
|
|
26
|
+
def update_timestamp(self):
|
|
27
|
+
"""Update timestamp"""
|
|
28
|
+
self.updated_at = datetime.now().isoformat()
|
|
29
|
+
|
|
30
|
+
def get_provider_config(self, provider_name: str) -> Optional[Any]:
|
|
31
|
+
"""Get the configuration of the specified provider"""
|
|
32
|
+
return getattr(self.providers, provider_name, None)
|
|
33
|
+
|
|
34
|
+
def is_provider_configured(self, provider_name: str) -> bool:
|
|
35
|
+
"""Check if the provider has been configured"""
|
|
36
|
+
provider = self.get_provider_config(provider_name)
|
|
37
|
+
return provider is not None and provider.is_configured()
|
|
38
|
+
|
|
39
|
+
class Config:
|
|
40
|
+
json_schema_extra = {
|
|
41
|
+
"example": {
|
|
42
|
+
"version": "1.0.0",
|
|
43
|
+
"default_provider": "openai",
|
|
44
|
+
"default_model": "gpt-4",
|
|
45
|
+
"providers": {
|
|
46
|
+
"openai": {
|
|
47
|
+
"api_key": "sk-...",
|
|
48
|
+
"api_base": "https://api.openai.com/v1"
|
|
49
|
+
},
|
|
50
|
+
"anthropic": {
|
|
51
|
+
"api_key": "sk-ant-..."
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# src/sourcebot/config/provider_config.py
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
from typing import Optional, Dict
|
|
4
|
+
from .base import Base
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ProviderConfig(Base):
|
|
8
|
+
"""LLM provider configuration."""
|
|
9
|
+
|
|
10
|
+
api_key: str = Field(default="", description="API key")
|
|
11
|
+
api_base: str = Field(default="", description="API base URL")
|
|
12
|
+
temperature: float = 0.7
|
|
13
|
+
extra_headers: Optional[Dict[str, str]] = Field(default=None, description="Custom request headers")
|
|
14
|
+
timeout: Optional[int] = Field(default=None, description="Timeout (seconds)")
|
|
15
|
+
max_retries: int = Field(default=3, description="Maximum number of retries")
|
|
16
|
+
|
|
17
|
+
def is_configured(self) -> bool:
|
|
18
|
+
"""Check if it is configured"""
|
|
19
|
+
return bool(self.api_key)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProvidersConfig(Base):
|
|
23
|
+
"""Configuration for LLM providers."""
|
|
24
|
+
|
|
25
|
+
custom: ProviderConfig = Field(default_factory=ProviderConfig, description="Custom OpenAI compatible endpoints")
|
|
26
|
+
anthropic: ProviderConfig = Field(default_factory=ProviderConfig, description="Anthropic Claude")
|
|
27
|
+
openai: ProviderConfig = Field(default_factory=ProviderConfig, description="OpenAI")
|
|
28
|
+
openrouter: ProviderConfig = Field(default_factory=ProviderConfig, description="OpenRouter")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
deepseek: ProviderConfig = Field(default_factory=ProviderConfig, description="DeepSeek")
|
|
32
|
+
zhipu: ProviderConfig = Field(default_factory=ProviderConfig, description="zhipu")
|
|
33
|
+
dashscope: ProviderConfig = Field(default_factory=ProviderConfig, description="dashscope")
|
|
34
|
+
moonshot: ProviderConfig = Field(default_factory=ProviderConfig, description="Moonshot AI")
|
|
35
|
+
minimax: ProviderConfig = Field(default_factory=ProviderConfig, description="MiniMax")
|
|
36
|
+
|
|
37
|
+
aihubmix: ProviderConfig = Field(default_factory=ProviderConfig, description="AiHubMix API gateway")
|
|
38
|
+
siliconflow: ProviderConfig = Field(default_factory=ProviderConfig, description="SiliconFlow")
|
|
39
|
+
volcengine: ProviderConfig = Field(default_factory=ProviderConfig, description="volcengine")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
groq: ProviderConfig = Field(default_factory=ProviderConfig, description="Groq")
|
|
43
|
+
vllm: ProviderConfig = Field(default_factory=ProviderConfig, description="vLLM")
|
|
44
|
+
gemini: ProviderConfig = Field(default_factory=ProviderConfig, description="Google Gemini")
|
|
45
|
+
|
|
46
|
+
# OAuth
|
|
47
|
+
openai_codex: ProviderConfig = Field(default_factory=ProviderConfig, description="OpenAI Codex (OAuth)")
|
|
48
|
+
github_copilot: ProviderConfig = Field(default_factory=ProviderConfig, description="Github Copilot (OAuth)")
|
|
49
|
+
|
|
50
|
+
def get_provider(self, name: str) -> ProviderConfig:
|
|
51
|
+
"""Retrieve provider configuration for a specified name"""
|
|
52
|
+
return getattr(self, name, None)
|
|
53
|
+
|
|
54
|
+
def list_configured(self) -> list[str]:
|
|
55
|
+
"""List all configured providers"""
|
|
56
|
+
return [name for name, provider in self if provider.is_configured()]
|
|
57
|
+
|
|
58
|
+
def __iter__(self):
|
|
59
|
+
"""Iterate through all providers"""
|
|
60
|
+
for field_name in self.model_fields:
|
|
61
|
+
if field_name not in ['model_config']:
|
|
62
|
+
yield field_name, getattr(self, field_name)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# sourcebot/config/workspace_config.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pydantic import Field, field_validator
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from .base import Base
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModelConfig(Base):
|
|
11
|
+
"""Model configuration"""
|
|
12
|
+
provider: str = Field(..., description="Provider name")
|
|
13
|
+
model: str = Field(..., description="Model name")
|
|
14
|
+
temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="Temperature")
|
|
15
|
+
max_tokens: int = Field(default=2000, gt=0, description="Maximum tokens")
|
|
16
|
+
top_p: float = Field(default=1.0, ge=0.0, le=1.0, description="Top-p")
|
|
17
|
+
|
|
18
|
+
@field_validator('provider')
|
|
19
|
+
def validate_provider(cls, v):
|
|
20
|
+
"""Validate provider name"""
|
|
21
|
+
# Only basic validation here, specific availability is determined by global configuration
|
|
22
|
+
if not v or not isinstance(v, str):
|
|
23
|
+
raise ValueError("Provider name must be a non-empty string")
|
|
24
|
+
return v.lower()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkspaceConfig(Base):
|
|
28
|
+
"""Workspace configuration - saved independently for each project"""
|
|
29
|
+
|
|
30
|
+
# Project information
|
|
31
|
+
project_name: str = Field(..., description="Project name")
|
|
32
|
+
project_root: str = Field(..., description="Project root directory")
|
|
33
|
+
project_rules: str = Field("", description="Project rules keywords")
|
|
34
|
+
project_skill: str = Field("", description="Project skill keywords")
|
|
35
|
+
docker_image: str = Field("python:3.11-slim", description="docker image")
|
|
36
|
+
created_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Creation time")
|
|
37
|
+
|
|
38
|
+
# Model configuration
|
|
39
|
+
models: Dict[str, ModelConfig] = Field(
|
|
40
|
+
default_factory=lambda: {
|
|
41
|
+
"main_agent": ModelConfig(
|
|
42
|
+
provider = "dashscope",
|
|
43
|
+
model = "qwen3.5-plus",
|
|
44
|
+
temperature = 0.7,
|
|
45
|
+
max_tokens = 2000,
|
|
46
|
+
top_p = 1.0
|
|
47
|
+
),
|
|
48
|
+
"sub_agent": ModelConfig(
|
|
49
|
+
provider = "dashscope",
|
|
50
|
+
model = "qwen3.5-plus",
|
|
51
|
+
temperature = 0.7,
|
|
52
|
+
max_tokens = 2000,
|
|
53
|
+
top_p = 1.0
|
|
54
|
+
)
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Workspace settings
|
|
60
|
+
context_files: List[str] = Field(default_factory=list, description="Context file patterns")
|
|
61
|
+
ignore_patterns: List[str] = Field(
|
|
62
|
+
default_factory=lambda: ["**/__pycache__", "**/.git", "**/.idea", "**/*.pyc"],
|
|
63
|
+
description="Ignore file patterns"
|
|
64
|
+
)
|
|
65
|
+
max_file_size: int = Field(default=10 * 1024 * 1024, description="Maximum file size (bytes)")
|
|
66
|
+
|
|
67
|
+
# Custom settings
|
|
68
|
+
settings: Dict[str, Any] = Field(default_factory=dict, description="Custom settings")
|
|
69
|
+
|
|
70
|
+
def get_model_config(self, model_name: Optional[str] = None) -> Optional[ModelConfig]:
|
|
71
|
+
"""Get model configuration"""
|
|
72
|
+
if model_name is None:
|
|
73
|
+
model_name = self.default_model
|
|
74
|
+
return self.models.get(model_name)
|
|
75
|
+
|
|
76
|
+
def add_model(self, name: str, provider: str, model: str, **kwargs):
|
|
77
|
+
"""Add model configuration"""
|
|
78
|
+
self.models[name] = ModelConfig(provider=provider, model=model, **kwargs)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def root_path(self) -> Path:
|
|
82
|
+
"""Get project root directory path"""
|
|
83
|
+
return Path(self.project_root).expanduser().resolve()
|
|
84
|
+
|
|
85
|
+
class Config:
|
|
86
|
+
json_schema_extra = {
|
|
87
|
+
"example": {
|
|
88
|
+
"project_name": "my-ai-project",
|
|
89
|
+
"project_root": "/home/user/projects/my-ai-project",
|
|
90
|
+
"project_rules": "python",
|
|
91
|
+
"project_skill": "python",
|
|
92
|
+
"docker_image": "python:3.11-slim",
|
|
93
|
+
"models": {
|
|
94
|
+
"gpt4": {
|
|
95
|
+
"provider": "openai",
|
|
96
|
+
"model": "gpt-4",
|
|
97
|
+
"temperature": 0.7
|
|
98
|
+
},
|
|
99
|
+
"claude": { # TODO: Anthropic compatible
|
|
100
|
+
"provider": "anthropic",
|
|
101
|
+
"model": "claude-3-opus-20240229",
|
|
102
|
+
"temperature": 0.5
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from sourcebot.context.context_builder import ContextBuilder
|
|
2
|
+
from sourcebot.context.identity import get_identity
|
|
3
|
+
from sourcebot.context.message_builder import MessageBuilder
|
|
4
|
+
# from sourcebot.context.rules_loader import RulesLoader
|
|
5
|
+
__all__ = ["ContextBuilder", "get_identity", "MessageBuilder"]
|