devorch 0.1.2__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.
- cli/commands/ask.py +0 -0
- cli/commands/edit.py +0 -0
- cli/commands/run.py +0 -0
- cli/main.py +1527 -0
- config/permissions.py +285 -0
- config/profiles.py +0 -0
- config/settings.py +185 -0
- core/agent.py +433 -0
- core/context.py +0 -0
- core/executor.py +213 -0
- core/modes.py +141 -0
- core/planner.py +16 -0
- core/sessions.py +271 -0
- core/tasks.py +142 -0
- devorch-0.1.2.dist-info/METADATA +528 -0
- devorch-0.1.2.dist-info/RECORD +50 -0
- devorch-0.1.2.dist-info/WHEEL +4 -0
- devorch-0.1.2.dist-info/entry_points.txt +2 -0
- providers/__init__.py +130 -0
- providers/anthropic.py +127 -0
- providers/base.py +51 -0
- providers/custom.py +186 -0
- providers/deepseek.py +170 -0
- providers/gemini.py +192 -0
- providers/github_copilot.py +196 -0
- providers/groq.py +158 -0
- providers/kimi.py +181 -0
- providers/lmstudio.py +147 -0
- providers/local.py +214 -0
- providers/mistral.py +161 -0
- providers/openai.py +114 -0
- providers/openrouter.py +195 -0
- providers/together.py +159 -0
- schemas/message.py +32 -0
- schemas/task.py +115 -0
- schemas/tool.py +0 -0
- tools/__init__.py +30 -0
- tools/base.py +40 -0
- tools/edit.py +236 -0
- tools/filesystem.py +224 -0
- tools/git.py +0 -0
- tools/grep.py +280 -0
- tools/search.py +150 -0
- tools/shell.py +55 -0
- tools/task.py +91 -0
- tools/terminal.py +123 -0
- tools/terminal_session.py +298 -0
- tools/tests.py +0 -0
- tools/websearch.py +166 -0
- utils/logger.py +52 -0
providers/openai.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from openai import OpenAI
|
|
4
|
+
|
|
5
|
+
from providers.base import LLMProvider, ModelInfo
|
|
6
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAIProvider(LLMProvider):
|
|
10
|
+
name = "openai"
|
|
11
|
+
|
|
12
|
+
DEFAULT_MODELS = [
|
|
13
|
+
"gpt-4o",
|
|
14
|
+
"gpt-4o-mini",
|
|
15
|
+
"gpt-4-turbo",
|
|
16
|
+
"gpt-4",
|
|
17
|
+
"gpt-3.5-turbo",
|
|
18
|
+
"o1-preview",
|
|
19
|
+
"o1-mini",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __init__(self, model: str = "gpt-4o", api_key: str | None = None):
|
|
23
|
+
self.client = OpenAI(api_key=api_key)
|
|
24
|
+
self.model = model
|
|
25
|
+
|
|
26
|
+
def list_models(self) -> list[ModelInfo]:
|
|
27
|
+
"""Fetch available models from OpenAI API."""
|
|
28
|
+
try:
|
|
29
|
+
response = self.client.models.list()
|
|
30
|
+
models = []
|
|
31
|
+
# Filter to chat models
|
|
32
|
+
chat_prefixes = ("gpt-4", "gpt-3.5", "o1")
|
|
33
|
+
for model in response.data:
|
|
34
|
+
if any(model.id.startswith(p) for p in chat_prefixes):
|
|
35
|
+
models.append(
|
|
36
|
+
ModelInfo(
|
|
37
|
+
id=model.id,
|
|
38
|
+
name=model.id,
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
# Sort by name
|
|
42
|
+
models.sort(key=lambda m: m.id)
|
|
43
|
+
return models if models else [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
44
|
+
except Exception:
|
|
45
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
46
|
+
|
|
47
|
+
def generate(
|
|
48
|
+
self,
|
|
49
|
+
messages: list[Message],
|
|
50
|
+
tools: list | None = None,
|
|
51
|
+
stream: bool = False,
|
|
52
|
+
) -> LLMResponse:
|
|
53
|
+
# Format messages for OpenAI
|
|
54
|
+
formatted_messages = []
|
|
55
|
+
for msg in messages:
|
|
56
|
+
if msg.role == "tool":
|
|
57
|
+
formatted_msg = {
|
|
58
|
+
"role": "tool",
|
|
59
|
+
"content": msg.content,
|
|
60
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
61
|
+
}
|
|
62
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
63
|
+
# Preserve tool_calls in assistant messages
|
|
64
|
+
formatted_msg = {
|
|
65
|
+
"role": "assistant",
|
|
66
|
+
"content": msg.content or "",
|
|
67
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
68
|
+
}
|
|
69
|
+
else:
|
|
70
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
71
|
+
formatted_messages.append(formatted_msg)
|
|
72
|
+
|
|
73
|
+
# Format tools for OpenAI
|
|
74
|
+
formatted_tools = None
|
|
75
|
+
if tools:
|
|
76
|
+
formatted_tools = []
|
|
77
|
+
for tool in tools:
|
|
78
|
+
formatted_tools.append(
|
|
79
|
+
{
|
|
80
|
+
"type": "function",
|
|
81
|
+
"function": {
|
|
82
|
+
"name": tool["name"],
|
|
83
|
+
"description": tool["description"],
|
|
84
|
+
"parameters": tool.get(
|
|
85
|
+
"parameters", {"type": "object", "properties": {}}
|
|
86
|
+
),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = self.client.chat.completions.create(
|
|
92
|
+
model=self.model, messages=formatted_messages, tools=formatted_tools, temperature=0.0
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
choice = response.choices[0]
|
|
96
|
+
message = choice.message
|
|
97
|
+
|
|
98
|
+
tool_calls = []
|
|
99
|
+
if message.tool_calls:
|
|
100
|
+
for tc in message.tool_calls:
|
|
101
|
+
# OpenAI returns arguments as a JSON string
|
|
102
|
+
args = json.loads(tc.function.arguments) if tc.function.arguments else {}
|
|
103
|
+
|
|
104
|
+
tool_call = ToolCall(name=tc.function.name, arguments=args, id=tc.id)
|
|
105
|
+
tool_calls.append(tool_call)
|
|
106
|
+
|
|
107
|
+
# Handle case where assistant message has no content but has tool calls
|
|
108
|
+
content = message.content if message.content else "Calling tool..."
|
|
109
|
+
|
|
110
|
+
return LLMResponse(
|
|
111
|
+
message=Message(role="assistant", content=content),
|
|
112
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
113
|
+
raw=response,
|
|
114
|
+
)
|
providers/openrouter.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenRouter Provider - Access multiple models through one API.
|
|
3
|
+
https://openrouter.ai/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from providers.base import LLMProvider, ModelInfo
|
|
11
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OpenRouterProvider(LLMProvider):
|
|
15
|
+
"""
|
|
16
|
+
OpenRouter provides access to many models through a single API.
|
|
17
|
+
Supports: Claude, GPT-4, Llama, Mistral, and many more.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "openrouter"
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODELS = [
|
|
23
|
+
"anthropic/claude-sonnet-4",
|
|
24
|
+
"anthropic/claude-3.5-sonnet",
|
|
25
|
+
"openai/gpt-4o",
|
|
26
|
+
"openai/gpt-4o-mini",
|
|
27
|
+
"meta-llama/llama-3.3-70b-instruct",
|
|
28
|
+
"google/gemini-2.0-flash-001",
|
|
29
|
+
"mistralai/mistral-large-2411",
|
|
30
|
+
"deepseek/deepseek-chat-v3-0324",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
BASE_URL = "https://openrouter.ai/api/v1"
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
model: str = "anthropic/claude-3.5-sonnet",
|
|
38
|
+
api_key: str | None = None,
|
|
39
|
+
site_url: str = "https://github.com/Amanbig/DevOrch",
|
|
40
|
+
site_name: str = "DevOrch",
|
|
41
|
+
):
|
|
42
|
+
self.model = model
|
|
43
|
+
self.api_key = api_key
|
|
44
|
+
self.site_url = site_url
|
|
45
|
+
self.site_name = site_name
|
|
46
|
+
self.client = httpx.Client(timeout=120.0)
|
|
47
|
+
|
|
48
|
+
def _get_headers(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
51
|
+
"HTTP-Referer": self.site_url,
|
|
52
|
+
"X-Title": self.site_name,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def list_models(self) -> list[ModelInfo]:
|
|
57
|
+
"""Fetch available models from OpenRouter API."""
|
|
58
|
+
try:
|
|
59
|
+
response = self.client.get(f"{self.BASE_URL}/models", headers=self._get_headers())
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
data = response.json()
|
|
62
|
+
|
|
63
|
+
models = []
|
|
64
|
+
for model in data.get("data", []):
|
|
65
|
+
model_id = model.get("id", "")
|
|
66
|
+
# Skip models that don't support chat or are deprecated
|
|
67
|
+
architecture = model.get("architecture", {})
|
|
68
|
+
if architecture.get("modality") == "text->image":
|
|
69
|
+
continue # Skip image generation models
|
|
70
|
+
|
|
71
|
+
models.append(
|
|
72
|
+
ModelInfo(
|
|
73
|
+
id=model_id,
|
|
74
|
+
name=model.get("name", model_id),
|
|
75
|
+
context_length=model.get("context_length"),
|
|
76
|
+
description=model.get("description"),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Sort by name, prioritizing well-known providers
|
|
81
|
+
def sort_key(m):
|
|
82
|
+
priority_prefixes = [
|
|
83
|
+
"openai/",
|
|
84
|
+
"anthropic/",
|
|
85
|
+
"google/",
|
|
86
|
+
"meta-llama/",
|
|
87
|
+
"mistralai/",
|
|
88
|
+
"deepseek/",
|
|
89
|
+
]
|
|
90
|
+
for i, prefix in enumerate(priority_prefixes):
|
|
91
|
+
if m.id.startswith(prefix):
|
|
92
|
+
return (i, m.name)
|
|
93
|
+
return (len(priority_prefixes), m.name)
|
|
94
|
+
|
|
95
|
+
models.sort(key=sort_key)
|
|
96
|
+
return models if models else [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
97
|
+
|
|
98
|
+
except Exception:
|
|
99
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
100
|
+
|
|
101
|
+
def generate(
|
|
102
|
+
self,
|
|
103
|
+
messages: list[Message],
|
|
104
|
+
tools: list | None = None,
|
|
105
|
+
stream: bool = False,
|
|
106
|
+
) -> LLMResponse:
|
|
107
|
+
# Format messages for OpenRouter API
|
|
108
|
+
formatted_messages = []
|
|
109
|
+
for msg in messages:
|
|
110
|
+
if msg.role == "tool":
|
|
111
|
+
formatted_msg = {
|
|
112
|
+
"role": "tool",
|
|
113
|
+
"content": msg.content,
|
|
114
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
115
|
+
}
|
|
116
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
117
|
+
# Preserve tool_calls in assistant messages
|
|
118
|
+
formatted_msg = {
|
|
119
|
+
"role": "assistant",
|
|
120
|
+
"content": msg.content or "",
|
|
121
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
122
|
+
}
|
|
123
|
+
else:
|
|
124
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
125
|
+
formatted_messages.append(formatted_msg)
|
|
126
|
+
|
|
127
|
+
# Format tools (OpenAI-compatible format)
|
|
128
|
+
formatted_tools = None
|
|
129
|
+
if tools:
|
|
130
|
+
formatted_tools = []
|
|
131
|
+
for tool in tools:
|
|
132
|
+
formatted_tools.append(
|
|
133
|
+
{
|
|
134
|
+
"type": "function",
|
|
135
|
+
"function": {
|
|
136
|
+
"name": tool["name"],
|
|
137
|
+
"description": tool["description"],
|
|
138
|
+
"parameters": tool.get(
|
|
139
|
+
"parameters", {"type": "object", "properties": {}}
|
|
140
|
+
),
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
payload = {
|
|
146
|
+
"model": self.model,
|
|
147
|
+
"messages": formatted_messages,
|
|
148
|
+
"temperature": 0.0,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if formatted_tools:
|
|
152
|
+
payload["tools"] = formatted_tools
|
|
153
|
+
|
|
154
|
+
response = self.client.post(
|
|
155
|
+
f"{self.BASE_URL}/chat/completions", headers=self._get_headers(), json=payload
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Handle errors with better messages
|
|
159
|
+
if response.status_code == 404:
|
|
160
|
+
raise Exception(
|
|
161
|
+
f"Model '{self.model}' not found on OpenRouter. Try /model to select a different model."
|
|
162
|
+
)
|
|
163
|
+
elif response.status_code == 401:
|
|
164
|
+
raise Exception("Invalid OpenRouter API key. Please check your API key.")
|
|
165
|
+
elif response.status_code == 402:
|
|
166
|
+
raise Exception("OpenRouter credits exhausted. Please add credits to your account.")
|
|
167
|
+
|
|
168
|
+
response.raise_for_status()
|
|
169
|
+
data = response.json()
|
|
170
|
+
|
|
171
|
+
choice = data["choices"][0]
|
|
172
|
+
message = choice["message"]
|
|
173
|
+
|
|
174
|
+
tool_calls = []
|
|
175
|
+
if message.get("tool_calls"):
|
|
176
|
+
for tc in message["tool_calls"]:
|
|
177
|
+
args = (
|
|
178
|
+
json.loads(tc["function"]["arguments"])
|
|
179
|
+
if tc["function"].get("arguments")
|
|
180
|
+
else {}
|
|
181
|
+
)
|
|
182
|
+
tool_call = ToolCall(
|
|
183
|
+
name=tc["function"]["name"],
|
|
184
|
+
arguments=args,
|
|
185
|
+
id=tc.get("id", tc["function"]["name"]),
|
|
186
|
+
)
|
|
187
|
+
tool_calls.append(tool_call)
|
|
188
|
+
|
|
189
|
+
content = message.get("content") or "Calling tool..."
|
|
190
|
+
|
|
191
|
+
return LLMResponse(
|
|
192
|
+
message=Message(role="assistant", content=content),
|
|
193
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
194
|
+
raw=data,
|
|
195
|
+
)
|
providers/together.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Together AI Provider - Open source models at scale
|
|
3
|
+
https://together.ai/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from providers.base import LLMProvider, ModelInfo
|
|
11
|
+
from schemas.message import LLMResponse, Message, ToolCall
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TogetherProvider(LLMProvider):
|
|
15
|
+
"""
|
|
16
|
+
Together AI provider for open source models.
|
|
17
|
+
Supports Llama, Mistral, Code Llama, and more.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "together"
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODELS = [
|
|
23
|
+
"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
|
|
24
|
+
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
|
25
|
+
"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
|
|
26
|
+
"mistralai/Mixtral-8x22B-Instruct-v0.1",
|
|
27
|
+
"mistralai/Mistral-7B-Instruct-v0.3",
|
|
28
|
+
"Qwen/Qwen2-72B-Instruct",
|
|
29
|
+
"deepseek-ai/deepseek-coder-33b-instruct",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
BASE_URL = "https://api.together.xyz/v1"
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
model: str = "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
):
|
|
39
|
+
self.model = model
|
|
40
|
+
self.api_key = api_key
|
|
41
|
+
self.client = httpx.Client(timeout=120.0)
|
|
42
|
+
|
|
43
|
+
def _get_headers(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def list_models(self) -> list[ModelInfo]:
|
|
50
|
+
"""Fetch available models from Together API."""
|
|
51
|
+
try:
|
|
52
|
+
response = self.client.get(f"{self.BASE_URL}/models", headers=self._get_headers())
|
|
53
|
+
response.raise_for_status()
|
|
54
|
+
data = response.json()
|
|
55
|
+
|
|
56
|
+
models = []
|
|
57
|
+
# Filter to chat/instruct models
|
|
58
|
+
for model in data:
|
|
59
|
+
model_id = model.get("id", "")
|
|
60
|
+
model_type = model.get("type", "")
|
|
61
|
+
if "chat" in model_type.lower() or "instruct" in model_id.lower():
|
|
62
|
+
models.append(
|
|
63
|
+
ModelInfo(
|
|
64
|
+
id=model_id,
|
|
65
|
+
name=model.get("display_name", model_id),
|
|
66
|
+
context_length=model.get("context_length"),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
models.sort(key=lambda m: m.name)
|
|
71
|
+
return models[:50] if models else [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
72
|
+
|
|
73
|
+
except Exception:
|
|
74
|
+
return [ModelInfo(id=m, name=m) for m in self.DEFAULT_MODELS]
|
|
75
|
+
|
|
76
|
+
def generate(
|
|
77
|
+
self,
|
|
78
|
+
messages: list[Message],
|
|
79
|
+
tools: list | None = None,
|
|
80
|
+
stream: bool = False,
|
|
81
|
+
) -> LLMResponse:
|
|
82
|
+
# Format messages for Together API
|
|
83
|
+
formatted_messages = []
|
|
84
|
+
for msg in messages:
|
|
85
|
+
if msg.role == "tool":
|
|
86
|
+
formatted_msg = {
|
|
87
|
+
"role": "tool",
|
|
88
|
+
"content": msg.content,
|
|
89
|
+
"tool_call_id": msg.tool_call_id or msg.name,
|
|
90
|
+
}
|
|
91
|
+
elif msg.role == "assistant" and msg.metadata and msg.metadata.get("tool_calls"):
|
|
92
|
+
# Preserve tool_calls in assistant messages
|
|
93
|
+
formatted_msg = {
|
|
94
|
+
"role": "assistant",
|
|
95
|
+
"content": msg.content or "",
|
|
96
|
+
"tool_calls": msg.metadata["tool_calls"],
|
|
97
|
+
}
|
|
98
|
+
else:
|
|
99
|
+
formatted_msg = {"role": msg.role, "content": msg.content}
|
|
100
|
+
formatted_messages.append(formatted_msg)
|
|
101
|
+
|
|
102
|
+
# Format tools
|
|
103
|
+
formatted_tools = None
|
|
104
|
+
if tools:
|
|
105
|
+
formatted_tools = []
|
|
106
|
+
for tool in tools:
|
|
107
|
+
formatted_tools.append(
|
|
108
|
+
{
|
|
109
|
+
"type": "function",
|
|
110
|
+
"function": {
|
|
111
|
+
"name": tool["name"],
|
|
112
|
+
"description": tool["description"],
|
|
113
|
+
"parameters": tool.get(
|
|
114
|
+
"parameters", {"type": "object", "properties": {}}
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
payload = {
|
|
121
|
+
"model": self.model,
|
|
122
|
+
"messages": formatted_messages,
|
|
123
|
+
"temperature": 0.0,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if formatted_tools:
|
|
127
|
+
payload["tools"] = formatted_tools
|
|
128
|
+
|
|
129
|
+
response = self.client.post(
|
|
130
|
+
f"{self.BASE_URL}/chat/completions", headers=self._get_headers(), json=payload
|
|
131
|
+
)
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
data = response.json()
|
|
134
|
+
|
|
135
|
+
choice = data["choices"][0]
|
|
136
|
+
message = choice["message"]
|
|
137
|
+
|
|
138
|
+
tool_calls = []
|
|
139
|
+
if message.get("tool_calls"):
|
|
140
|
+
for tc in message["tool_calls"]:
|
|
141
|
+
args = (
|
|
142
|
+
json.loads(tc["function"]["arguments"])
|
|
143
|
+
if tc["function"].get("arguments")
|
|
144
|
+
else {}
|
|
145
|
+
)
|
|
146
|
+
tool_call = ToolCall(
|
|
147
|
+
name=tc["function"]["name"],
|
|
148
|
+
arguments=args,
|
|
149
|
+
id=tc.get("id", tc["function"]["name"]),
|
|
150
|
+
)
|
|
151
|
+
tool_calls.append(tool_call)
|
|
152
|
+
|
|
153
|
+
content = message.get("content") or "Calling tool..."
|
|
154
|
+
|
|
155
|
+
return LLMResponse(
|
|
156
|
+
message=Message(role="assistant", content=content),
|
|
157
|
+
tool_calls=tool_calls if tool_calls else None,
|
|
158
|
+
raw=data,
|
|
159
|
+
)
|
schemas/message.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ToolCall:
|
|
7
|
+
name: str
|
|
8
|
+
arguments: dict[str, Any]
|
|
9
|
+
id: str | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Message:
|
|
14
|
+
role: str # "system" | "user" | "tool" | "assistant"
|
|
15
|
+
content: str
|
|
16
|
+
name: str | None = None
|
|
17
|
+
tool_call_id: str | None = None
|
|
18
|
+
metadata: dict[str, Any] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Tool:
|
|
23
|
+
name: str
|
|
24
|
+
description: str
|
|
25
|
+
arguments: dict[str, Any] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LLMResponse:
|
|
30
|
+
message: Message
|
|
31
|
+
tool_calls: list[ToolCall] | None = None
|
|
32
|
+
raw: Any | None = None
|
schemas/task.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Task schema for tracking work progress."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskStatus(str, Enum):
|
|
9
|
+
"""Status of a task."""
|
|
10
|
+
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
IN_PROGRESS = "in_progress"
|
|
13
|
+
COMPLETED = "completed"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Task:
|
|
18
|
+
"""A single task to track."""
|
|
19
|
+
|
|
20
|
+
content: str # What needs to be done (imperative form)
|
|
21
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
22
|
+
id: str | None = None
|
|
23
|
+
active_form: str | None = None # Present continuous form (e.g., "Running tests")
|
|
24
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
25
|
+
completed_at: datetime | None = None
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
"""Convert task to dictionary."""
|
|
29
|
+
return {
|
|
30
|
+
"id": self.id,
|
|
31
|
+
"content": self.content,
|
|
32
|
+
"status": self.status.value,
|
|
33
|
+
"active_form": self.active_form,
|
|
34
|
+
"created_at": self.created_at.isoformat(),
|
|
35
|
+
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, data: dict) -> "Task":
|
|
40
|
+
"""Create task from dictionary."""
|
|
41
|
+
return cls(
|
|
42
|
+
id=data.get("id"),
|
|
43
|
+
content=data["content"],
|
|
44
|
+
status=TaskStatus(data.get("status", "pending")),
|
|
45
|
+
active_form=data.get("active_form") or data.get("activeForm"),
|
|
46
|
+
created_at=datetime.fromisoformat(data["created_at"])
|
|
47
|
+
if data.get("created_at")
|
|
48
|
+
else datetime.now(),
|
|
49
|
+
completed_at=datetime.fromisoformat(data["completed_at"])
|
|
50
|
+
if data.get("completed_at")
|
|
51
|
+
else None,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class TaskList:
|
|
57
|
+
"""A list of tasks."""
|
|
58
|
+
|
|
59
|
+
tasks: list[Task] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
def add(self, task: Task) -> Task:
|
|
62
|
+
"""Add a task to the list."""
|
|
63
|
+
if not task.id:
|
|
64
|
+
task.id = f"task_{len(self.tasks) + 1}"
|
|
65
|
+
self.tasks.append(task)
|
|
66
|
+
return task
|
|
67
|
+
|
|
68
|
+
def get(self, task_id: str) -> Task | None:
|
|
69
|
+
"""Get a task by ID."""
|
|
70
|
+
for task in self.tasks:
|
|
71
|
+
if task.id == task_id:
|
|
72
|
+
return task
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def update_status(self, task_id: str, status: TaskStatus) -> Task | None:
|
|
76
|
+
"""Update a task's status."""
|
|
77
|
+
task = self.get(task_id)
|
|
78
|
+
if task:
|
|
79
|
+
task.status = status
|
|
80
|
+
if status == TaskStatus.COMPLETED:
|
|
81
|
+
task.completed_at = datetime.now()
|
|
82
|
+
return task
|
|
83
|
+
|
|
84
|
+
def get_by_status(self, status: TaskStatus) -> list[Task]:
|
|
85
|
+
"""Get all tasks with a specific status."""
|
|
86
|
+
return [t for t in self.tasks if t.status == status]
|
|
87
|
+
|
|
88
|
+
def get_current(self) -> Task | None:
|
|
89
|
+
"""Get the current in-progress task."""
|
|
90
|
+
in_progress = self.get_by_status(TaskStatus.IN_PROGRESS)
|
|
91
|
+
return in_progress[0] if in_progress else None
|
|
92
|
+
|
|
93
|
+
def clear(self):
|
|
94
|
+
"""Clear all tasks."""
|
|
95
|
+
self.tasks = []
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def pending_count(self) -> int:
|
|
99
|
+
return len(self.get_by_status(TaskStatus.PENDING))
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def in_progress_count(self) -> int:
|
|
103
|
+
return len(self.get_by_status(TaskStatus.IN_PROGRESS))
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def completed_count(self) -> int:
|
|
107
|
+
return len(self.get_by_status(TaskStatus.COMPLETED))
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def total_count(self) -> int:
|
|
111
|
+
return len(self.tasks)
|
|
112
|
+
|
|
113
|
+
def to_list(self) -> list[dict]:
|
|
114
|
+
"""Convert to list of dictionaries."""
|
|
115
|
+
return [t.to_dict() for t in self.tasks]
|
schemas/tool.py
ADDED
|
File without changes
|
tools/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevOrch Tools - Tools for interacting with the system.
|
|
3
|
+
|
|
4
|
+
Available tools:
|
|
5
|
+
- ShellTool: Execute shell commands
|
|
6
|
+
- OpenTerminalTool: Run a command in a new terminal window (servers, scaffolds, long-running processes)
|
|
7
|
+
- TerminalSessionTool: Managed background sessions with read/send/stop/list
|
|
8
|
+
- FilesystemTool: Read, write, list files with line-specific control
|
|
9
|
+
- SearchTool: Find files by glob patterns
|
|
10
|
+
- GrepTool: Search file contents with regex
|
|
11
|
+
- EditTool: Make targeted edits to files
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from tools.edit import EditTool
|
|
15
|
+
from tools.filesystem import FilesystemTool
|
|
16
|
+
from tools.grep import GrepTool
|
|
17
|
+
from tools.search import SearchTool
|
|
18
|
+
from tools.shell import ShellTool
|
|
19
|
+
from tools.terminal import OpenTerminalTool
|
|
20
|
+
from tools.terminal_session import TerminalSessionTool
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ShellTool",
|
|
24
|
+
"OpenTerminalTool",
|
|
25
|
+
"TerminalSessionTool",
|
|
26
|
+
"FilesystemTool",
|
|
27
|
+
"SearchTool",
|
|
28
|
+
"GrepTool",
|
|
29
|
+
"EditTool",
|
|
30
|
+
]
|
tools/base.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Tool(ABC):
|
|
8
|
+
"""
|
|
9
|
+
Base class for all tools.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
name: str = ""
|
|
13
|
+
description: str = ""
|
|
14
|
+
args_schema: type[BaseModel] = None
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def run(self, arguments: dict[str, Any]) -> Any:
|
|
18
|
+
"""
|
|
19
|
+
Execute the tool with given arguments.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
def schema(self) -> dict[str, Any]:
|
|
24
|
+
"""
|
|
25
|
+
JSON schema exposed to LLMs.
|
|
26
|
+
"""
|
|
27
|
+
parameters = {"type": "object", "properties": {}}
|
|
28
|
+
if self.args_schema:
|
|
29
|
+
schema_dump = self.args_schema.model_json_schema()
|
|
30
|
+
parameters = {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"properties": schema_dump.get("properties", {}),
|
|
33
|
+
"required": schema_dump.get("required", []),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
"name": self.name,
|
|
38
|
+
"description": self.description,
|
|
39
|
+
"parameters": parameters,
|
|
40
|
+
}
|