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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from RAG.find import find_symbol_implementation
|
|
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
|
+
print(f"{BLUE}GetSymbolSourceCode {symbol_name}{RESET}")
|
|
9
|
+
|
|
10
|
+
# Check if the file is outside the current working directory
|
|
11
|
+
if file_path and 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
|
+
try:
|
|
19
|
+
result = find_symbol_implementation(symbol_name, file_path)
|
|
20
|
+
except Exception as e:
|
|
21
|
+
result = f"Error finding symbol implementation: {str(e)}"
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
"role": "tool",
|
|
25
|
+
"tool_call_id": toolcall_id,
|
|
26
|
+
"content": result,
|
|
27
|
+
}
|
Tools/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from Agent.tools import ToolRegistry
|
|
2
|
+
|
|
3
|
+
def setup_toolcalls(registry: ToolRegistry):
|
|
4
|
+
from . import shell, temp_background_service, shell_kill, read, write, replace, remove, GetSymbolSourceCode, GetFileCodeStructure, document, walk_call_tree, list_dir, search, fuzzy_search, read_image, dispatch_subagent, todo_list, web_fetch, web_search, view_changes, ask_user, edit_symbol
|
|
5
|
+
from skills.tool import handle as update_skill_handle
|
|
6
|
+
from skills.tool import handle_get_skill as get_skill_handle
|
|
7
|
+
|
|
8
|
+
registry.set_handler("UglyWholeFileContentDump", read.handle)
|
|
9
|
+
registry.set_handler("SearchAllFilesContent", search.handle)
|
|
10
|
+
registry.set_handler("FileNameSearch", fuzzy_search.handle)
|
|
11
|
+
registry.set_handler("ListDir", list_dir.handle)
|
|
12
|
+
registry.set_handler("WriteFile", write.handle)
|
|
13
|
+
registry.set_handler("Shell", shell.handle)
|
|
14
|
+
registry.set_handler("TempBackgroundService", temp_background_service.handle)
|
|
15
|
+
registry.set_handler("ShellKill", shell_kill.handle)
|
|
16
|
+
registry.set_handler("ReplaceText", replace.handle)
|
|
17
|
+
registry.set_handler("RemoveFile", remove.handle)
|
|
18
|
+
registry.set_handler("GetSymbolSourceCode", GetSymbolSourceCode.handle)
|
|
19
|
+
registry.set_handler("GetFileCodeSemantics", GetFileCodeStructure.handle)
|
|
20
|
+
registry.set_handler("WalkCallTree", walk_call_tree.handle)
|
|
21
|
+
registry.set_handler("Document", document.handle)
|
|
22
|
+
registry.set_handler("SetSkill", update_skill_handle)
|
|
23
|
+
registry.set_handler("GetSkill", get_skill_handle)
|
|
24
|
+
registry.set_handler("ReadImage", read_image.handle)
|
|
25
|
+
registry.set_handler("WebFetch", web_fetch.handle)
|
|
26
|
+
registry.set_handler("WebSearch", web_search.handle)
|
|
27
|
+
registry.set_handler("DispatchSubagent", dispatch_subagent.handle)
|
|
28
|
+
registry.set_handler("CreateTodoList", todo_list.handle_create_todo_list)
|
|
29
|
+
registry.set_handler("AddTask", todo_list.handle_add_task)
|
|
30
|
+
registry.set_handler("GetTodoList", todo_list.handle_get_todo_list)
|
|
31
|
+
registry.set_handler("ApproveTodoList", todo_list.handle_approve_todo_list)
|
|
32
|
+
registry.set_handler("ExecuteNextTask", todo_list.handle_execute_next_task)
|
|
33
|
+
registry.set_handler("MarkTaskComplete", todo_list.handle_mark_task_complete)
|
|
34
|
+
registry.set_handler("MarkTaskFailed", todo_list.handle_mark_task_failed)
|
|
35
|
+
registry.set_handler("MarkTaskCancelled", todo_list.handle_mark_task_cancelled)
|
|
36
|
+
registry.set_handler("GetActiveTodoList", todo_list.handle_get_active_todo_list)
|
|
37
|
+
registry.set_handler("AskUser", ask_user.handle)
|
|
38
|
+
registry.set_handler("ViewChanges", view_changes.handle)
|
|
39
|
+
registry.set_handler("EditSymbol", edit_symbol.handle)
|
Tools/ask_user.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from .utils import BLUE, GREEN, YELLOW, RED, RESET
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def handle(arguments, toolcall_id):
|
|
5
|
+
"""Ask the user a question, optionally with predefined options."""
|
|
6
|
+
from prompt_toolkit import prompt
|
|
7
|
+
|
|
8
|
+
question = arguments.get("question", "")
|
|
9
|
+
options = arguments.get("options", [])
|
|
10
|
+
allow_multiple = arguments.get("allow_multiple", False)
|
|
11
|
+
|
|
12
|
+
if not question:
|
|
13
|
+
return {
|
|
14
|
+
"role": "tool",
|
|
15
|
+
"tool_call_id": toolcall_id,
|
|
16
|
+
"content": "Error: 'question' is required",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
print(f"\n{BLUE}Agent has a question:{RESET}")
|
|
21
|
+
print(f"{YELLOW}{question}{RESET}")
|
|
22
|
+
print()
|
|
23
|
+
|
|
24
|
+
if not options:
|
|
25
|
+
# Free-form question
|
|
26
|
+
user_input = prompt("Your answer: ", multiline=True).strip()
|
|
27
|
+
print()
|
|
28
|
+
return {
|
|
29
|
+
"role": "tool",
|
|
30
|
+
"tool_call_id": toolcall_id,
|
|
31
|
+
"content": f"User answered: {user_input}",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Display numbered options
|
|
35
|
+
print(f"{BLUE}Options:{RESET}")
|
|
36
|
+
for i, opt in enumerate(options):
|
|
37
|
+
label = opt.get("label", f"Option {i + 1}")
|
|
38
|
+
description = opt.get("description", "")
|
|
39
|
+
print(f" {GREEN}{i + 1}.{RESET} {label}")
|
|
40
|
+
if description:
|
|
41
|
+
print(f" {description}")
|
|
42
|
+
print()
|
|
43
|
+
|
|
44
|
+
if allow_multiple:
|
|
45
|
+
hint = "Enter one or more numbers (comma-separated), or type your own answer"
|
|
46
|
+
else:
|
|
47
|
+
hint = "Enter a number, or type your own answer"
|
|
48
|
+
|
|
49
|
+
user_input = prompt(f"{hint}: ", multiline=True).strip()
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
if not user_input:
|
|
53
|
+
return {
|
|
54
|
+
"role": "tool",
|
|
55
|
+
"tool_call_id": toolcall_id,
|
|
56
|
+
"content": "User skipped the question (no answer provided)",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Try to parse as number(s)
|
|
60
|
+
selected_labels = []
|
|
61
|
+
try:
|
|
62
|
+
if allow_multiple:
|
|
63
|
+
indices = [int(x.strip()) for x in user_input.split(",")]
|
|
64
|
+
for idx in indices:
|
|
65
|
+
if 1 <= idx <= len(options):
|
|
66
|
+
selected_labels.append(options[idx - 1].get("label", f"Option {idx}"))
|
|
67
|
+
else:
|
|
68
|
+
selected_labels.append(f"(invalid: {idx})")
|
|
69
|
+
else:
|
|
70
|
+
idx = int(user_input)
|
|
71
|
+
if 1 <= idx <= len(options):
|
|
72
|
+
selected_labels.append(options[idx - 1].get("label", f"Option {idx}"))
|
|
73
|
+
else:
|
|
74
|
+
selected_labels.append(f"(invalid: {idx})")
|
|
75
|
+
answer = ", ".join(selected_labels)
|
|
76
|
+
print(f"{GREEN}User selected: {answer}{RESET}\n")
|
|
77
|
+
return {
|
|
78
|
+
"role": "tool",
|
|
79
|
+
"tool_call_id": toolcall_id,
|
|
80
|
+
"content": f"User selected: {answer}",
|
|
81
|
+
}
|
|
82
|
+
except ValueError:
|
|
83
|
+
# User typed a custom answer
|
|
84
|
+
print(f"{GREEN}User answered: {user_input}{RESET}\n")
|
|
85
|
+
return {
|
|
86
|
+
"role": "tool",
|
|
87
|
+
"tool_call_id": toolcall_id,
|
|
88
|
+
"content": f"User answered: {user_input}",
|
|
89
|
+
}
|
|
90
|
+
except (KeyboardInterrupt, EOFError):
|
|
91
|
+
print(f"\n{RED}User cancelled the question{RESET}\n")
|
|
92
|
+
return {
|
|
93
|
+
"role": "tool",
|
|
94
|
+
"tool_call_id": toolcall_id,
|
|
95
|
+
"content": "User cancelled the question (Ctrl+C)",
|
|
96
|
+
}
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return {
|
|
99
|
+
"role": "tool",
|
|
100
|
+
"tool_call_id": toolcall_id,
|
|
101
|
+
"content": f"Failed to ask user: {str(e)}",
|
|
102
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from .utils import BLUE, RESET
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _get_session_depth(session_id: int) -> int:
|
|
5
|
+
"""Calculate the depth of a session in the subagent hierarchy by traversing parent_session_id."""
|
|
6
|
+
import sqlite3
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
DB_PATH = Path(".raggie/.raggie.chat")
|
|
10
|
+
depth = 0
|
|
11
|
+
current_id = session_id
|
|
12
|
+
visited = set()
|
|
13
|
+
|
|
14
|
+
conn = sqlite3.connect(DB_PATH)
|
|
15
|
+
cursor = conn.cursor()
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
while current_id is not None and current_id not in visited:
|
|
19
|
+
visited.add(current_id)
|
|
20
|
+
cursor.execute("""
|
|
21
|
+
SELECT parent_session_id FROM sessions WHERE id = ?
|
|
22
|
+
""", (current_id,))
|
|
23
|
+
result = cursor.fetchone()
|
|
24
|
+
if result and result[0] is not None:
|
|
25
|
+
current_id = result[0]
|
|
26
|
+
depth += 1
|
|
27
|
+
else:
|
|
28
|
+
break
|
|
29
|
+
finally:
|
|
30
|
+
conn.close()
|
|
31
|
+
|
|
32
|
+
return depth
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _collect_subagent_output(subagent, subagent_session_id, prompt=None, resume=False):
|
|
36
|
+
"""Run a subagent and collect its output. If resume=True, resume dangling work instead of starting fresh."""
|
|
37
|
+
output_parts = []
|
|
38
|
+
error_occurred = False
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
if resume:
|
|
42
|
+
events = subagent.resume_dangling_tool_work()
|
|
43
|
+
else:
|
|
44
|
+
events = subagent.start(prompt=prompt)
|
|
45
|
+
|
|
46
|
+
if events is None:
|
|
47
|
+
events = []
|
|
48
|
+
|
|
49
|
+
for event_type, *event_data in events:
|
|
50
|
+
if event_type == "response":
|
|
51
|
+
output_parts.append(event_data[0])
|
|
52
|
+
elif event_type == "response_end":
|
|
53
|
+
output_parts.append(event_data[0])
|
|
54
|
+
elif event_type == "error":
|
|
55
|
+
output_parts.append(f"Error: {event_data[0]}")
|
|
56
|
+
error_occurred = True
|
|
57
|
+
break
|
|
58
|
+
except Exception as e:
|
|
59
|
+
output_parts.append(f"Subagent execution failed: {str(e)}")
|
|
60
|
+
error_occurred = True
|
|
61
|
+
|
|
62
|
+
output = "\n".join(output_parts) if output_parts else "No output from subagent"
|
|
63
|
+
|
|
64
|
+
if error_occurred:
|
|
65
|
+
output = f"Subagent encountered errors:\n{output}"
|
|
66
|
+
|
|
67
|
+
from Agent.chat_history_db import get_all_changes_by_session_chain
|
|
68
|
+
changes = get_all_changes_by_session_chain(subagent_session_id)
|
|
69
|
+
if changes:
|
|
70
|
+
changes_summary = "\n".join(
|
|
71
|
+
f" - [{c['change_type']}] {c['file_path'] or 'N/A'}: {c['description']}"
|
|
72
|
+
for c in changes
|
|
73
|
+
)
|
|
74
|
+
output = f"{output}\n\n--- Changes made by subagent for you to review ---\n{changes_summary}"
|
|
75
|
+
|
|
76
|
+
return output
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle(arguments, toolcall_id, parent_session_id=None, skip_depth_check=False):
|
|
80
|
+
prompt = arguments.get("prompt")
|
|
81
|
+
|
|
82
|
+
if not prompt:
|
|
83
|
+
return {
|
|
84
|
+
"role": "tool",
|
|
85
|
+
"tool_call_id": toolcall_id,
|
|
86
|
+
"content": "Error: 'prompt' is a required parameter",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if parent_session_id is None:
|
|
90
|
+
return {
|
|
91
|
+
"role": "tool",
|
|
92
|
+
"tool_call_id": toolcall_id,
|
|
93
|
+
"content": "Error: parent_session_id is required to determine the subagent role",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
from Agent.chat_history_db import get_session_role, get_session_effort, get_session_depth
|
|
97
|
+
from Agent.effort_levels import is_depth_allowed, effort_name, effort_max_depth
|
|
98
|
+
role = get_session_role(parent_session_id)
|
|
99
|
+
if not role:
|
|
100
|
+
return {
|
|
101
|
+
"role": "tool",
|
|
102
|
+
"tool_call_id": toolcall_id,
|
|
103
|
+
"content": f"Error: Could not determine role for parent session {parent_session_id}",
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Check depth limit via the effort system
|
|
107
|
+
if not skip_depth_check:
|
|
108
|
+
effort = get_session_effort(parent_session_id)
|
|
109
|
+
depth = get_session_depth(parent_session_id)
|
|
110
|
+
if effort is not None and not is_depth_allowed(effort, depth):
|
|
111
|
+
max_d = effort_max_depth(effort)
|
|
112
|
+
return {
|
|
113
|
+
"role": "tool",
|
|
114
|
+
"tool_call_id": toolcall_id,
|
|
115
|
+
"content": f"Cannot dispatch subagent: effort level '{effort_name(effort)}' limits depth to {max_d}. Current depth is {depth}.",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
from Agent.chat_history_db import (
|
|
120
|
+
get_chat_id_for_session, create_session, get_child_session_by_toolcall,
|
|
121
|
+
is_session_finished, load_messages,
|
|
122
|
+
get_session_effort, get_session_depth,
|
|
123
|
+
resolve_session_id,
|
|
124
|
+
)
|
|
125
|
+
from Agent.agent import Agent
|
|
126
|
+
from Tools import setup_toolcalls
|
|
127
|
+
from Commands import setup_commands
|
|
128
|
+
|
|
129
|
+
chat_id = get_chat_id_for_session(parent_session_id)
|
|
130
|
+
if chat_id is None:
|
|
131
|
+
return {
|
|
132
|
+
"role": "tool",
|
|
133
|
+
"tool_call_id": toolcall_id,
|
|
134
|
+
"content": f"Error: Could not find chat for parent session {parent_session_id}",
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Check for existing child session matching this toolcall_id (from a previous interrupted dispatch)
|
|
138
|
+
child = get_child_session_by_toolcall(parent_session_id, toolcall_id)
|
|
139
|
+
if child is not None:
|
|
140
|
+
child_session_id = child["id"]
|
|
141
|
+
# Follow redirect chain to find the active session (may have been handed over)
|
|
142
|
+
active_session_id = resolve_session_id(child_session_id)
|
|
143
|
+
if is_session_finished(child_session_id):
|
|
144
|
+
# Subagent completed but main agent was interrupted before processing the result
|
|
145
|
+
print(f"{BLUE}Found completed subagent session {active_session_id}, retrieving output...{RESET}")
|
|
146
|
+
messages = load_messages(active_session_id)
|
|
147
|
+
output_parts = []
|
|
148
|
+
for msg in messages:
|
|
149
|
+
if msg.get("role") == "assistant" and not msg.get("tool_calls"):
|
|
150
|
+
output_parts.append(msg.get("content", ""))
|
|
151
|
+
output = "\n".join(output_parts) if output_parts else "No output from subagent"
|
|
152
|
+
|
|
153
|
+
from Agent.chat_history_db import get_all_changes_by_session_chain
|
|
154
|
+
changes = get_all_changes_by_session_chain(active_session_id)
|
|
155
|
+
if changes:
|
|
156
|
+
changes_summary = "\n".join(
|
|
157
|
+
f" - [{c['change_type']}] {c['file_path'] or 'N/A'}: {c['description']}"
|
|
158
|
+
for c in changes
|
|
159
|
+
)
|
|
160
|
+
output = f"{output}\n\n--- Changes made by subagent for you to review ---\n{changes_summary}"
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
"role": "tool",
|
|
164
|
+
"tool_call_id": toolcall_id,
|
|
165
|
+
"content": output,
|
|
166
|
+
"subagent_session_id": active_session_id,
|
|
167
|
+
}
|
|
168
|
+
else:
|
|
169
|
+
# Subagent was interrupted — resume it
|
|
170
|
+
print(f"{BLUE}Resuming interrupted subagent session {active_session_id}...{RESET}")
|
|
171
|
+
subagent = Agent(role=role, chat_id=chat_id, session_id=active_session_id)
|
|
172
|
+
setup_toolcalls(subagent.tool_registry)
|
|
173
|
+
setup_commands(subagent.command_registry)
|
|
174
|
+
|
|
175
|
+
output = _collect_subagent_output(
|
|
176
|
+
subagent, active_session_id, resume=True
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
"role": "tool",
|
|
181
|
+
"tool_call_id": toolcall_id,
|
|
182
|
+
"content": output,
|
|
183
|
+
"subagent_session_id": active_session_id,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# No existing child session — create a new one under the parent's chat
|
|
187
|
+
print(f"{BLUE}Dispatching subagent with role '{role}'{RESET}")
|
|
188
|
+
parent_effort = get_session_effort(parent_session_id)
|
|
189
|
+
parent_depth = get_session_depth(parent_session_id)
|
|
190
|
+
subagent_session_id = create_session(
|
|
191
|
+
chat_id, parent_session_id=parent_session_id, toolcall_id=toolcall_id,
|
|
192
|
+
effort=parent_effort, depth=parent_depth + 1,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
subagent = Agent(role=role, chat_id=chat_id, session_id=subagent_session_id)
|
|
196
|
+
setup_toolcalls(subagent.tool_registry)
|
|
197
|
+
setup_commands(subagent.command_registry)
|
|
198
|
+
|
|
199
|
+
output = _collect_subagent_output(
|
|
200
|
+
subagent, subagent_session_id, prompt=prompt, resume=False
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"role": "tool",
|
|
205
|
+
"tool_call_id": toolcall_id,
|
|
206
|
+
"content": output,
|
|
207
|
+
"subagent_session_id": subagent_session_id,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return {
|
|
212
|
+
"role": "tool",
|
|
213
|
+
"tool_call_id": toolcall_id,
|
|
214
|
+
"content": f"Failed to dispatch subagent: {str(e)}",
|
|
215
|
+
}
|
Tools/document.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from RAG.document import update_symbol_description, get_symbol_description
|
|
2
|
+
from .utils import is_within_cwd, BLUE, RESET
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def handle(arguments, toolcall_id):
|
|
6
|
+
action = arguments.get("action", "update")
|
|
7
|
+
symbol_name = arguments["symbol_name"]
|
|
8
|
+
print(f"{BLUE}Document {symbol_name}{RESET}")
|
|
9
|
+
symbol_type = arguments.get("symbol_type", "function")
|
|
10
|
+
file_path = arguments.get("file_path")
|
|
11
|
+
|
|
12
|
+
# Check if the file is outside the current working directory
|
|
13
|
+
if file_path and not is_within_cwd(file_path):
|
|
14
|
+
return {
|
|
15
|
+
"role": "tool",
|
|
16
|
+
"tool_call_id": toolcall_id,
|
|
17
|
+
"content": "Error: access denied - path is outside the current working directory",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
if action == "read":
|
|
22
|
+
result = get_symbol_description(symbol_name, symbol_type, file_path)
|
|
23
|
+
elif action == "update":
|
|
24
|
+
description = arguments["description"]
|
|
25
|
+
result = update_symbol_description(symbol_name, description, symbol_type, file_path)
|
|
26
|
+
else:
|
|
27
|
+
result = f"Error: Unknown action '{action}'. Must be 'read' or 'update'."
|
|
28
|
+
except Exception as e:
|
|
29
|
+
result = f"Error: {str(e)}"
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
"role": "tool",
|
|
33
|
+
"tool_call_id": toolcall_id,
|
|
34
|
+
"content": result,
|
|
35
|
+
}
|
Tools/edit_symbol.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from RAG.find import find_symbol_location, find_frontend_entity_location
|
|
4
|
+
from .utils import is_ignored_by_gitignore, is_within_cwd, BLUE, RESET, auto_record_change, reindex_after_change
|
|
5
|
+
|
|
6
|
+
# Frontend entity types that can be edited via edit_safety
|
|
7
|
+
_FRONTEND_ENTITY_TYPES = {
|
|
8
|
+
"markup_element", "css_rule", "custom_property",
|
|
9
|
+
"event_binding", "property_binding", "jsx_subtree",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle(arguments, toolcall_id, session_id=None, code_indexer=None):
|
|
14
|
+
symbol_name = arguments["symbol_name"]
|
|
15
|
+
file_path = arguments.get("file_path")
|
|
16
|
+
new_source = arguments.get("new_source")
|
|
17
|
+
entity_type = arguments.get("entity_type")
|
|
18
|
+
entity_id = arguments.get("entity_id")
|
|
19
|
+
|
|
20
|
+
print(f"{BLUE}EditSymbol {symbol_name}{RESET}")
|
|
21
|
+
|
|
22
|
+
if not new_source:
|
|
23
|
+
return {
|
|
24
|
+
"role": "tool",
|
|
25
|
+
"tool_call_id": toolcall_id,
|
|
26
|
+
"content": "Error: 'new_source' cannot be empty.",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Frontend entity edit path
|
|
31
|
+
if entity_type and entity_type in _FRONTEND_ENTITY_TYPES and entity_id is not None:
|
|
32
|
+
return _handle_frontend_edit(
|
|
33
|
+
entity_type, entity_id, new_source, file_path,
|
|
34
|
+
toolcall_id, session_id, code_indexer,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Standard symbol edit path (functions, classes)
|
|
38
|
+
loc = find_symbol_location(symbol_name, file_path)
|
|
39
|
+
if loc is None:
|
|
40
|
+
hint = ""
|
|
41
|
+
if file_path:
|
|
42
|
+
hint = f" in file '{file_path}'"
|
|
43
|
+
return {
|
|
44
|
+
"role": "tool",
|
|
45
|
+
"tool_call_id": toolcall_id,
|
|
46
|
+
"content": (
|
|
47
|
+
f"Error: Symbol '{symbol_name}' not found in code index{hint}. "
|
|
48
|
+
"Make sure the code index is up to date and the symbol name is correct."
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resolved_path = loc["file_path"]
|
|
53
|
+
start_line = loc["start_line"]
|
|
54
|
+
end_line = loc["end_line"]
|
|
55
|
+
old_source = loc["source"]
|
|
56
|
+
|
|
57
|
+
# Security checks
|
|
58
|
+
if not is_within_cwd(resolved_path):
|
|
59
|
+
return {
|
|
60
|
+
"role": "tool",
|
|
61
|
+
"tool_call_id": toolcall_id,
|
|
62
|
+
"content": "Error: access denied - path is outside the current working directory",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if is_ignored_by_gitignore(resolved_path):
|
|
66
|
+
return {
|
|
67
|
+
"role": "tool",
|
|
68
|
+
"tool_call_id": toolcall_id,
|
|
69
|
+
"content": (
|
|
70
|
+
f"Error: File '{resolved_path}' is in .gitignore. "
|
|
71
|
+
"Operations on gitignored files are not allowed."
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if not os.path.exists(resolved_path):
|
|
76
|
+
return {
|
|
77
|
+
"role": "tool",
|
|
78
|
+
"tool_call_id": toolcall_id,
|
|
79
|
+
"content": f"Error: File '{resolved_path}' does not exist.",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
with open(resolved_path, "r", encoding="utf-8") as f:
|
|
83
|
+
content = f.read()
|
|
84
|
+
|
|
85
|
+
lines = content.split("\n")
|
|
86
|
+
|
|
87
|
+
# Replace lines [start_line-1 : end_line] (1-indexed inclusive → 0-indexed slice)
|
|
88
|
+
old_lines = lines[start_line - 1:end_line]
|
|
89
|
+
new_lines = new_source.split("\n")
|
|
90
|
+
|
|
91
|
+
new_file_lines = lines[:start_line - 1] + new_lines + lines[end_line:]
|
|
92
|
+
new_content = "\n".join(new_file_lines)
|
|
93
|
+
|
|
94
|
+
with open(resolved_path, "w", encoding="utf-8") as f:
|
|
95
|
+
f.write(new_content)
|
|
96
|
+
|
|
97
|
+
# Build diff view
|
|
98
|
+
CONTEXT = 5
|
|
99
|
+
ctx_start = max(0, start_line - 1 - CONTEXT)
|
|
100
|
+
ctx_end = min(len(lines), end_line + CONTEXT)
|
|
101
|
+
|
|
102
|
+
diff_lines = [
|
|
103
|
+
f"@@ {resolved_path}:{start_line}-{end_line} ({len(old_lines)} lines "
|
|
104
|
+
f"-> {len(new_lines)} lines) @@",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for i in range(ctx_start, start_line - 1):
|
|
108
|
+
diff_lines.append(f" {lines[i]}")
|
|
109
|
+
|
|
110
|
+
for line in old_lines:
|
|
111
|
+
diff_lines.append(f" - {line}")
|
|
112
|
+
|
|
113
|
+
for line in new_lines:
|
|
114
|
+
diff_lines.append(f" + {line}")
|
|
115
|
+
|
|
116
|
+
for i in range(end_line, ctx_end):
|
|
117
|
+
diff_lines.append(f" {lines[i]}")
|
|
118
|
+
|
|
119
|
+
diff_text = "\n".join(diff_lines)
|
|
120
|
+
|
|
121
|
+
if session_id is not None:
|
|
122
|
+
from Agent.chat_history_db import record_session_file
|
|
123
|
+
record_session_file(session_id, resolved_path, "edit_symbol")
|
|
124
|
+
auto_record_change(
|
|
125
|
+
session_id, resolved_path, "file_edit",
|
|
126
|
+
f"Edited symbol '{symbol_name}' in {resolved_path}: replaced lines {start_line}-{end_line}",
|
|
127
|
+
diff_text,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
reindex_after_change(code_indexer)
|
|
131
|
+
|
|
132
|
+
summary = (
|
|
133
|
+
f"Replaced {loc['kind']} '{symbol_name}' in {resolved_path} "
|
|
134
|
+
f"(lines {start_line}-{end_line} -> {len(new_lines)} lines):\n\n"
|
|
135
|
+
f"{diff_text}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"role": "tool",
|
|
140
|
+
"tool_call_id": toolcall_id,
|
|
141
|
+
"content": summary,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return {
|
|
146
|
+
"role": "tool",
|
|
147
|
+
"tool_call_id": toolcall_id,
|
|
148
|
+
"content": f"Error executing edit_symbol: {str(e)}",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _handle_frontend_edit(entity_type, entity_id, new_source, file_path,
|
|
153
|
+
toolcall_id, session_id, code_indexer):
|
|
154
|
+
"""Handle editing of frontend semantic entities via edit_safety."""
|
|
155
|
+
from pathlib import Path
|
|
156
|
+
from indexing.frontend.edit_safety import validate_edit_range, apply_frontend_edit
|
|
157
|
+
|
|
158
|
+
db_path = Path.cwd() / ".raggie" / ".code_index.raggie"
|
|
159
|
+
if not db_path.exists():
|
|
160
|
+
return {
|
|
161
|
+
"role": "tool",
|
|
162
|
+
"tool_call_id": toolcall_id,
|
|
163
|
+
"content": f"Error: Code index database not found at {db_path}",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
import sqlite3
|
|
167
|
+
conn = sqlite3.connect(str(db_path))
|
|
168
|
+
conn.row_factory = sqlite3.Row
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
# Resolve file_path if not provided
|
|
172
|
+
if not file_path:
|
|
173
|
+
loc = find_frontend_entity_location(entity_type, entity_id)
|
|
174
|
+
if loc is None:
|
|
175
|
+
return {
|
|
176
|
+
"role": "tool",
|
|
177
|
+
"tool_call_id": toolcall_id,
|
|
178
|
+
"content": f"Error: Could not resolve file path for {entity_type}#{entity_id}",
|
|
179
|
+
}
|
|
180
|
+
file_path = loc["file_path"]
|
|
181
|
+
|
|
182
|
+
# Security checks
|
|
183
|
+
resolved = file_path
|
|
184
|
+
if not os.path.isabs(resolved):
|
|
185
|
+
for base in [os.getcwd(), os.path.join(os.getcwd(), "src")]:
|
|
186
|
+
candidate = os.path.join(base, resolved)
|
|
187
|
+
if os.path.exists(candidate):
|
|
188
|
+
resolved = candidate
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
if not is_within_cwd(resolved):
|
|
192
|
+
return {
|
|
193
|
+
"role": "tool",
|
|
194
|
+
"tool_call_id": toolcall_id,
|
|
195
|
+
"content": "Error: access denied - path is outside the current working directory",
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if is_ignored_by_gitignore(resolved):
|
|
199
|
+
return {
|
|
200
|
+
"role": "tool",
|
|
201
|
+
"tool_call_id": toolcall_id,
|
|
202
|
+
"content": (
|
|
203
|
+
f"Error: File '{resolved}' is in .gitignore. "
|
|
204
|
+
"Operations on gitignored files are not allowed."
|
|
205
|
+
),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
# Apply the edit
|
|
209
|
+
result = apply_frontend_edit(conn, entity_type, entity_id, new_source, file_path)
|
|
210
|
+
|
|
211
|
+
if not result.success:
|
|
212
|
+
return {
|
|
213
|
+
"role": "tool",
|
|
214
|
+
"tool_call_id": toolcall_id,
|
|
215
|
+
"content": f"Error: Edit rejected: {result.reason}",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
# Record the change
|
|
219
|
+
if session_id is not None:
|
|
220
|
+
from Agent.chat_history_db import record_session_file
|
|
221
|
+
record_session_file(session_id, result.file_path, "edit_symbol")
|
|
222
|
+
auto_record_change(
|
|
223
|
+
session_id, result.file_path, "file_edit",
|
|
224
|
+
f"Edited {entity_type}#{entity_id} in {result.file_path}",
|
|
225
|
+
result.diff,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Reindex
|
|
229
|
+
reindex_after_change(code_indexer)
|
|
230
|
+
|
|
231
|
+
summary = (
|
|
232
|
+
f"Replaced {entity_type}#{entity_id} in {result.file_path} "
|
|
233
|
+
f"(lines {result.source_range['start_line']}-{result.source_range['end_line']}):\n\n"
|
|
234
|
+
f"{result.diff}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"role": "tool",
|
|
239
|
+
"tool_call_id": toolcall_id,
|
|
240
|
+
"content": summary,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return {
|
|
245
|
+
"role": "tool",
|
|
246
|
+
"tool_call_id": toolcall_id,
|
|
247
|
+
"content": f"Error executing frontend edit: {str(e)}",
|
|
248
|
+
}
|
|
249
|
+
finally:
|
|
250
|
+
conn.close()
|