raggiecode 0.2.1__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.
- Agent/__init__.py +0 -0
- Agent/agent.py +891 -0
- Agent/chat_history_db.py +1500 -0
- Agent/command.py +49 -0
- Agent/config.py +46 -0
- Agent/effort_levels.py +33 -0
- Agent/git_manager.py +727 -0
- Agent/tools.py +35 -0
- Commands/__init__.py +18 -0
- Commands/effort.py +42 -0
- Commands/global_todo.py +23 -0
- Commands/help.py +22 -0
- Commands/reasoning.py +24 -0
- Commands/redo.py +11 -0
- Commands/reindex.py +27 -0
- Commands/shell.py +28 -0
- Commands/stream.py +24 -0
- Commands/undo.py +13 -0
- Commands/unlimited_effort.py +8 -0
- Commands/window_size.py +29 -0
- RAG/__init__.py +0 -0
- RAG/document.py +119 -0
- RAG/find.py +408 -0
- RAG/graph.py +231 -0
- Tools/GetFileCodeStructure.py +43 -0
- Tools/GetSymbolSourceCode.py +27 -0
- Tools/__init__.py +39 -0
- Tools/ask_user.py +102 -0
- Tools/dispatch_subagent.py +215 -0
- Tools/document.py +35 -0
- Tools/edit_symbol.py +250 -0
- Tools/fuzzy_search.py +119 -0
- Tools/list_dir.py +51 -0
- Tools/read.py +49 -0
- Tools/read_image.py +75 -0
- Tools/remove.py +75 -0
- Tools/replace.py +305 -0
- Tools/search.py +41 -0
- Tools/shell.py +149 -0
- Tools/shell_kill.py +87 -0
- Tools/temp_background_service.py +113 -0
- Tools/todo_list.py +481 -0
- Tools/utils.py +116 -0
- Tools/view_changes.py +179 -0
- Tools/walk_call_tree.py +30 -0
- Tools/web_fetch.py +175 -0
- Tools/web_search.py +69 -0
- Tools/write.py +48 -0
- cli.py +111 -0
- config/__init__.py +0 -0
- config/coder_system_prompt.md +119 -0
- config/roles.json +43 -0
- config/tools.json +709 -0
- indexing/__init__.py +0 -0
- indexing/cli.py +128 -0
- indexing/code_index_sdk.py +832 -0
- indexing/code_indexer.py +1763 -0
- indexing/db_schema.py +396 -0
- indexing/export_to_json.py +346 -0
- indexing/extractors.py +189 -0
- indexing/file_utils.py +97 -0
- indexing/frontend/__init__.py +0 -0
- indexing/frontend/css_extractor.py +195 -0
- indexing/frontend/css_parser.py +387 -0
- indexing/frontend/css_selector_utils.py +226 -0
- indexing/frontend/edit_safety.py +573 -0
- indexing/frontend/graph.py +838 -0
- indexing/frontend/html_extractor.py +496 -0
- indexing/frontend/html_parser.py +314 -0
- indexing/frontend/jsx_extractor.py +1204 -0
- indexing/frontend/location_lookup.py +247 -0
- indexing/frontend/resolver.py +485 -0
- indexing/frontend/runtime_resolver.py +862 -0
- indexing/frontend/semantic_output.py +705 -0
- indexing/frontend/source_location.py +69 -0
- indexing/frontend_config.py +72 -0
- indexing/frontend_models.py +347 -0
- indexing/language_config.py +360 -0
- indexing/models.py +284 -0
- indexing/node_utils.py +1112 -0
- indexing/parse_worker.py +1082 -0
- indexing/queries.py +1542 -0
- indexing/sdk_examples.py +426 -0
- interactive.py +248 -0
- raggie.py +673 -0
- raggiecode-0.2.1.dist-info/METADATA +944 -0
- raggiecode-0.2.1.dist-info/RECORD +93 -0
- raggiecode-0.2.1.dist-info/WHEEL +5 -0
- raggiecode-0.2.1.dist-info/entry_points.txt +2 -0
- raggiecode-0.2.1.dist-info/top_level.txt +10 -0
- skills/__init__.py +3 -0
- skills/manager.py +114 -0
- skills/tool.py +121 -0
Tools/view_changes.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from Agent.git_manager import GitManager
|
|
3
|
+
from .utils import BLUE, RESET
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle(arguments, toolcall_id):
|
|
7
|
+
"""Handle ViewChanges tool calls."""
|
|
8
|
+
print(f"{BLUE}ViewChanges{RESET}")
|
|
9
|
+
|
|
10
|
+
view_type = arguments.get("view_type", "status")
|
|
11
|
+
path = arguments.get("path")
|
|
12
|
+
max_count = arguments.get("max_count", 10)
|
|
13
|
+
category = arguments.get("category")
|
|
14
|
+
max_diff_lines = arguments.get("max_diff_lines", 500)
|
|
15
|
+
|
|
16
|
+
# --- Input validation ---
|
|
17
|
+
VALID_VIEW_TYPES = {'status', 'diff', 'log'}
|
|
18
|
+
if view_type not in VALID_VIEW_TYPES:
|
|
19
|
+
return {
|
|
20
|
+
"role": "tool",
|
|
21
|
+
"tool_call_id": toolcall_id,
|
|
22
|
+
"content": (
|
|
23
|
+
f"Unknown view_type: '{view_type}'. "
|
|
24
|
+
f"Supported values: {', '.join(sorted(VALID_VIEW_TYPES))}."
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# Treat empty string category as None (no filter)
|
|
29
|
+
if category is not None and category == "":
|
|
30
|
+
category = None
|
|
31
|
+
|
|
32
|
+
VALID_CATEGORIES = {'added', 'modified', 'deleted', 'unchanged'}
|
|
33
|
+
if category is not None and category not in VALID_CATEGORIES:
|
|
34
|
+
return {
|
|
35
|
+
"role": "tool",
|
|
36
|
+
"tool_call_id": toolcall_id,
|
|
37
|
+
"content": (
|
|
38
|
+
f"Invalid category: '{category}'. "
|
|
39
|
+
f"Must be one of: {', '.join(sorted(VALID_CATEGORIES))}."
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Validate max_count
|
|
44
|
+
if not isinstance(max_count, (int, float)) or isinstance(max_count, bool):
|
|
45
|
+
return {
|
|
46
|
+
"role": "tool",
|
|
47
|
+
"tool_call_id": toolcall_id,
|
|
48
|
+
"content": f"Invalid max_count: must be a number, got {type(max_count).__name__}.",
|
|
49
|
+
}
|
|
50
|
+
max_count = int(max_count)
|
|
51
|
+
if max_count < 0:
|
|
52
|
+
return {
|
|
53
|
+
"role": "tool",
|
|
54
|
+
"tool_call_id": toolcall_id,
|
|
55
|
+
"content": f"Invalid max_count: must be non-negative, got {max_count}.",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Validate max_diff_lines
|
|
59
|
+
if max_diff_lines is not None:
|
|
60
|
+
if not isinstance(max_diff_lines, (int, float)) or isinstance(max_diff_lines, bool):
|
|
61
|
+
return {
|
|
62
|
+
"role": "tool",
|
|
63
|
+
"tool_call_id": toolcall_id,
|
|
64
|
+
"content": f"Invalid max_diff_lines: must be a number, got {type(max_diff_lines).__name__}.",
|
|
65
|
+
}
|
|
66
|
+
max_diff_lines = int(max_diff_lines)
|
|
67
|
+
if max_diff_lines < 0:
|
|
68
|
+
return {
|
|
69
|
+
"role": "tool",
|
|
70
|
+
"tool_call_id": toolcall_id,
|
|
71
|
+
"content": f"Invalid max_diff_lines: must be non-negative, got {max_diff_lines}.",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
git_manager = GitManager(root_dir=os.getcwd())
|
|
76
|
+
|
|
77
|
+
if view_type == "status":
|
|
78
|
+
status = git_manager.get_status(category=category)
|
|
79
|
+
result_parts = []
|
|
80
|
+
|
|
81
|
+
if status["commit_id"]:
|
|
82
|
+
result_parts.append(f"Last commit: {status['commit_id'][:8]} - {status['commit_message']}")
|
|
83
|
+
else:
|
|
84
|
+
result_parts.append("No commits yet.")
|
|
85
|
+
|
|
86
|
+
if category:
|
|
87
|
+
# Only show the requested category
|
|
88
|
+
label = category.capitalize()
|
|
89
|
+
items = status[category]
|
|
90
|
+
result_parts.append("")
|
|
91
|
+
result_parts.append(f"{label} ({len(items)}):")
|
|
92
|
+
for f in items[:50]:
|
|
93
|
+
symbol = {"added": "+", "modified": "~", "deleted": "-", "unchanged": " "}.get(category, "")
|
|
94
|
+
result_parts.append(f" {symbol} {f}")
|
|
95
|
+
if len(items) > 50:
|
|
96
|
+
result_parts.append(f" ... and {len(items) - 50} more")
|
|
97
|
+
else:
|
|
98
|
+
# Show all categories
|
|
99
|
+
result_parts.append("")
|
|
100
|
+
result_parts.append(f"Added ({len(status['added'])}):")
|
|
101
|
+
for f in status["added"][:50]:
|
|
102
|
+
result_parts.append(f" + {f}")
|
|
103
|
+
if len(status["added"]) > 50:
|
|
104
|
+
result_parts.append(f" ... and {len(status['added']) - 50} more")
|
|
105
|
+
|
|
106
|
+
result_parts.append("")
|
|
107
|
+
result_parts.append(f"Modified ({len(status['modified'])}):")
|
|
108
|
+
for f in status["modified"][:50]:
|
|
109
|
+
result_parts.append(f" ~ {f}")
|
|
110
|
+
if len(status["modified"]) > 50:
|
|
111
|
+
result_parts.append(f" ... and {len(status['modified']) - 50} more")
|
|
112
|
+
|
|
113
|
+
result_parts.append("")
|
|
114
|
+
result_parts.append(f"Deleted ({len(status['deleted'])}):")
|
|
115
|
+
for f in status["deleted"][:50]:
|
|
116
|
+
result_parts.append(f" - {f}")
|
|
117
|
+
if len(status["deleted"]) > 50:
|
|
118
|
+
result_parts.append(f" ... and {len(status['deleted']) - 50} more")
|
|
119
|
+
|
|
120
|
+
result_parts.append("")
|
|
121
|
+
result_parts.append(f"Unchanged ({len(status['unchanged'])} files)")
|
|
122
|
+
|
|
123
|
+
content = "\n".join(result_parts)
|
|
124
|
+
|
|
125
|
+
elif view_type == "diff":
|
|
126
|
+
diffs = git_manager.get_diff(path_filter=path, max_diff_lines=max_diff_lines)
|
|
127
|
+
|
|
128
|
+
if not diffs:
|
|
129
|
+
content = "No differences found between working tree and last commit."
|
|
130
|
+
else:
|
|
131
|
+
result_parts = []
|
|
132
|
+
result_parts.append(f"Showing {len(diffs)} changed file(s):")
|
|
133
|
+
if path:
|
|
134
|
+
result_parts.append(f"(filtered to files containing '{path}')")
|
|
135
|
+
if max_diff_lines:
|
|
136
|
+
result_parts.append(f"(diffs truncated to {max_diff_lines} lines each)")
|
|
137
|
+
result_parts.append("")
|
|
138
|
+
|
|
139
|
+
for d in diffs:
|
|
140
|
+
change_symbol = {"added": "+", "modified": "~", "deleted": "-"}.get(d["change_type"], "?")
|
|
141
|
+
result_parts.append(f"{'='*60}")
|
|
142
|
+
result_parts.append(f"{change_symbol} {d['change_type'].upper()}: {d['path']}")
|
|
143
|
+
result_parts.append(f"{'='*60}")
|
|
144
|
+
result_parts.append(d["content"])
|
|
145
|
+
result_parts.append("")
|
|
146
|
+
|
|
147
|
+
content = "\n".join(result_parts)
|
|
148
|
+
|
|
149
|
+
elif view_type == "log":
|
|
150
|
+
commits = git_manager.get_log(max_count=max_count)
|
|
151
|
+
|
|
152
|
+
if not commits:
|
|
153
|
+
content = "No commits found."
|
|
154
|
+
else:
|
|
155
|
+
result_parts = []
|
|
156
|
+
result_parts.append(f"Last {len(commits)} commit(s):")
|
|
157
|
+
result_parts.append("")
|
|
158
|
+
for c in commits:
|
|
159
|
+
short_id = c["commit_id"][:8]
|
|
160
|
+
result_parts.append(f" commit {short_id}")
|
|
161
|
+
result_parts.append(f" Author: {c['author']}")
|
|
162
|
+
result_parts.append(f" Date: {c['timestamp']}")
|
|
163
|
+
result_parts.append("")
|
|
164
|
+
result_parts.append(f" {c['message']}")
|
|
165
|
+
result_parts.append("")
|
|
166
|
+
content = "\n".join(result_parts)
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
"role": "tool",
|
|
170
|
+
"tool_call_id": toolcall_id,
|
|
171
|
+
"content": content,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
return {
|
|
176
|
+
"role": "tool",
|
|
177
|
+
"tool_call_id": toolcall_id,
|
|
178
|
+
"content": f"Error executing ViewChanges: {str(e)}",
|
|
179
|
+
}
|
Tools/walk_call_tree.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from RAG.graph import walk_call_tree
|
|
2
|
+
from .utils import is_within_cwd, BLUE, RESET
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle(arguments, toolcall_id):
|
|
6
|
+
symbol_name = arguments["symbol_name"]
|
|
7
|
+
file_path = arguments.get("file_path")
|
|
8
|
+
max_depth = arguments.get("max_depth", 5)
|
|
9
|
+
include_external = arguments.get("include_external", False)
|
|
10
|
+
exclude = arguments.get("exclude")
|
|
11
|
+
print(f"{BLUE}WalkCallTree {symbol_name} (depth={max_depth}){RESET}")
|
|
12
|
+
|
|
13
|
+
# Check if the file is outside the current working directory
|
|
14
|
+
if file_path and not is_within_cwd(file_path):
|
|
15
|
+
return {
|
|
16
|
+
"role": "tool",
|
|
17
|
+
"tool_call_id": toolcall_id,
|
|
18
|
+
"content": "Error: access denied - path is outside the current working directory",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
result = walk_call_tree(symbol_name, file_path, max_depth, include_external, exclude)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
result = f"Error walking call tree: {str(e)}"
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
"role": "tool",
|
|
28
|
+
"tool_call_id": toolcall_id,
|
|
29
|
+
"content": result,
|
|
30
|
+
}
|
Tools/web_fetch.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import gzip
|
|
3
|
+
import zlib
|
|
4
|
+
from html.parser import HTMLParser
|
|
5
|
+
from urllib.request import Request, urlopen
|
|
6
|
+
from urllib.error import HTTPError, URLError
|
|
7
|
+
|
|
8
|
+
from .utils import BLUE, RED, RESET
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_CHARS = 20000
|
|
12
|
+
DEFAULT_TIMEOUT = 20
|
|
13
|
+
USER_AGENT = "Mozilla/5.0 (compatible; RaggieBot/1.0; +https://github.com/myuser/raggie)"
|
|
14
|
+
|
|
15
|
+
_BLOCK_TAGS = {
|
|
16
|
+
"p", "div", "section", "article", "header", "footer", "main", "br",
|
|
17
|
+
"h1", "h2", "h3", "h4", "h5", "h6", "li", "tr", "ul", "ol", "table",
|
|
18
|
+
"blockquote", "pre", "hr", "figure", "figcaption", "nav",
|
|
19
|
+
}
|
|
20
|
+
_SKIP_TAGS = {"script", "style", "noscript", "template", "svg", "head"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _TextExtractor(HTMLParser):
|
|
24
|
+
"""Convert HTML into readable plain text using only the stdlib.
|
|
25
|
+
|
|
26
|
+
Drops script/style/etc., inserts newlines around block-level elements,
|
|
27
|
+
and collapses runs of whitespace.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__(convert_charrefs=True)
|
|
32
|
+
self._parts = []
|
|
33
|
+
self._skip_depth = 0
|
|
34
|
+
|
|
35
|
+
def handle_starttag(self, tag, attrs):
|
|
36
|
+
if tag in _SKIP_TAGS:
|
|
37
|
+
self._skip_depth += 1
|
|
38
|
+
elif tag in _BLOCK_TAGS:
|
|
39
|
+
self._parts.append("\n")
|
|
40
|
+
|
|
41
|
+
def handle_endtag(self, tag):
|
|
42
|
+
if tag in _SKIP_TAGS and self._skip_depth > 0:
|
|
43
|
+
self._skip_depth -= 1
|
|
44
|
+
elif tag in _BLOCK_TAGS:
|
|
45
|
+
self._parts.append("\n")
|
|
46
|
+
|
|
47
|
+
def handle_data(self, data):
|
|
48
|
+
if self._skip_depth == 0 and data.strip():
|
|
49
|
+
self._parts.append(data)
|
|
50
|
+
|
|
51
|
+
def get_text(self):
|
|
52
|
+
text = "".join(self._parts)
|
|
53
|
+
# Collapse horizontal whitespace, then squeeze blank lines.
|
|
54
|
+
text = re.sub(r"[ \t\r\f\v]+", " ", text)
|
|
55
|
+
text = re.sub(r"\n[ \t]+", "\n", text)
|
|
56
|
+
text = re.sub(r"\n{3,}", "\n\n", text)
|
|
57
|
+
return text.strip()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _decode_body(raw, headers):
|
|
61
|
+
"""Decompress (if needed) and decode the response body to str."""
|
|
62
|
+
encoding = (headers.get("Content-Encoding") or "").lower()
|
|
63
|
+
if "gzip" in encoding:
|
|
64
|
+
try:
|
|
65
|
+
raw = gzip.decompress(raw)
|
|
66
|
+
except OSError as e:
|
|
67
|
+
print(f"{RED}Warning: Failed to decompress gzip response: {e}{RESET}")
|
|
68
|
+
elif "deflate" in encoding:
|
|
69
|
+
try:
|
|
70
|
+
raw = zlib.decompress(raw)
|
|
71
|
+
except zlib.error:
|
|
72
|
+
raw = zlib.decompress(raw, -zlib.MAX_WBITS)
|
|
73
|
+
|
|
74
|
+
charset = "utf-8"
|
|
75
|
+
content_type = headers.get("Content-Type", "")
|
|
76
|
+
match = re.search(r"charset=([\w\-]+)", content_type, re.IGNORECASE)
|
|
77
|
+
if match:
|
|
78
|
+
charset = match.group(1)
|
|
79
|
+
try:
|
|
80
|
+
return raw.decode(charset, errors="replace")
|
|
81
|
+
except (LookupError, TypeError):
|
|
82
|
+
return raw.decode("utf-8", errors="replace")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def handle(arguments, toolcall_id):
|
|
86
|
+
url = arguments.get("url")
|
|
87
|
+
print(f"{BLUE}WebFetch {url}{RESET}")
|
|
88
|
+
|
|
89
|
+
if not url:
|
|
90
|
+
return {
|
|
91
|
+
"role": "tool",
|
|
92
|
+
"tool_call_id": toolcall_id,
|
|
93
|
+
"content": "Error: 'url' is required",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if not re.match(r"^https?://", url, re.IGNORECASE):
|
|
97
|
+
return {
|
|
98
|
+
"role": "tool",
|
|
99
|
+
"tool_call_id": toolcall_id,
|
|
100
|
+
"content": "Error: url must start with http:// or https://",
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
max_chars = arguments.get("max_chars", DEFAULT_MAX_CHARS)
|
|
104
|
+
try:
|
|
105
|
+
max_chars = int(max_chars)
|
|
106
|
+
except (TypeError, ValueError):
|
|
107
|
+
max_chars = DEFAULT_MAX_CHARS
|
|
108
|
+
|
|
109
|
+
req = Request(
|
|
110
|
+
url,
|
|
111
|
+
headers={
|
|
112
|
+
"User-Agent": USER_AGENT,
|
|
113
|
+
"Accept": "text/html,application/xhtml+xml,application/json,text/plain,*/*",
|
|
114
|
+
"Accept-Encoding": "gzip, deflate",
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
with urlopen(req, timeout=DEFAULT_TIMEOUT) as resp:
|
|
120
|
+
headers = resp.headers
|
|
121
|
+
raw = resp.read()
|
|
122
|
+
final_url = resp.geturl()
|
|
123
|
+
except HTTPError as err:
|
|
124
|
+
return {
|
|
125
|
+
"role": "tool",
|
|
126
|
+
"tool_call_id": toolcall_id,
|
|
127
|
+
"content": f"Error: HTTP {err.code} {err.reason} for {url}",
|
|
128
|
+
}
|
|
129
|
+
except URLError as err:
|
|
130
|
+
return {
|
|
131
|
+
"role": "tool",
|
|
132
|
+
"tool_call_id": toolcall_id,
|
|
133
|
+
"content": f"Error: failed to fetch {url}: {err.reason}",
|
|
134
|
+
}
|
|
135
|
+
except Exception as err:
|
|
136
|
+
return {
|
|
137
|
+
"role": "tool",
|
|
138
|
+
"tool_call_id": toolcall_id,
|
|
139
|
+
"content": f"Error fetching {url}: {err}",
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
content_type = (headers.get("Content-Type") or "").lower()
|
|
143
|
+
|
|
144
|
+
if "html" in content_type:
|
|
145
|
+
body = _decode_body(raw, headers)
|
|
146
|
+
parser = _TextExtractor()
|
|
147
|
+
try:
|
|
148
|
+
parser.feed(body)
|
|
149
|
+
text = parser.get_text()
|
|
150
|
+
except Exception:
|
|
151
|
+
text = body
|
|
152
|
+
elif content_type.startswith("text/") or "json" in content_type or "xml" in content_type:
|
|
153
|
+
text = _decode_body(raw, headers)
|
|
154
|
+
else:
|
|
155
|
+
return {
|
|
156
|
+
"role": "tool",
|
|
157
|
+
"tool_call_id": toolcall_id,
|
|
158
|
+
"content": f"Error: unsupported content type '{content_type}' for {url} ({len(raw)} bytes). Only text/HTML/JSON/XML are supported.",
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
truncated = False
|
|
162
|
+
if len(text) > max_chars:
|
|
163
|
+
text = text[:max_chars]
|
|
164
|
+
truncated = True
|
|
165
|
+
|
|
166
|
+
header = f"URL: {final_url}\nContent-Type: {content_type or 'unknown'}\n"
|
|
167
|
+
if truncated:
|
|
168
|
+
header += f"[truncated to {max_chars} chars]\n"
|
|
169
|
+
header += "\n"
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"role": "tool",
|
|
173
|
+
"tool_call_id": toolcall_id,
|
|
174
|
+
"content": header + text,
|
|
175
|
+
}
|
Tools/web_search.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from .utils import BLUE, RESET
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
DEFAULT_MAX_RESULTS = 5
|
|
5
|
+
MAX_RESULTS_CAP = 20
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle(arguments, toolcall_id):
|
|
9
|
+
query = arguments.get("query")
|
|
10
|
+
print(f"{BLUE}WebSearch {query}{RESET}")
|
|
11
|
+
|
|
12
|
+
if not query or not str(query).strip():
|
|
13
|
+
return {
|
|
14
|
+
"role": "tool",
|
|
15
|
+
"tool_call_id": toolcall_id,
|
|
16
|
+
"content": "Error: 'query' is required",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
max_results = arguments.get("max_results", DEFAULT_MAX_RESULTS)
|
|
20
|
+
try:
|
|
21
|
+
max_results = int(max_results)
|
|
22
|
+
except (TypeError, ValueError):
|
|
23
|
+
max_results = DEFAULT_MAX_RESULTS
|
|
24
|
+
max_results = max(1, min(max_results, MAX_RESULTS_CAP))
|
|
25
|
+
|
|
26
|
+
region = arguments.get("region") or "wt-wt"
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from ddgs import DDGS
|
|
30
|
+
except ImportError:
|
|
31
|
+
return {
|
|
32
|
+
"role": "tool",
|
|
33
|
+
"tool_call_id": toolcall_id,
|
|
34
|
+
"content": "Error: the 'ddgs' package is not installed. Run: pip install ddgs",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
results = DDGS().text(query, region=region, max_results=max_results)
|
|
39
|
+
except Exception as err:
|
|
40
|
+
return {
|
|
41
|
+
"role": "tool",
|
|
42
|
+
"tool_call_id": toolcall_id,
|
|
43
|
+
"content": f"Error performing web search: {err}",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if not results:
|
|
47
|
+
return {
|
|
48
|
+
"role": "tool",
|
|
49
|
+
"tool_call_id": toolcall_id,
|
|
50
|
+
"content": f"No results found for '{query}'.",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines = [f"Search results for '{query}':", ""]
|
|
54
|
+
for i, item in enumerate(results, start=1):
|
|
55
|
+
title = (item.get("title") or "").strip()
|
|
56
|
+
href = (item.get("href") or "").strip()
|
|
57
|
+
body = (item.get("body") or "").strip()
|
|
58
|
+
lines.append(f"{i}. {title}")
|
|
59
|
+
if href:
|
|
60
|
+
lines.append(f" URL: {href}")
|
|
61
|
+
if body:
|
|
62
|
+
lines.append(f" {body}")
|
|
63
|
+
lines.append("")
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"role": "tool",
|
|
67
|
+
"tool_call_id": toolcall_id,
|
|
68
|
+
"content": "\n".join(lines).strip(),
|
|
69
|
+
}
|
Tools/write.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from .utils import is_ignored_by_gitignore, is_within_cwd, BLUE, RESET, auto_record_change, reindex_after_change, remove_em_dashes
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def handle(arguments, toolcall_id, session_id=None, code_indexer=None):
|
|
5
|
+
file_path = arguments["file_path"]
|
|
6
|
+
content = arguments["content"]
|
|
7
|
+
print(f"{BLUE}Writing {file_path}{RESET}")
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
# Check if the file is outside the current working directory
|
|
11
|
+
if not is_within_cwd(file_path):
|
|
12
|
+
return {
|
|
13
|
+
"role": "tool",
|
|
14
|
+
"tool_call_id": toolcall_id,
|
|
15
|
+
"content": "Error: access denied - path is outside the current working directory",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Check if file is in .gitignore
|
|
19
|
+
if is_ignored_by_gitignore(file_path):
|
|
20
|
+
return {
|
|
21
|
+
"role": "tool",
|
|
22
|
+
"tool_call_id": toolcall_id,
|
|
23
|
+
"content": f"Error: File '{file_path}' is in .gitignore. Operations on gitignored files are not allowed.",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
content = remove_em_dashes(content)
|
|
27
|
+
|
|
28
|
+
with open(file_path, "w") as f:
|
|
29
|
+
f.write(content)
|
|
30
|
+
|
|
31
|
+
if session_id is not None:
|
|
32
|
+
from Agent.chat_history_db import record_session_file
|
|
33
|
+
record_session_file(session_id, file_path, "write")
|
|
34
|
+
auto_record_change(session_id, file_path, "file_create", f"Created file {file_path}")
|
|
35
|
+
|
|
36
|
+
reindex_after_change(code_indexer)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"role": "tool",
|
|
40
|
+
"tool_call_id": toolcall_id,
|
|
41
|
+
"content": f"file {file_path} written successfully",
|
|
42
|
+
}
|
|
43
|
+
except Exception as e:
|
|
44
|
+
return {
|
|
45
|
+
"role": "tool",
|
|
46
|
+
"tool_call_id": toolcall_id,
|
|
47
|
+
"content": f"Error writing file: {str(e)}",
|
|
48
|
+
}
|
cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
KNOWN_COMMANDS = {"skill", "roles", "keys", "setup"}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _create_agent_parser():
|
|
8
|
+
"""Parser for the default agent mode: raggie <role> <project-dir>."""
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="raggie",
|
|
11
|
+
description="Raggie - AI Coding Agent",
|
|
12
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
13
|
+
epilog="""commands:
|
|
14
|
+
raggie <role> <project-dir> Run the AI agent (e.g. raggie code .)
|
|
15
|
+
raggie skill <role> ... Manage agent skills
|
|
16
|
+
raggie roles List and edit agent roles
|
|
17
|
+
raggie keys Manage API keys
|
|
18
|
+
raggie setup First-time setup (keys + roles)
|
|
19
|
+
|
|
20
|
+
examples:
|
|
21
|
+
raggie code . # interactive mode in current dir
|
|
22
|
+
raggie code /path/to/project # interactive mode in a project
|
|
23
|
+
raggie code . --prompt "Write a hello function" # single prompt
|
|
24
|
+
raggie code /tmp/new-project # creates dir if missing
|
|
25
|
+
raggie skill code --show # list all skills for role
|
|
26
|
+
raggie skill code --show --name testing # show a specific skill
|
|
27
|
+
raggie skill code --import-skill skills.md --name testing # import from file
|
|
28
|
+
raggie skill code --delete --name testing # delete a skill
|
|
29
|
+
raggie skill --list-all # list all skills across all roles
|
|
30
|
+
"""
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument("--version", action="version", version=f"raggie v{__import__('importlib.metadata').metadata.version('raggiecode')}")
|
|
33
|
+
parser.add_argument("role", nargs="?", help="The agent role to use (defined in roles.json)")
|
|
34
|
+
parser.add_argument("project_dir", nargs="?", default=".", help="Path to the project directory (use '.' for current directory)")
|
|
35
|
+
parser.add_argument("--prompt", help="The initial prompt for the agent (if not provided, runs in interactive mode)")
|
|
36
|
+
parser.add_argument("--effort", type=int, choices=[1, 2, 3, 4, 5], help="Effort level: 1=Zen, 2=Serious, 3=Extreme, 4=Feral, 5=Insane (default: 1=Zen)")
|
|
37
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug mode to display tool call outputs")
|
|
38
|
+
return parser
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _create_subcommand_parser():
|
|
42
|
+
"""Parser for subcommands: skill, roles, keys."""
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
description="Raggie - AI Coding Agent",
|
|
45
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument("--version", action="version", version=f"raggie v{__import__('importlib.metadata').metadata.version('raggiecode')}")
|
|
48
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
49
|
+
|
|
50
|
+
# Skill command
|
|
51
|
+
skill_parser = subparsers.add_parser(
|
|
52
|
+
"skill",
|
|
53
|
+
help="Manage agent skills",
|
|
54
|
+
description="Manage named skills stored in the database. A role can have multiple skills, each identified by a unique name.",
|
|
55
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
56
|
+
epilog="""
|
|
57
|
+
Examples:
|
|
58
|
+
raggie skill code --show # list all skills for role 'code'
|
|
59
|
+
raggie skill code --show --name testing # show full content of a specific skill
|
|
60
|
+
raggie skill code --import-skill my-skill.md --name testing # import from file
|
|
61
|
+
raggie skill code --export-skill backup.md --name testing # export to file
|
|
62
|
+
raggie skill code --delete --name testing # delete a skill
|
|
63
|
+
raggie skill --list-all # list all skills across all roles
|
|
64
|
+
"""
|
|
65
|
+
)
|
|
66
|
+
skill_parser.add_argument("role", nargs="?", help="The agent role (required unless using --list-all)")
|
|
67
|
+
skill_parser.add_argument("--name", help="The skill name (required for --import-skill, --export-skill, --delete)")
|
|
68
|
+
skill_parser.add_argument("--import-skill", metavar="FILE", help="Import skill from markdown file into the database (requires --name)")
|
|
69
|
+
skill_parser.add_argument("--export-skill", metavar="FILE", help="Export skill from database to markdown file (requires --name)")
|
|
70
|
+
skill_parser.add_argument("--show", action="store_true", help="Display skill(s) for the role (all if --name omitted)")
|
|
71
|
+
skill_parser.add_argument("--delete", action="store_true", help="Delete a skill (requires --name)")
|
|
72
|
+
skill_parser.add_argument("--list-all", action="store_true", help="List all skills across all roles")
|
|
73
|
+
|
|
74
|
+
# Roles command
|
|
75
|
+
subparsers.add_parser(
|
|
76
|
+
"roles",
|
|
77
|
+
help="List and edit agent roles",
|
|
78
|
+
description="List all agent roles and interactively edit their base URL and model settings.",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Keys command
|
|
82
|
+
subparsers.add_parser(
|
|
83
|
+
"keys",
|
|
84
|
+
help="Manage API keys",
|
|
85
|
+
description="Interactive interface to list, add, and remove API keys stored in keys.json.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Setup command
|
|
89
|
+
subparsers.add_parser(
|
|
90
|
+
"setup",
|
|
91
|
+
help="First-time setup wizard",
|
|
92
|
+
description="Guided first-time setup: configure API keys, then review agent roles.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return parser
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_args():
|
|
99
|
+
"""Parse command-line arguments and return the parsed args."""
|
|
100
|
+
if len(sys.argv) > 1 and sys.argv[1] in KNOWN_COMMANDS:
|
|
101
|
+
parser = _create_subcommand_parser()
|
|
102
|
+
else:
|
|
103
|
+
parser = _create_agent_parser()
|
|
104
|
+
|
|
105
|
+
args = parser.parse_args()
|
|
106
|
+
|
|
107
|
+
if getattr(args, "command", None) is None and getattr(args, "role", None) is None:
|
|
108
|
+
parser.print_help()
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
return args
|
config/__init__.py
ADDED
|
File without changes
|