feat: add distributed async dataset with unix socket server and refactor agent communication bus
Implement AsyncDataSet class supporting client-server model over Unix sockets with SQLite backend, including KV store, table management, and concurrent query handling. Rename `get_messages` to `receive_messages` in AgentCommunicationBus and update all callers. Remove deprecated `get_recommended_agent` function from agent_roles, `invalidate_tool` from tool_cache, and legacy `receive_messages` wrapper. Add multiplexer command routing in handlers with `/prompt` command support. Introduce comprehensive help documentation system for workflows. Update default API URLs to production endpoints and refactor adaptive context window calculation in AdvancedContextManager.
This commit is contained in:
@@ -43,6 +43,11 @@ from pr.tools.memory import (
|
||||
from pr.tools.patch import apply_patch, create_diff
|
||||
from pr.tools.python_exec import python_exec
|
||||
from pr.tools.web import http_fetch, web_search, web_search_news
|
||||
from pr.tools.context_modifier import (
|
||||
modify_context_add,
|
||||
modify_context_replace,
|
||||
modify_context_delete,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"add_knowledge_entry",
|
||||
|
||||
+143
-595
@@ -1,596 +1,144 @@
|
||||
import inspect
|
||||
from typing import get_type_hints, get_origin, get_args
|
||||
import pr.tools
|
||||
|
||||
|
||||
def _type_to_json_schema(py_type):
|
||||
"""Convert Python type to JSON Schema type."""
|
||||
if py_type == str:
|
||||
return {"type": "string"}
|
||||
elif py_type == int:
|
||||
return {"type": "integer"}
|
||||
elif py_type == float:
|
||||
return {"type": "number"}
|
||||
elif py_type == bool:
|
||||
return {"type": "boolean"}
|
||||
elif get_origin(py_type) == list:
|
||||
return {"type": "array", "items": _type_to_json_schema(get_args(py_type)[0])}
|
||||
elif get_origin(py_type) == dict:
|
||||
return {"type": "object"}
|
||||
else:
|
||||
# Default to string for unknown types
|
||||
return {"type": "string"}
|
||||
|
||||
|
||||
def _generate_tool_schema(func):
|
||||
"""Generate JSON Schema for a tool function."""
|
||||
sig = inspect.signature(func)
|
||||
docstring = func.__doc__ or ""
|
||||
|
||||
# Extract description from docstring
|
||||
description = docstring.strip().split("\n")[0] if docstring else ""
|
||||
|
||||
# Get type hints
|
||||
type_hints = get_type_hints(func)
|
||||
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name in ["db_conn", "python_globals"]: # Skip internal parameters
|
||||
continue
|
||||
|
||||
param_type = type_hints.get(param_name, str)
|
||||
schema = _type_to_json_schema(param_type)
|
||||
|
||||
# Add description from docstring if available
|
||||
param_doc = ""
|
||||
if docstring:
|
||||
lines = docstring.split("\n")
|
||||
in_args = False
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("Args:") or line.startswith("Arguments:"):
|
||||
in_args = True
|
||||
continue
|
||||
elif in_args and line.startswith(param_name + ":"):
|
||||
param_doc = line.split(":", 1)[1].strip()
|
||||
break
|
||||
elif in_args and line == "":
|
||||
continue
|
||||
elif in_args and not line.startswith(" "):
|
||||
break
|
||||
|
||||
if param_doc:
|
||||
schema["description"] = param_doc
|
||||
|
||||
# Set default if available
|
||||
if param.default != inspect.Parameter.empty:
|
||||
schema["default"] = param.default
|
||||
|
||||
properties[param_name] = schema
|
||||
|
||||
# Required if no default
|
||||
if param.default == inspect.Parameter.empty:
|
||||
required.append(param_name)
|
||||
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func.__name__,
|
||||
"description": description,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_tools_definition():
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "kill_process",
|
||||
"description": "Terminate a background process by its PID. Use this to stop processes started with run_command that exceeded their timeout.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "The process ID returned by run_command when status is 'running'.",
|
||||
}
|
||||
},
|
||||
"required": ["pid"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tail_process",
|
||||
"description": "Monitor and retrieve output from a background process by its PID. Use this to check on processes started with run_command that exceeded their timeout.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pid": {
|
||||
"type": "integer",
|
||||
"description": "The process ID returned by run_command when status is 'running'.",
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Maximum seconds to wait for process completion. Returns partial output if still running.",
|
||||
"default": 30,
|
||||
},
|
||||
},
|
||||
"required": ["pid"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "http_fetch",
|
||||
"description": "Fetch content from an HTTP URL",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {"type": "string", "description": "The URL to fetch"},
|
||||
"headers": {
|
||||
"type": "object",
|
||||
"description": "Optional HTTP headers",
|
||||
},
|
||||
},
|
||||
"required": ["url"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command",
|
||||
"description": "Execute a shell command and capture output. Returns immediately after timeout with PID if still running. Use tail_process to monitor or kill_process to terminate long-running commands.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The shell command to execute",
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Maximum seconds to wait for completion",
|
||||
"default": 30,
|
||||
},
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "start_interactive_session",
|
||||
"description": "Execute an interactive terminal command that requires user input or displays UI. The command runs in a dedicated session and returns a session name.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The interactive command to execute (e.g., vim, nano, top)",
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "send_input_to_session",
|
||||
"description": "Send input to an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the session",
|
||||
},
|
||||
"input_data": {
|
||||
"type": "string",
|
||||
"description": "The input to send to the session",
|
||||
},
|
||||
},
|
||||
"required": ["session_name", "input_data"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_session_output",
|
||||
"description": "Read output from an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the session",
|
||||
}
|
||||
},
|
||||
"required": ["session_name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "close_interactive_session",
|
||||
"description": "Close an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {
|
||||
"type": "string",
|
||||
"description": "The name of the session",
|
||||
}
|
||||
},
|
||||
"required": ["session_name"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read contents of a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
}
|
||||
},
|
||||
"required": ["filepath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write",
|
||||
},
|
||||
},
|
||||
"required": ["filepath", "content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_directory",
|
||||
"description": "List directory contents",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory path",
|
||||
"default": ".",
|
||||
},
|
||||
"recursive": {
|
||||
"type": "boolean",
|
||||
"description": "List recursively",
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "mkdir",
|
||||
"description": "Create a new directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Path of the directory to create",
|
||||
}
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "chdir",
|
||||
"description": "Change the current working directory",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string", "description": "Path to change to"}},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "getpwd",
|
||||
"description": "Get the current working directory",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_set",
|
||||
"description": "Set a key-value pair in the database",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {"type": "string", "description": "The key"},
|
||||
"value": {"type": "string", "description": "The value"},
|
||||
},
|
||||
"required": ["key", "value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_get",
|
||||
"description": "Get a value from the database",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"key": {"type": "string", "description": "The key"}},
|
||||
"required": ["key"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "db_query",
|
||||
"description": "Execute a database query",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "SQL query"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search",
|
||||
"description": "Perform a web search",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"query": {"type": "string", "description": "Search query"}},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "web_search_news",
|
||||
"description": "Perform a web search for news",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for news",
|
||||
}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "python_exec",
|
||||
"description": "Execute Python code",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Python code to execute",
|
||||
}
|
||||
},
|
||||
"required": ["code"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "index_source_directory",
|
||||
"description": "Index directory recursively and read all source files.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string", "description": "Path to index"}},
|
||||
"required": ["path"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_replace",
|
||||
"description": "Search and replace text in a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "String to replace",
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "Replacement string",
|
||||
},
|
||||
},
|
||||
"required": ["filepath", "old_string", "new_string"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "apply_patch",
|
||||
"description": "Apply a patch to a file, especially for source code",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to patch",
|
||||
},
|
||||
"patch_content": {
|
||||
"type": "string",
|
||||
"description": "The patch content as a string",
|
||||
},
|
||||
},
|
||||
"required": ["filepath", "patch_content"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "create_diff",
|
||||
"description": "Create a unified diff between two files",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file1": {
|
||||
"type": "string",
|
||||
"description": "Path to the first file",
|
||||
},
|
||||
"file2": {
|
||||
"type": "string",
|
||||
"description": "Path to the second file",
|
||||
},
|
||||
"fromfile": {
|
||||
"type": "string",
|
||||
"description": "Label for the first file",
|
||||
"default": "file1",
|
||||
},
|
||||
"tofile": {
|
||||
"type": "string",
|
||||
"description": "Label for the second file",
|
||||
"default": "file2",
|
||||
},
|
||||
},
|
||||
"required": ["file1", "file2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "open_editor",
|
||||
"description": "Open the RPEditor for a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
}
|
||||
},
|
||||
"required": ["filepath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "close_editor",
|
||||
"description": "Close the RPEditor. Always close files when finished editing.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
}
|
||||
},
|
||||
"required": ["filepath"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_insert_text",
|
||||
"description": "Insert text at cursor position in the editor",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
},
|
||||
"text": {"type": "string", "description": "Text to insert"},
|
||||
"line": {
|
||||
"type": "integer",
|
||||
"description": "Line number (optional)",
|
||||
},
|
||||
"col": {
|
||||
"type": "integer",
|
||||
"description": "Column number (optional)",
|
||||
},
|
||||
},
|
||||
"required": ["filepath", "text"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_replace_text",
|
||||
"description": "Replace text in a range",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
},
|
||||
"start_line": {"type": "integer", "description": "Start line"},
|
||||
"start_col": {"type": "integer", "description": "Start column"},
|
||||
"end_line": {"type": "integer", "description": "End line"},
|
||||
"end_col": {"type": "integer", "description": "End column"},
|
||||
"new_text": {"type": "string", "description": "New text"},
|
||||
},
|
||||
"required": [
|
||||
"filepath",
|
||||
"start_line",
|
||||
"start_col",
|
||||
"end_line",
|
||||
"end_col",
|
||||
"new_text",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "editor_search",
|
||||
"description": "Search for a pattern in the file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath": {
|
||||
"type": "string",
|
||||
"description": "Path to the file",
|
||||
},
|
||||
"pattern": {"type": "string", "description": "Regex pattern"},
|
||||
"start_line": {
|
||||
"type": "integer",
|
||||
"description": "Start line",
|
||||
"default": 0,
|
||||
},
|
||||
},
|
||||
"required": ["filepath", "pattern"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_file_diff",
|
||||
"description": "Display a visual colored diff between two files with syntax highlighting and statistics",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filepath1": {
|
||||
"type": "string",
|
||||
"description": "Path to the original file",
|
||||
},
|
||||
"filepath2": {
|
||||
"type": "string",
|
||||
"description": "Path to the modified file",
|
||||
},
|
||||
"format_type": {
|
||||
"type": "string",
|
||||
"description": "Display format: 'unified' or 'side-by-side'",
|
||||
"default": "unified",
|
||||
},
|
||||
},
|
||||
"required": ["filepath1", "filepath2"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_edit_summary",
|
||||
"description": "Display a summary of all edit operations performed during the session",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "display_edit_timeline",
|
||||
"description": "Display a timeline of all edit operations with details",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"show_content": {
|
||||
"type": "boolean",
|
||||
"description": "Show content previews",
|
||||
"default": False,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "clear_edit_tracker",
|
||||
"description": "Clear the edit tracker to start fresh",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
},
|
||||
]
|
||||
"""Dynamically generate tool definitions from all tool functions."""
|
||||
tools = []
|
||||
|
||||
# Get all functions from pr.tools modules
|
||||
for name in dir(pr.tools):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
|
||||
obj = getattr(pr.tools, name)
|
||||
if callable(obj) and hasattr(obj, "__module__") and obj.__module__.startswith("pr.tools."):
|
||||
# Check if it's a tool function (has docstring and proper signature)
|
||||
if obj.__doc__:
|
||||
try:
|
||||
schema = _generate_tool_schema(obj)
|
||||
tools.append(schema)
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not generate schema for {name}: {e}")
|
||||
continue
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
def get_func_map(db_conn=None, python_globals=None):
|
||||
"""Dynamically generate function map for tool execution."""
|
||||
func_map = {}
|
||||
|
||||
# Include all functions from __all__ in pr.tools
|
||||
for name in getattr(pr.tools, "__all__", []):
|
||||
if name.startswith("_"):
|
||||
continue
|
||||
|
||||
obj = getattr(pr.tools, name, None)
|
||||
if callable(obj) and hasattr(obj, "__module__") and obj.__module__.startswith("pr.tools."):
|
||||
sig = inspect.signature(obj)
|
||||
params = list(sig.parameters.keys())
|
||||
|
||||
# Create wrapper based on parameters
|
||||
if "db_conn" in params and "python_globals" in params:
|
||||
func_map[name] = (
|
||||
lambda func=obj, db_conn=db_conn, python_globals=python_globals, **kw: func(
|
||||
**kw, db_conn=db_conn, python_globals=python_globals
|
||||
)
|
||||
)
|
||||
elif "db_conn" in params:
|
||||
func_map[name] = lambda func=obj, db_conn=db_conn, **kw: func(**kw, db_conn=db_conn)
|
||||
elif "python_globals" in params:
|
||||
func_map[name] = lambda func=obj, python_globals=python_globals, **kw: func(
|
||||
**kw, python_globals=python_globals
|
||||
)
|
||||
else:
|
||||
func_map[name] = lambda func=obj, **kw: func(**kw)
|
||||
|
||||
return func_map
|
||||
|
||||
+13
-1
@@ -2,7 +2,6 @@ import select
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from pr.multiplexer import close_multiplexer, create_multiplexer, get_multiplexer
|
||||
|
||||
_processes = {}
|
||||
|
||||
@@ -99,6 +98,19 @@ def tail_process(pid: int, timeout: int = 30):
|
||||
|
||||
|
||||
def run_command(command, timeout=30, monitored=False, cwd=None):
|
||||
"""Execute a shell command and return the output.
|
||||
|
||||
Args:
|
||||
command: The shell command to execute.
|
||||
timeout: Maximum time in seconds to wait for completion.
|
||||
monitored: Whether to monitor the process (unused).
|
||||
cwd: Working directory for the command.
|
||||
|
||||
Returns:
|
||||
Dict with status, stdout, stderr, returncode, and optionally pid if still running.
|
||||
"""
|
||||
from pr.multiplexer import close_multiplexer, create_multiplexer
|
||||
|
||||
mux_name = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
print(f"Executing command: {command}") print(f"Killing process: {pid}")import os
|
||||
import select
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from pr.multiplexer import close_multiplexer, create_multiplexer, get_multiplexer
|
||||
|
||||
_processes = {}
|
||||
|
||||
|
||||
def _register_process(pid: int, process):
|
||||
_processes[pid] = process
|
||||
return _processes
|
||||
|
||||
|
||||
def _get_process(pid: int):
|
||||
return _processes.get(pid)
|
||||
|
||||
|
||||
def kill_process(pid: int):
|
||||
try:
|
||||
process = _get_process(pid)
|
||||
if process:
|
||||
process.kill()
|
||||
_processes.pop(pid)
|
||||
|
||||
mux_name = f"cmd-{pid}"
|
||||
if get_multiplexer(mux_name):
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {"status": "success", "message": f"Process {pid} has been killed"}
|
||||
else:
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def tail_process(pid: int, timeout: int = 30):
|
||||
process = _get_process(pid)
|
||||
if process:
|
||||
mux_name = f"cmd-{pid}"
|
||||
mux = get_multiplexer(mux_name)
|
||||
|
||||
if not mux:
|
||||
mux_name, mux = create_multiplexer(mux_name, show_output=True)
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
timeout_duration = timeout
|
||||
stdout_content = ""
|
||||
stderr_content = ""
|
||||
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
remaining_stdout, remaining_stderr = process.communicate()
|
||||
if remaining_stdout:
|
||||
mux.write_stdout(remaining_stdout)
|
||||
stdout_content += remaining_stdout
|
||||
if remaining_stderr:
|
||||
mux.write_stderr(remaining_stderr)
|
||||
stderr_content += remaining_stderr
|
||||
|
||||
if pid in _processes:
|
||||
_processes.pop(pid)
|
||||
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"stdout": stdout_content,
|
||||
"stderr": stderr_content,
|
||||
"returncode": process.returncode,
|
||||
}
|
||||
|
||||
if time.time() - start_time > timeout_duration:
|
||||
return {
|
||||
"status": "running",
|
||||
"message": "Process is still running. Call tail_process again to continue monitoring.",
|
||||
"stdout_so_far": stdout_content,
|
||||
"stderr_so_far": stderr_content,
|
||||
"pid": pid,
|
||||
}
|
||||
|
||||
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
||||
for pipe in ready:
|
||||
if pipe == process.stdout:
|
||||
line = process.stdout.readline()
|
||||
if line:
|
||||
mux.write_stdout(line)
|
||||
stdout_content += line
|
||||
elif pipe == process.stderr:
|
||||
line = process.stderr.readline()
|
||||
if line:
|
||||
mux.write_stderr(line)
|
||||
stderr_content += line
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
else:
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
|
||||
|
||||
def run_command(command, timeout=30, monitored=False):
|
||||
mux_name = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
_register_process(process.pid, process)
|
||||
|
||||
mux_name, mux = create_multiplexer(f"cmd-{process.pid}", show_output=True)
|
||||
|
||||
start_time = time.time()
|
||||
timeout_duration = timeout
|
||||
stdout_content = ""
|
||||
stderr_content = ""
|
||||
|
||||
while True:
|
||||
if process.poll() is not None:
|
||||
remaining_stdout, remaining_stderr = process.communicate()
|
||||
if remaining_stdout:
|
||||
mux.write_stdout(remaining_stdout)
|
||||
stdout_content += remaining_stdout
|
||||
if remaining_stderr:
|
||||
mux.write_stderr(remaining_stderr)
|
||||
stderr_content += remaining_stderr
|
||||
|
||||
if process.pid in _processes:
|
||||
_processes.pop(process.pid)
|
||||
|
||||
close_multiplexer(mux_name)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"stdout": stdout_content,
|
||||
"stderr": stderr_content,
|
||||
"returncode": process.returncode,
|
||||
}
|
||||
|
||||
if time.time() - start_time > timeout_duration:
|
||||
return {
|
||||
"status": "running",
|
||||
"message": f"Process still running after {timeout}s timeout. Use tail_process({process.pid}) to monitor or kill_process({process.pid}) to terminate.",
|
||||
"stdout_so_far": stdout_content,
|
||||
"stderr_so_far": stderr_content,
|
||||
"pid": process.pid,
|
||||
"mux_name": mux_name,
|
||||
}
|
||||
|
||||
ready, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
||||
for pipe in ready:
|
||||
if pipe == process.stdout:
|
||||
line = process.stdout.readline()
|
||||
if line:
|
||||
mux.write_stdout(line)
|
||||
stdout_content += line
|
||||
elif pipe == process.stderr:
|
||||
line = process.stderr.readline()
|
||||
if line:
|
||||
mux.write_stderr(line)
|
||||
stderr_content += line
|
||||
except Exception as e:
|
||||
if mux_name:
|
||||
close_multiplexer(mux_name)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def run_command_interactive(command):
|
||||
try:
|
||||
return_code = os.system(command)
|
||||
return {"status": "success", "returncode": return_code}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,65 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
CONTEXT_FILE = '/home/retoor/.local/share/rp/.rcontext.txt'
|
||||
|
||||
def _read_context() -> str:
|
||||
if not os.path.exists(CONTEXT_FILE):
|
||||
raise FileNotFoundError(f"Context file {CONTEXT_FILE} not found.")
|
||||
with open(CONTEXT_FILE, 'r') as f:
|
||||
return f.read()
|
||||
|
||||
def _write_context(content: str):
|
||||
with open(CONTEXT_FILE, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
def modify_context_add(new_content: str, position: Optional[str] = None) -> str:
|
||||
"""
|
||||
Add new content to the .rcontext.txt file.
|
||||
|
||||
Args:
|
||||
new_content: The content to add.
|
||||
position: Optional marker to insert before (e.g., '***').
|
||||
"""
|
||||
current = _read_context()
|
||||
if position and position in current:
|
||||
# Insert before the position
|
||||
parts = current.split(position, 1)
|
||||
updated = parts[0] + new_content + '\n\n' + position + parts[1]
|
||||
else:
|
||||
# Append at the end
|
||||
updated = current + '\n\n' + new_content
|
||||
_write_context(updated)
|
||||
return f"Added: {new_content[:100]}... (full addition applied). Consequences: Enhances functionality as requested."
|
||||
|
||||
def modify_context_replace(old_content: str, new_content: str) -> str:
|
||||
"""
|
||||
Replace old content with new content in .rcontext.txt.
|
||||
|
||||
Args:
|
||||
old_content: The content to replace.
|
||||
new_content: The replacement content.
|
||||
"""
|
||||
current = _read_context()
|
||||
if old_content not in current:
|
||||
raise ValueError(f"Old content not found: {old_content[:50]}...")
|
||||
updated = current.replace(old_content, new_content, 1) # Replace first occurrence
|
||||
_write_context(updated)
|
||||
return f"Replaced: '{old_content[:50]}...' with '{new_content[:50]}...'. Consequences: Changes behavior as specified; verify for unintended effects."
|
||||
|
||||
def modify_context_delete(content_to_delete: str, confirmed: bool = False) -> str:
|
||||
"""
|
||||
Delete content from .rcontext.txt, but only if confirmed.
|
||||
|
||||
Args:
|
||||
content_to_delete: The content to delete.
|
||||
confirmed: Must be True to proceed with deletion.
|
||||
"""
|
||||
if not confirmed:
|
||||
raise PermissionError(f"Deletion not confirmed. To delete '{content_to_delete[:50]}...', you must explicitly confirm. Are you sure? This may affect system behavior permanently.")
|
||||
current = _read_context()
|
||||
if content_to_delete not in current:
|
||||
raise ValueError(f"Content to delete not found: {content_to_delete[:50]}...")
|
||||
updated = current.replace(content_to_delete, '', 1)
|
||||
_write_context(updated)
|
||||
return f"Deleted: '{content_to_delete[:50]}...'. Consequences: Removed specified content; system may lose referenced rules or guidelines."
|
||||
@@ -2,6 +2,16 @@ import time
|
||||
|
||||
|
||||
def db_set(key, value, db_conn):
|
||||
"""Set a key-value pair in the database.
|
||||
|
||||
Args:
|
||||
key: The key to set.
|
||||
value: The value to store.
|
||||
db_conn: Database connection.
|
||||
|
||||
Returns:
|
||||
Dict with status and message.
|
||||
"""
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
@@ -19,6 +29,15 @@ def db_set(key, value, db_conn):
|
||||
|
||||
|
||||
def db_get(key, db_conn):
|
||||
"""Get a value from the database.
|
||||
|
||||
Args:
|
||||
key: The key to retrieve.
|
||||
db_conn: Database connection.
|
||||
|
||||
Returns:
|
||||
Dict with status and value.
|
||||
"""
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
@@ -35,6 +54,15 @@ def db_get(key, db_conn):
|
||||
|
||||
|
||||
def db_query(query, db_conn):
|
||||
"""Execute a database query.
|
||||
|
||||
Args:
|
||||
query: SQL query to execute.
|
||||
db_conn: Database connection.
|
||||
|
||||
Returns:
|
||||
Dict with status and query results.
|
||||
"""
|
||||
if not db_conn:
|
||||
return {"status": "error", "error": "Database not initialized"}
|
||||
|
||||
|
||||
@@ -0,0 +1,838 @@
|
||||
import sys
|
||||
import os
|
||||
import ast
|
||||
import inspect
|
||||
import time
|
||||
import threading
|
||||
import gc
|
||||
import weakref
|
||||
import linecache
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class MemoryTracker:
|
||||
def __init__(self):
|
||||
self.allocations = defaultdict(list)
|
||||
self.references = weakref.WeakValueDictionary()
|
||||
self.peak_memory = 0
|
||||
self.current_memory = 0
|
||||
|
||||
def track_object(self, obj, location):
|
||||
try:
|
||||
obj_id = id(obj)
|
||||
obj_size = sys.getsizeof(obj)
|
||||
self.allocations[location].append(
|
||||
{
|
||||
"id": obj_id,
|
||||
"type": type(obj).__name__,
|
||||
"size": obj_size,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
)
|
||||
self.current_memory += obj_size
|
||||
if self.current_memory > self.peak_memory:
|
||||
self.peak_memory = self.current_memory
|
||||
except:
|
||||
pass
|
||||
|
||||
def analyze_leaks(self):
|
||||
gc.collect()
|
||||
leaks = []
|
||||
for obj in gc.get_objects():
|
||||
if sys.getrefcount(obj) > 10:
|
||||
try:
|
||||
leaks.append(
|
||||
{
|
||||
"type": type(obj).__name__,
|
||||
"refcount": sys.getrefcount(obj),
|
||||
"size": sys.getsizeof(obj),
|
||||
}
|
||||
)
|
||||
except:
|
||||
pass
|
||||
return sorted(leaks, key=lambda x: x["refcount"], reverse=True)[:20]
|
||||
|
||||
def get_report(self):
|
||||
return {
|
||||
"peak_memory": self.peak_memory,
|
||||
"current_memory": self.current_memory,
|
||||
"allocation_count": sum(len(v) for v in self.allocations.values()),
|
||||
"leaks": self.analyze_leaks(),
|
||||
}
|
||||
|
||||
|
||||
class PerformanceProfiler:
|
||||
def __init__(self):
|
||||
self.function_times = defaultdict(lambda: {"calls": 0, "total_time": 0.0, "self_time": 0.0})
|
||||
self.call_stack = []
|
||||
self.start_times = {}
|
||||
|
||||
def enter_function(self, frame):
|
||||
func_name = self._get_function_name(frame)
|
||||
self.call_stack.append(func_name)
|
||||
self.start_times[id(frame)] = time.perf_counter()
|
||||
|
||||
def exit_function(self, frame):
|
||||
func_name = self._get_function_name(frame)
|
||||
frame_id = id(frame)
|
||||
|
||||
if frame_id in self.start_times:
|
||||
elapsed = time.perf_counter() - self.start_times[frame_id]
|
||||
self.function_times[func_name]["calls"] += 1
|
||||
self.function_times[func_name]["total_time"] += elapsed
|
||||
self.function_times[func_name]["self_time"] += elapsed
|
||||
del self.start_times[frame_id]
|
||||
|
||||
if self.call_stack:
|
||||
self.call_stack.pop()
|
||||
|
||||
def _get_function_name(self, frame):
|
||||
return f"{frame.f_code.co_filename}:{frame.f_code.co_name}:{frame.f_lineno}"
|
||||
|
||||
def get_hotspots(self, limit=20):
|
||||
sorted_funcs = sorted(
|
||||
self.function_times.items(), key=lambda x: x[1]["total_time"], reverse=True
|
||||
)
|
||||
return sorted_funcs[:limit]
|
||||
|
||||
|
||||
class StaticAnalyzer(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
self.issues = []
|
||||
self.complexity = 0
|
||||
self.unused_vars = set()
|
||||
self.undefined_vars = set()
|
||||
self.defined_vars = set()
|
||||
self.used_vars = set()
|
||||
self.functions = {}
|
||||
self.classes = {}
|
||||
self.imports = []
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
self.functions[node.name] = {
|
||||
"lineno": node.lineno,
|
||||
"args": [arg.arg for arg in node.args.args],
|
||||
"decorators": [
|
||||
d.id if isinstance(d, ast.Name) else "complex" for d in node.decorator_list
|
||||
],
|
||||
"complexity": self._calculate_complexity(node),
|
||||
}
|
||||
|
||||
if len(node.body) == 0:
|
||||
self.issues.append(f"Line {node.lineno}: Empty function '{node.name}'")
|
||||
|
||||
if len(node.args.args) > 7:
|
||||
self.issues.append(
|
||||
f"Line {node.lineno}: Function '{node.name}' has too many parameters ({len(node.args.args)})"
|
||||
)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
self.classes[node.name] = {
|
||||
"lineno": node.lineno,
|
||||
"bases": [b.id if isinstance(b, ast.Name) else "complex" for b in node.bases],
|
||||
"methods": [],
|
||||
}
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Import(self, node):
|
||||
for alias in node.names:
|
||||
self.imports.append(alias.name)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
if node.module:
|
||||
self.imports.append(node.module)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_Name(self, node):
|
||||
if isinstance(node.ctx, ast.Store):
|
||||
self.defined_vars.add(node.id)
|
||||
elif isinstance(node.ctx, ast.Load):
|
||||
self.used_vars.add(node.id)
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_If(self, node):
|
||||
self.complexity += 1
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_For(self, node):
|
||||
self.complexity += 1
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_While(self, node):
|
||||
self.complexity += 1
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ExceptHandler(self, node):
|
||||
self.complexity += 1
|
||||
if node.type is None:
|
||||
self.issues.append(f"Line {node.lineno}: Bare except clause (catches all exceptions)")
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_BinOp(self, node):
|
||||
if isinstance(node.op, ast.Add):
|
||||
if isinstance(node.left, ast.Str) or isinstance(node.right, ast.Str):
|
||||
self.issues.append(
|
||||
f"Line {node.lineno}: String concatenation with '+' (use f-strings or join)"
|
||||
)
|
||||
self.generic_visit(node)
|
||||
|
||||
def _calculate_complexity(self, node):
|
||||
complexity = 1
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, (ast.If, ast.For, ast.While, ast.ExceptHandler)):
|
||||
complexity += 1
|
||||
return complexity
|
||||
|
||||
def finalize(self):
|
||||
self.unused_vars = self.defined_vars - self.used_vars
|
||||
self.undefined_vars = self.used_vars - self.defined_vars - set(dir(__builtins__))
|
||||
|
||||
for var in self.unused_vars:
|
||||
self.issues.append(f"Unused variable: '{var}'")
|
||||
|
||||
def analyze_code(self, source_code):
|
||||
try:
|
||||
tree = ast.parse(source_code)
|
||||
self.visit(tree)
|
||||
self.finalize()
|
||||
return {
|
||||
"issues": self.issues,
|
||||
"complexity": self.complexity,
|
||||
"functions": self.functions,
|
||||
"classes": self.classes,
|
||||
"imports": self.imports,
|
||||
"unused_vars": list(self.unused_vars),
|
||||
}
|
||||
except SyntaxError as e:
|
||||
return {"error": f"Syntax error at line {e.lineno}: {e.msg}"}
|
||||
|
||||
|
||||
class DynamicTracer:
|
||||
def __init__(self):
|
||||
self.execution_trace = []
|
||||
self.exception_trace = []
|
||||
self.variable_changes = defaultdict(list)
|
||||
self.line_coverage = set()
|
||||
self.function_calls = defaultdict(int)
|
||||
self.max_trace_length = 10000
|
||||
self.memory_tracker = MemoryTracker()
|
||||
self.profiler = PerformanceProfiler()
|
||||
self.trace_active = False
|
||||
|
||||
def trace_calls(self, frame, event, arg):
|
||||
if not self.trace_active:
|
||||
return
|
||||
|
||||
if len(self.execution_trace) >= self.max_trace_length:
|
||||
return self.trace_calls
|
||||
|
||||
co = frame.f_code
|
||||
func_name = co.co_name
|
||||
filename = co.co_filename
|
||||
line_no = frame.f_lineno
|
||||
|
||||
if "site-packages" in filename or filename.startswith("<"):
|
||||
return self.trace_calls
|
||||
|
||||
trace_entry = {
|
||||
"event": event,
|
||||
"function": func_name,
|
||||
"filename": filename,
|
||||
"lineno": line_no,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
if event == "call":
|
||||
self.function_calls[f"{filename}:{func_name}"] += 1
|
||||
self.profiler.enter_function(frame)
|
||||
trace_entry["locals"] = {
|
||||
k: repr(v)[:100] for k, v in frame.f_locals.items() if not k.startswith("__")
|
||||
}
|
||||
|
||||
elif event == "return":
|
||||
self.profiler.exit_function(frame)
|
||||
trace_entry["return_value"] = repr(arg)[:100] if arg else None
|
||||
|
||||
elif event == "line":
|
||||
self.line_coverage.add((filename, line_no))
|
||||
line_code = linecache.getline(filename, line_no).strip()
|
||||
trace_entry["code"] = line_code
|
||||
|
||||
for var, value in frame.f_locals.items():
|
||||
if not var.startswith("__"):
|
||||
self.variable_changes[var].append(
|
||||
{"line": line_no, "value": repr(value)[:100], "timestamp": time.time()}
|
||||
)
|
||||
self.memory_tracker.track_object(value, f"{filename}:{line_no}")
|
||||
|
||||
elif event == "exception":
|
||||
exc_type, exc_value, exc_tb = arg
|
||||
self.exception_trace.append(
|
||||
{
|
||||
"type": exc_type.__name__,
|
||||
"message": str(exc_value),
|
||||
"filename": filename,
|
||||
"function": func_name,
|
||||
"lineno": line_no,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
)
|
||||
trace_entry["exception"] = {"type": exc_type.__name__, "message": str(exc_value)}
|
||||
|
||||
self.execution_trace.append(trace_entry)
|
||||
return self.trace_calls
|
||||
|
||||
def start_tracing(self):
|
||||
self.trace_active = True
|
||||
sys.settrace(self.trace_calls)
|
||||
threading.settrace(self.trace_calls)
|
||||
|
||||
def stop_tracing(self):
|
||||
self.trace_active = False
|
||||
sys.settrace(None)
|
||||
threading.settrace(None)
|
||||
|
||||
def get_trace_report(self):
|
||||
return {
|
||||
"execution_trace": self.execution_trace[-100:],
|
||||
"exception_trace": self.exception_trace,
|
||||
"line_coverage": list(self.line_coverage),
|
||||
"function_calls": dict(self.function_calls),
|
||||
"variable_changes": {k: v[-10:] for k, v in self.variable_changes.items()},
|
||||
"hotspots": self.profiler.get_hotspots(20),
|
||||
"memory_report": self.memory_tracker.get_report(),
|
||||
}
|
||||
|
||||
|
||||
class GitBisectAutomator:
|
||||
def __init__(self, repo_path="."):
|
||||
self.repo_path = repo_path
|
||||
|
||||
def is_git_repo(self):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--git-dir"],
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except:
|
||||
return False
|
||||
|
||||
def get_commit_history(self, limit=50):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "log", f"--max-count={limit}", "--oneline"],
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
commits = []
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
parts = line.split(" ", 1)
|
||||
if len(parts) == 2:
|
||||
commits.append({"hash": parts[0], "message": parts[1]})
|
||||
return commits
|
||||
except:
|
||||
pass
|
||||
return []
|
||||
|
||||
def blame_file(self, filepath):
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "blame", "-L", "1,50", filepath],
|
||||
cwd=self.repo_path,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class LogAnalyzer:
|
||||
def __init__(self):
|
||||
self.log_patterns = {
|
||||
"error": re.compile(r"error|exception|fail|critical", re.IGNORECASE),
|
||||
"warning": re.compile(r"warn|caution", re.IGNORECASE),
|
||||
"debug": re.compile(r"debug|trace", re.IGNORECASE),
|
||||
"timestamp": re.compile(r"\\d{4}-\\d{2}-\\d{2}[\\s_T]\\d{2}:\\d{2}:\\d{2}"),
|
||||
}
|
||||
self.anomalies = []
|
||||
|
||||
def analyze_logs(self, log_content):
|
||||
lines = log_content.split("\n")
|
||||
errors = []
|
||||
warnings = []
|
||||
timestamps = []
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if self.log_patterns["error"].search(line):
|
||||
errors.append({"line": i + 1, "content": line})
|
||||
elif self.log_patterns["warning"].search(line):
|
||||
warnings.append({"line": i + 1, "content": line})
|
||||
|
||||
ts_match = self.log_patterns["timestamp"].search(line)
|
||||
if ts_match:
|
||||
timestamps.append(ts_match.group())
|
||||
|
||||
return {
|
||||
"total_lines": len(lines),
|
||||
"errors": errors[:50],
|
||||
"warnings": warnings[:50],
|
||||
"error_count": len(errors),
|
||||
"warning_count": len(warnings),
|
||||
"timestamps": timestamps[:20],
|
||||
}
|
||||
|
||||
|
||||
class ExceptionAnalyzer:
|
||||
def __init__(self):
|
||||
self.exceptions = []
|
||||
self.exception_counts = defaultdict(int)
|
||||
|
||||
def capture_exception(self, exc_type, exc_value, exc_traceback):
|
||||
exc_info = {
|
||||
"type": exc_type.__name__,
|
||||
"message": str(exc_value),
|
||||
"timestamp": time.time(),
|
||||
"traceback": [],
|
||||
}
|
||||
|
||||
tb = exc_traceback
|
||||
while tb is not None:
|
||||
frame = tb.tb_frame
|
||||
exc_info["traceback"].append(
|
||||
{
|
||||
"filename": frame.f_code.co_filename,
|
||||
"function": frame.f_code.co_name,
|
||||
"lineno": tb.tb_lineno,
|
||||
"locals": {
|
||||
k: repr(v)[:100]
|
||||
for k, v in frame.f_locals.items()
|
||||
if not k.startswith("__")
|
||||
},
|
||||
}
|
||||
)
|
||||
tb = tb.tb_next
|
||||
|
||||
self.exceptions.append(exc_info)
|
||||
self.exception_counts[exc_type.__name__] += 1
|
||||
return exc_info
|
||||
|
||||
def get_report(self):
|
||||
return {
|
||||
"total_exceptions": len(self.exceptions),
|
||||
"exception_types": dict(self.exception_counts),
|
||||
"recent_exceptions": self.exceptions[-10:],
|
||||
}
|
||||
|
||||
|
||||
class TestGenerator:
|
||||
def __init__(self):
|
||||
self.test_cases = []
|
||||
|
||||
def generate_tests_for_function(self, func_name, func_signature):
|
||||
test_template = f"""def test_{func_name}_basic():
|
||||
result = {func_name}()
|
||||
assert result is not None
|
||||
|
||||
def test_{func_name}_edge_cases():
|
||||
pass
|
||||
|
||||
def test_{func_name}_exceptions():
|
||||
pass
|
||||
"""
|
||||
return test_template
|
||||
|
||||
def analyze_function_for_tests(self, func_obj):
|
||||
sig = inspect.signature(func_obj)
|
||||
test_inputs = []
|
||||
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param.annotation != inspect.Parameter.empty:
|
||||
param_type = param.annotation
|
||||
if param_type == int:
|
||||
test_inputs.append([0, 1, -1, 100])
|
||||
elif param_type == str:
|
||||
test_inputs.append(["", "test", "a" * 100])
|
||||
elif param_type == list:
|
||||
test_inputs.append([[], [1], [1, 2, 3]])
|
||||
else:
|
||||
test_inputs.append([None])
|
||||
else:
|
||||
test_inputs.append([None, 0, "", []])
|
||||
|
||||
return test_inputs
|
||||
|
||||
|
||||
class CodeFlowVisualizer:
|
||||
def __init__(self):
|
||||
self.flow_graph = defaultdict(list)
|
||||
self.call_hierarchy = defaultdict(set)
|
||||
|
||||
def build_flow_from_trace(self, execution_trace):
|
||||
for i in range(len(execution_trace) - 1):
|
||||
current = execution_trace[i]
|
||||
next_step = execution_trace[i + 1]
|
||||
|
||||
current_node = f"{current['function']}:{current['lineno']}"
|
||||
next_node = f"{next_step['function']}:{next_step['lineno']}"
|
||||
|
||||
self.flow_graph[current_node].append(next_node)
|
||||
|
||||
if current["event"] == "call":
|
||||
caller = current["function"]
|
||||
callee = next_step["function"]
|
||||
self.call_hierarchy[caller].add(callee)
|
||||
|
||||
def generate_text_visualization(self):
|
||||
output = []
|
||||
output.append("Call Hierarchy:")
|
||||
for caller, callees in sorted(self.call_hierarchy.items()):
|
||||
output.append(f" {caller}")
|
||||
for callee in sorted(callees):
|
||||
output.append(f" -> {callee}")
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
class AutomatedDebugger:
|
||||
def __init__(self):
|
||||
self.static_analyzer = StaticAnalyzer()
|
||||
self.dynamic_tracer = DynamicTracer()
|
||||
self.exception_analyzer = ExceptionAnalyzer()
|
||||
self.log_analyzer = LogAnalyzer()
|
||||
self.git_automator = GitBisectAutomator()
|
||||
self.test_generator = TestGenerator()
|
||||
self.flow_visualizer = CodeFlowVisualizer()
|
||||
self.original_excepthook = sys.excepthook
|
||||
|
||||
def analyze_source_file(self, filepath):
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
source_code = f.read()
|
||||
return self.static_analyzer.analyze_code(source_code)
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def run_with_tracing(self, target_func, *args, **kwargs):
|
||||
self.dynamic_tracer.start_tracing()
|
||||
result = None
|
||||
exception = None
|
||||
|
||||
try:
|
||||
result = target_func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
exception = self.exception_analyzer.capture_exception(type(e), e, e.__traceback__)
|
||||
finally:
|
||||
self.dynamic_tracer.stop_tracing()
|
||||
|
||||
self.flow_visualizer.build_flow_from_trace(self.dynamic_tracer.execution_trace)
|
||||
|
||||
return {
|
||||
"result": result,
|
||||
"exception": exception,
|
||||
"trace": self.dynamic_tracer.get_trace_report(),
|
||||
"flow": self.flow_visualizer.generate_text_visualization(),
|
||||
}
|
||||
|
||||
def analyze_logs(self, log_file_or_content):
|
||||
if os.path.isfile(log_file_or_content):
|
||||
with open(log_file_or_content, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
else:
|
||||
content = log_file_or_content
|
||||
|
||||
return self.log_analyzer.analyze_logs(content)
|
||||
|
||||
def generate_debug_report(self, output_file="debug_report.json"):
|
||||
report = {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"static_analysis": self.static_analyzer.issues,
|
||||
"dynamic_trace": self.dynamic_tracer.get_trace_report(),
|
||||
"exceptions": self.exception_analyzer.get_report(),
|
||||
"git_info": (
|
||||
self.git_automator.get_commit_history(10)
|
||||
if self.git_automator.is_git_repo()
|
||||
else None
|
||||
),
|
||||
"flow_visualization": self.flow_visualizer.generate_text_visualization(),
|
||||
}
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(report, f, indent=2, default=str)
|
||||
|
||||
return report
|
||||
|
||||
def auto_debug_function(self, func, test_inputs=None):
|
||||
results = []
|
||||
|
||||
if test_inputs is None:
|
||||
test_inputs = self.test_generator.analyze_function_for_tests(func)
|
||||
|
||||
for input_set in test_inputs:
|
||||
try:
|
||||
if isinstance(input_set, list):
|
||||
result = self.run_with_tracing(func, *input_set)
|
||||
else:
|
||||
result = self.run_with_tracing(func, input_set)
|
||||
results.append(
|
||||
{
|
||||
"input": input_set,
|
||||
"success": result["exception"] is None,
|
||||
"output": result["result"],
|
||||
"trace_summary": {
|
||||
"function_calls": len(result["trace"]["function_calls"]),
|
||||
"exceptions": len(result["trace"]["exception_trace"]),
|
||||
},
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
results.append({"input": input_set, "success": False, "error": str(e)})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Global instances for tools
|
||||
_memory_tracker = MemoryTracker()
|
||||
_performance_profiler = PerformanceProfiler()
|
||||
_static_analyzer = StaticAnalyzer()
|
||||
_dynamic_tracer = DynamicTracer()
|
||||
_git_automator = GitBisectAutomator()
|
||||
_log_analyzer = LogAnalyzer()
|
||||
_exception_analyzer = ExceptionAnalyzer()
|
||||
_test_generator = TestGenerator()
|
||||
_code_flow_visualizer = CodeFlowVisualizer()
|
||||
_automated_debugger = AutomatedDebugger()
|
||||
|
||||
|
||||
# Tool functions
|
||||
def track_memory_allocation(location: str = "manual") -> dict:
|
||||
"""Track current memory allocation at a specific location."""
|
||||
try:
|
||||
_memory_tracker.track_object({}, location)
|
||||
return {
|
||||
"status": "success",
|
||||
"current_memory": _memory_tracker.current_memory,
|
||||
"peak_memory": _memory_tracker.peak_memory,
|
||||
"location": location,
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def analyze_memory_leaks() -> dict:
|
||||
"""Analyze potential memory leaks in the current process."""
|
||||
try:
|
||||
leaks = _memory_tracker.analyze_leaks()
|
||||
return {"status": "success", "leaks_found": len(leaks), "top_leaks": leaks[:10]}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def get_memory_report() -> dict:
|
||||
"""Get a comprehensive memory usage report."""
|
||||
try:
|
||||
return {"status": "success", "report": _memory_tracker.get_report()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def start_performance_profiling() -> dict:
|
||||
"""Start performance profiling."""
|
||||
try:
|
||||
PerformanceProfiler()
|
||||
return {"status": "success", "message": "Performance profiling started"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def stop_performance_profiling() -> dict:
|
||||
"""Stop performance profiling and get hotspots."""
|
||||
try:
|
||||
hotspots = _performance_profiler.get_hotspots(20)
|
||||
return {"status": "success", "hotspots": hotspots}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def analyze_source_code(source_code: str) -> dict:
|
||||
"""Perform static analysis on Python source code."""
|
||||
try:
|
||||
analyzer = StaticAnalyzer()
|
||||
result = analyzer.analyze_code(source_code)
|
||||
return {"status": "success", "analysis": result}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def analyze_source_file(filepath: str) -> dict:
|
||||
"""Analyze a Python source file statically."""
|
||||
try:
|
||||
result = _automated_debugger.analyze_source_file(filepath)
|
||||
return {"status": "success", "filepath": filepath, "analysis": result}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def start_dynamic_tracing() -> dict:
|
||||
"""Start dynamic execution tracing."""
|
||||
try:
|
||||
_dynamic_tracer.start_tracing()
|
||||
return {"status": "success", "message": "Dynamic tracing started"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def stop_dynamic_tracing() -> dict:
|
||||
"""Stop dynamic tracing and get trace report."""
|
||||
try:
|
||||
_dynamic_tracer.stop_tracing()
|
||||
report = _dynamic_tracer.get_trace_report()
|
||||
return {"status": "success", "trace_report": report}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def get_git_commit_history(limit: int = 50) -> dict:
|
||||
"""Get recent git commit history."""
|
||||
try:
|
||||
commits = _git_automator.get_commit_history(limit)
|
||||
return {
|
||||
"status": "success",
|
||||
"commits": commits,
|
||||
"is_git_repo": _git_automator.is_git_repo(),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def blame_file(filepath: str) -> dict:
|
||||
"""Get git blame information for a file."""
|
||||
try:
|
||||
blame_output = _git_automator.blame_file(filepath)
|
||||
return {"status": "success", "filepath": filepath, "blame": blame_output}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def analyze_log_content(log_content: str) -> dict:
|
||||
"""Analyze log content for errors, warnings, and patterns."""
|
||||
try:
|
||||
analysis = _log_analyzer.analyze_logs(log_content)
|
||||
return {"status": "success", "analysis": analysis}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def analyze_log_file(filepath: str) -> dict:
|
||||
"""Analyze a log file."""
|
||||
try:
|
||||
analysis = _automated_debugger.analyze_logs(filepath)
|
||||
return {"status": "success", "filepath": filepath, "analysis": analysis}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def get_exception_report() -> dict:
|
||||
"""Get a report of captured exceptions."""
|
||||
try:
|
||||
report = _exception_analyzer.get_report()
|
||||
return {"status": "success", "exception_report": report}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def generate_tests_for_function(func_name: str, func_signature: str = "") -> dict:
|
||||
"""Generate test templates for a function."""
|
||||
try:
|
||||
test_code = _test_generator.generate_tests_for_function(func_name, func_signature)
|
||||
return {"status": "success", "func_name": func_name, "test_code": test_code}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def visualize_code_flow_from_trace(execution_trace) -> dict:
|
||||
"""Visualize code flow from execution trace."""
|
||||
try:
|
||||
visualizer = CodeFlowVisualizer()
|
||||
visualizer.build_flow_from_trace(execution_trace)
|
||||
visualization = visualizer.generate_text_visualization()
|
||||
return {"status": "success", "flow_visualization": visualization}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def run_function_with_debugging(func_code: str, *args, **kwargs) -> dict:
|
||||
"""Execute a function with full debugging."""
|
||||
try:
|
||||
# Compile and execute the function
|
||||
local_vars = {}
|
||||
exec(func_code, globals(), local_vars)
|
||||
|
||||
# Find the function (assuming it's the last defined function)
|
||||
func = None
|
||||
for name, obj in local_vars.items():
|
||||
if callable(obj) and not name.startswith("_"):
|
||||
func = obj
|
||||
break
|
||||
|
||||
if func is None:
|
||||
return {"status": "error", "error": "No function found in code"}
|
||||
|
||||
result = _automated_debugger.run_with_tracing(func, *args, **kwargs)
|
||||
return {"status": "success", "debug_result": result}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def generate_comprehensive_debug_report(output_file: str = "debug_report.json") -> dict:
|
||||
"""Generate a comprehensive debug report."""
|
||||
try:
|
||||
report = _automated_debugger.generate_debug_report(output_file)
|
||||
return {
|
||||
"status": "success",
|
||||
"output_file": output_file,
|
||||
"report_summary": {
|
||||
"static_issues": len(report.get("static_analysis", [])),
|
||||
"exceptions": report.get("exceptions", {}).get("total_exceptions", 0),
|
||||
"function_calls": len(report.get("dynamic_trace", {}).get("function_calls", {})),
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def auto_debug_function(func_code: str, test_inputs: list = None) -> dict:
|
||||
"""Automatically debug a function with test inputs."""
|
||||
try:
|
||||
local_vars = {}
|
||||
exec(func_code, globals(), local_vars)
|
||||
|
||||
func = None
|
||||
for name, obj in local_vars.items():
|
||||
if callable(obj) and not name.startswith("_"):
|
||||
func = obj
|
||||
break
|
||||
|
||||
if func is None:
|
||||
return {"status": "error", "error": "No function found in code"}
|
||||
|
||||
if test_inputs is None:
|
||||
test_inputs = _test_generator.analyze_function_for_tests(func)
|
||||
|
||||
results = _automated_debugger.auto_debug_function(func, test_inputs)
|
||||
return {"status": "success", "debug_results": results}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
+11
-1
@@ -2,7 +2,7 @@ import os
|
||||
import os.path
|
||||
|
||||
from pr.editor import RPEditor
|
||||
from pr.multiplexer import close_multiplexer, create_multiplexer, get_multiplexer
|
||||
|
||||
|
||||
from ..tools.patch import display_content_diff
|
||||
from ..ui.edit_feedback import track_edit, tracker
|
||||
@@ -17,6 +17,8 @@ def get_editor(filepath):
|
||||
|
||||
|
||||
def close_editor(filepath):
|
||||
from pr.multiplexer import close_multiplexer, get_multiplexer
|
||||
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = get_editor(path)
|
||||
@@ -34,6 +36,8 @@ def close_editor(filepath):
|
||||
|
||||
|
||||
def open_editor(filepath):
|
||||
from pr.multiplexer import create_multiplexer
|
||||
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = RPEditor(path)
|
||||
@@ -53,6 +57,8 @@ def open_editor(filepath):
|
||||
|
||||
|
||||
def editor_insert_text(filepath, text, line=None, col=None, show_diff=True):
|
||||
from pr.multiplexer import get_multiplexer
|
||||
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
|
||||
@@ -98,6 +104,8 @@ def editor_insert_text(filepath, text, line=None, col=None, show_diff=True):
|
||||
def editor_replace_text(
|
||||
filepath, start_line, start_col, end_line, end_col, new_text, show_diff=True
|
||||
):
|
||||
from pr.multiplexer import get_multiplexer
|
||||
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
|
||||
@@ -148,6 +156,8 @@ def editor_replace_text(
|
||||
|
||||
|
||||
def editor_search(filepath, pattern, start_line=0):
|
||||
from pr.multiplexer import get_multiplexer
|
||||
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
editor = RPEditor(path)
|
||||
|
||||
+61
-7
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import os
|
||||
import time
|
||||
from typing import Optional, Any
|
||||
|
||||
from pr.editor import RPEditor
|
||||
|
||||
@@ -17,7 +18,17 @@ def get_uid():
|
||||
return _id
|
||||
|
||||
|
||||
def read_file(filepath, db_conn=None):
|
||||
def read_file(filepath: str, db_conn: Optional[Any] = None) -> dict:
|
||||
"""
|
||||
Read the contents of a file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to read
|
||||
db_conn: Optional database connection for tracking
|
||||
|
||||
Returns:
|
||||
dict: Status and content or error
|
||||
"""
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
with open(path) as f:
|
||||
@@ -31,7 +42,22 @@ def read_file(filepath, db_conn=None):
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def write_file(filepath, content, db_conn=None, show_diff=True):
|
||||
def write_file(
|
||||
filepath: str, content: str, db_conn: Optional[Any] = None, show_diff: bool = True
|
||||
) -> dict:
|
||||
"""
|
||||
Write content to a file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to write
|
||||
content: Content to write
|
||||
db_conn: Optional database connection for tracking
|
||||
show_diff: Whether to show diff of changes
|
||||
|
||||
Returns:
|
||||
dict: Status and message or error
|
||||
"""
|
||||
operation = None
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
old_content = ""
|
||||
@@ -93,12 +119,13 @@ def write_file(filepath, content, db_conn=None, show_diff=True):
|
||||
|
||||
return {"status": "success", "message": message}
|
||||
except Exception as e:
|
||||
if "operation" in locals():
|
||||
if operation is not None:
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def list_directory(path=".", recursive=False):
|
||||
"""List files and directories in the specified path."""
|
||||
try:
|
||||
path = os.path.expanduser(path)
|
||||
items = []
|
||||
@@ -139,6 +166,7 @@ def mkdir(path):
|
||||
|
||||
|
||||
def chdir(path):
|
||||
"""Change the current working directory."""
|
||||
try:
|
||||
os.chdir(os.path.expanduser(path))
|
||||
return {"status": "success", "new_path": os.getcwd()}
|
||||
@@ -147,13 +175,23 @@ def chdir(path):
|
||||
|
||||
|
||||
def getpwd():
|
||||
"""Get the current working directory."""
|
||||
try:
|
||||
return {"status": "success", "path": os.getcwd()}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def index_source_directory(path):
|
||||
def index_source_directory(path: str) -> dict:
|
||||
"""
|
||||
Index directory recursively and read all source files.
|
||||
|
||||
Args:
|
||||
path: Path to index
|
||||
|
||||
Returns:
|
||||
dict: Status and indexed files or error
|
||||
"""
|
||||
extensions = [
|
||||
".py",
|
||||
".js",
|
||||
@@ -189,7 +227,21 @@ def index_source_directory(path):
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def search_replace(filepath, old_string, new_string, db_conn=None):
|
||||
def search_replace(
|
||||
filepath: str, old_string: str, new_string: str, db_conn: Optional[Any] = None
|
||||
) -> dict:
|
||||
"""
|
||||
Search and replace text in a file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file
|
||||
old_string: String to replace
|
||||
new_string: Replacement string
|
||||
db_conn: Optional database connection for tracking
|
||||
|
||||
Returns:
|
||||
dict: Status and message or error
|
||||
"""
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if not os.path.exists(path):
|
||||
@@ -246,6 +298,7 @@ def open_editor(filepath):
|
||||
|
||||
|
||||
def editor_insert_text(filepath, text, line=None, col=None, show_diff=True, db_conn=None):
|
||||
operation = None
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
@@ -283,7 +336,7 @@ def editor_insert_text(filepath, text, line=None, col=None, show_diff=True, db_c
|
||||
tracker.mark_completed(operation)
|
||||
return {"status": "success", "message": f"Inserted text in {path}"}
|
||||
except Exception as e:
|
||||
if "operation" in locals():
|
||||
if operation is not None:
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
@@ -299,6 +352,7 @@ def editor_replace_text(
|
||||
db_conn=None,
|
||||
):
|
||||
try:
|
||||
operation = None
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
from pr.tools.database import db_get
|
||||
@@ -341,7 +395,7 @@ def editor_replace_text(
|
||||
tracker.mark_completed(operation)
|
||||
return {"status": "success", "message": f"Replaced text in {path}"}
|
||||
except Exception as e:
|
||||
if "operation" in locals():
|
||||
if operation is not None:
|
||||
tracker.mark_failed(operation)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import subprocess
|
||||
import threading
|
||||
import importlib
|
||||
|
||||
from pr.multiplexer import (
|
||||
close_multiplexer,
|
||||
create_multiplexer,
|
||||
get_all_multiplexer_states,
|
||||
get_multiplexer,
|
||||
)
|
||||
|
||||
def _get_multiplexer_functions():
|
||||
"""Lazy import multiplexer functions to avoid circular imports."""
|
||||
multiplexer = importlib.import_module("pr.multiplexer")
|
||||
return {
|
||||
"create_multiplexer": multiplexer.create_multiplexer,
|
||||
"get_multiplexer": multiplexer.get_multiplexer,
|
||||
"close_multiplexer": multiplexer.close_multiplexer,
|
||||
"get_all_multiplexer_states": multiplexer.get_all_multiplexer_states,
|
||||
}
|
||||
|
||||
|
||||
def start_interactive_session(command, session_name=None, process_type="generic", cwd=None):
|
||||
@@ -22,7 +27,8 @@ def start_interactive_session(command, session_name=None, process_type="generic"
|
||||
Returns:
|
||||
session_name: The name of the created session
|
||||
"""
|
||||
name, mux = create_multiplexer(session_name)
|
||||
funcs = _get_multiplexer_functions()
|
||||
name, mux = funcs["create_multiplexer"](session_name)
|
||||
mux.update_metadata("process_type", process_type)
|
||||
|
||||
# Start the process
|
||||
@@ -65,7 +71,7 @@ def start_interactive_session(command, session_name=None, process_type="generic"
|
||||
|
||||
return name
|
||||
except Exception as e:
|
||||
close_multiplexer(name)
|
||||
funcs["close_multiplexer"](name)
|
||||
raise e
|
||||
|
||||
|
||||
@@ -97,7 +103,8 @@ def send_input_to_session(session_name, input_data):
|
||||
session_name: Name of the session
|
||||
input_data: Input string to send
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
funcs = _get_multiplexer_functions()
|
||||
mux = funcs["get_multiplexer"](session_name)
|
||||
if not mux:
|
||||
raise ValueError(f"Session {session_name} not found")
|
||||
|
||||
@@ -112,6 +119,7 @@ def send_input_to_session(session_name, input_data):
|
||||
|
||||
|
||||
def read_session_output(session_name, lines=None):
|
||||
funcs = _get_multiplexer_functions()
|
||||
"""
|
||||
Read output from a session.
|
||||
|
||||
@@ -122,7 +130,7 @@ def read_session_output(session_name, lines=None):
|
||||
Returns:
|
||||
dict: {'stdout': str, 'stderr': str}
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
mux = funcs["get_multiplexer"](session_name)
|
||||
if not mux:
|
||||
raise ValueError(f"Session {session_name} not found")
|
||||
|
||||
@@ -142,7 +150,8 @@ def list_active_sessions():
|
||||
Returns:
|
||||
dict: Session states
|
||||
"""
|
||||
return get_all_multiplexer_states()
|
||||
funcs = _get_multiplexer_functions()
|
||||
return funcs["get_all_multiplexer_states"]()
|
||||
|
||||
|
||||
def get_session_status(session_name):
|
||||
@@ -155,7 +164,8 @@ def get_session_status(session_name):
|
||||
Returns:
|
||||
dict: Session metadata and status
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
funcs = _get_multiplexer_functions()
|
||||
mux = funcs["get_multiplexer"](session_name)
|
||||
if not mux:
|
||||
return None
|
||||
|
||||
@@ -175,10 +185,11 @@ def close_interactive_session(session_name):
|
||||
Close an interactive session.
|
||||
"""
|
||||
try:
|
||||
mux = get_multiplexer(session_name)
|
||||
funcs = _get_multiplexer_functions()
|
||||
mux = funcs["get_multiplexer"](session_name)
|
||||
if mux:
|
||||
mux.process.kill()
|
||||
close_multiplexer(session_name)
|
||||
funcs["close_multiplexer"](session_name)
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
@@ -7,6 +7,16 @@ from ..ui.diff_display import display_diff, get_diff_stats
|
||||
|
||||
|
||||
def apply_patch(filepath, patch_content, db_conn=None):
|
||||
"""Apply a patch to a file.
|
||||
|
||||
Args:
|
||||
filepath: Path to the file to patch.
|
||||
patch_content: The patch content as a string.
|
||||
db_conn: Database connection (optional).
|
||||
|
||||
Returns:
|
||||
Dict with status and output.
|
||||
"""
|
||||
try:
|
||||
path = os.path.expanduser(filepath)
|
||||
if db_conn:
|
||||
@@ -41,6 +51,19 @@ def apply_patch(filepath, patch_content, db_conn=None):
|
||||
def create_diff(
|
||||
file1, file2, fromfile="file1", tofile="file2", visual=False, format_type="unified"
|
||||
):
|
||||
"""Create a unified diff between two files.
|
||||
|
||||
Args:
|
||||
file1: Path to the first file.
|
||||
file2: Path to the second file.
|
||||
fromfile: Label for the first file.
|
||||
tofile: Label for the second file.
|
||||
visual: Whether to include visual diff.
|
||||
format_type: Diff format type.
|
||||
|
||||
Returns:
|
||||
Dict with status and diff content.
|
||||
"""
|
||||
try:
|
||||
path1 = os.path.expanduser(file1)
|
||||
path2 = os.path.expanduser(file2)
|
||||
|
||||
@@ -5,6 +5,16 @@ from io import StringIO
|
||||
|
||||
|
||||
def python_exec(code, python_globals, cwd=None):
|
||||
"""Execute Python code and capture the output.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute.
|
||||
python_globals: Dictionary of global variables for execution.
|
||||
cwd: Working directory for execution.
|
||||
|
||||
Returns:
|
||||
Dict with status and output, or error information.
|
||||
"""
|
||||
try:
|
||||
original_cwd = None
|
||||
if cwd:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
from pr.vision import post_image as vision_post_image
|
||||
import functools
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def post_image(path: str, prompt: str = None):
|
||||
"""Post an image for analysis.
|
||||
|
||||
Args:
|
||||
path: Path to the image file.
|
||||
prompt: Optional prompt for analysis.
|
||||
|
||||
Returns:
|
||||
Analysis result.
|
||||
"""
|
||||
try:
|
||||
return vision_post_image(path=path, prompt=prompt)
|
||||
except Exception:
|
||||
raise
|
||||
@@ -5,6 +5,15 @@ import urllib.request
|
||||
|
||||
|
||||
def http_fetch(url, headers=None):
|
||||
"""Fetch content from an HTTP URL.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch.
|
||||
headers: Optional HTTP headers.
|
||||
|
||||
Returns:
|
||||
Dict with status and content.
|
||||
"""
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
if headers:
|
||||
@@ -30,10 +39,26 @@ def _perform_search(base_url, query, params=None):
|
||||
|
||||
|
||||
def web_search(query):
|
||||
"""Perform a web search.
|
||||
|
||||
Args:
|
||||
query: Search query.
|
||||
|
||||
Returns:
|
||||
Dict with status and search results.
|
||||
"""
|
||||
base_url = "https://search.molodetz.nl/search"
|
||||
return _perform_search(base_url, query)
|
||||
|
||||
|
||||
def web_search_news(query):
|
||||
"""Perform a web search for news.
|
||||
|
||||
Args:
|
||||
query: Search query for news.
|
||||
|
||||
Returns:
|
||||
Dict with status and news search results.
|
||||
"""
|
||||
base_url = "https://search.molodetz.nl/search"
|
||||
return _perform_search(base_url, query)
|
||||
|
||||
Reference in New Issue
Block a user