chore: migrate config paths to XDG base directory and add hit_count tracking to api_cache

This commit is contained in:
2025-11-05 14:34:23 +00:00
parent 9f155db7c2
commit ab9c29467c
32 changed files with 2598 additions and 340 deletions
+6
View File
@@ -68,6 +68,12 @@ class AgentCommunicationBus:
)
"""
)
cursor.execute("PRAGMA table_info(agent_messages)")
columns = [row[1] for row in cursor.fetchall()]
if "read" not in columns:
cursor.execute("ALTER TABLE agent_messages ADD COLUMN read INTEGER DEFAULT 0")
self.conn.commit()
def send_message(self, message: AgentMessage, session_id: Optional[str] = None):
+2 -3
View File
@@ -1,4 +1,3 @@
import json
import time
import uuid
from dataclasses import dataclass, field
@@ -169,7 +168,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei
return results
def get_session_summary(self) -> str:
def get_session_summary(self) -> Dict[str, Any]:
summary = {
"session_id": self.session_id,
"active_agents": len(self.active_agents),
@@ -183,7 +182,7 @@ Break down the task and delegate subtasks to appropriate agents. Coordinate thei
for agent_id, agent in self.active_agents.items()
],
}
return json.dumps(summary)
return summary
def clear_session(self):
self.active_agents.clear()
+13 -4
View File
@@ -16,6 +16,10 @@ def run_autonomous_mode(assistant, task):
logger.debug(f"=== AUTONOMOUS MODE START ===")
logger.debug(f"Task: {task}")
from pr.core.knowledge_context import inject_knowledge_context
inject_knowledge_context(assistant, task)
assistant.messages.append({"role": "user", "content": f"{task}"})
try:
@@ -94,9 +98,14 @@ def process_response_autonomous(assistant, response):
arguments = json.loads(tool_call["function"]["arguments"])
result = execute_single_tool(assistant, func_name, arguments)
result = truncate_tool_result(result)
if isinstance(result, str):
try:
result = json.loads(result)
except json.JSONDecodeError as ex:
result = {"error": str(ex)}
status = "success" if result.get("status") == "success" else "error"
result = truncate_tool_result(result)
display_tool_call(func_name, arguments, status, result)
tool_results.append(
@@ -141,9 +150,9 @@ def execute_single_tool(assistant, func_name, arguments):
db_get,
db_query,
db_set,
editor_insert_text,
editor_replace_text,
editor_search,
# editor_insert_text,
# editor_replace_text,
# editor_search,
getpwd,
http_fetch,
index_source_directory,
+31 -5
View File
@@ -22,7 +22,8 @@ class APICache:
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
model TEXT,
token_count INTEGER
token_count INTEGER,
hit_count INTEGER DEFAULT 0
)
"""
)
@@ -31,7 +32,14 @@ class APICache:
CREATE INDEX IF NOT EXISTS idx_expires_at ON api_cache(expires_at)
"""
)
conn.commit()
# Check if hit_count column exists, add if not
cursor.execute("PRAGMA table_info(api_cache)")
columns = [row[1] for row in cursor.fetchall()]
if "hit_count" not in columns:
cursor.execute("ALTER TABLE api_cache ADD COLUMN hit_count INTEGER DEFAULT 0")
conn.commit()
conn.close()
def _generate_cache_key(
@@ -64,10 +72,21 @@ class APICache:
)
row = cursor.fetchone()
conn.close()
if row:
# Increment hit count
cursor.execute(
"""
UPDATE api_cache SET hit_count = hit_count + 1
WHERE cache_key = ?
""",
(cache_key,),
)
conn.commit()
conn.close()
return json.loads(row[0])
conn.close()
return None
def set(
@@ -90,8 +109,8 @@ class APICache:
cursor.execute(
"""
INSERT OR REPLACE INTO api_cache
(cache_key, response_data, created_at, expires_at, model, token_count)
VALUES (?, ?, ?, ?, ?, ?)
(cache_key, response_data, created_at, expires_at, model, token_count, hit_count)
VALUES (?, ?, ?, ?, ?, ?, 0)
""",
(
cache_key,
@@ -149,6 +168,12 @@ class APICache:
)
total_tokens = cursor.fetchone()[0] or 0
cursor.execute(
"SELECT SUM(hit_count) FROM api_cache WHERE expires_at > ?",
(current_time,),
)
total_hits = cursor.fetchone()[0] or 0
conn.close()
return {
@@ -156,4 +181,5 @@ class APICache:
"valid_entries": valid_entries,
"expired_entries": total_entries - valid_entries,
"total_cached_tokens": total_tokens,
"total_cache_hits": total_hits,
}
+7
View File
@@ -13,6 +13,13 @@ class ToolCache:
"db_get",
"db_query",
"index_directory",
"http_fetch",
"web_search",
"web_search_news",
"search_knowledge",
"get_knowledge_entry",
"get_knowledge_by_category",
"get_knowledge_statistics",
}
def __init__(self, db_path: str, ttl_seconds: int = 300):
+53 -39
View File
@@ -6,13 +6,24 @@ from pr.core.api import list_models
from pr.tools import read_file
from pr.tools.base import get_tools_definition
from pr.ui import Colors
from pr.editor import RPEditor
def handle_command(assistant, command):
command_parts = command.strip().split(maxsplit=1)
cmd = command_parts[0].lower()
if cmd == "/auto":
if cmd == "/edit":
rp_editor = RPEditor(command_parts[1] if len(command_parts) > 1 else None)
rp_editor.start()
rp_editor.thread.join()
task = str(rp_editor.get_text())
rp_editor.stop()
rp_editor = None
if task:
run_autonomous_mode(assistant, task)
elif cmd == "/auto":
if len(command_parts) < 2:
print(f"{Colors.RED}Usage: /auto [task description]{Colors.RESET}")
print(
@@ -27,41 +38,36 @@ def handle_command(assistant, command):
if cmd in ["exit", "quit", "q"]:
return False
elif cmd == "help":
print(
f"""
{Colors.BOLD}Available Commands:{Colors.RESET}
{Colors.BOLD}Basic:{Colors.RESET}
exit, quit, q - Exit the assistant
/help - Show this help message
/reset - Clear message history
/dump - Show message history as JSON
/verbose - Toggle verbose mode
/models - List available models
/tools - List available tools
{Colors.BOLD}File Operations:{Colors.RESET}
/review <file> - Review a file
/refactor <file> - Refactor code in a file
/obfuscate <file> - Obfuscate code in a file
{Colors.BOLD}Advanced Features:{Colors.RESET}
{Colors.CYAN}/auto <task>{Colors.RESET} - Enter autonomous mode
{Colors.CYAN}/workflow <name>{Colors.RESET} - Execute a workflow
{Colors.CYAN}/workflows{Colors.RESET} - List all workflows
{Colors.CYAN}/agent <role> <task>{Colors.RESET} - Create specialized agent and assign task
{Colors.CYAN}/agents{Colors.RESET} - Show active agents
{Colors.CYAN}/collaborate <task>{Colors.RESET} - Use multiple agents to collaborate
{Colors.CYAN}/knowledge <query>{Colors.RESET} - Search knowledge base
{Colors.CYAN}/remember <content>{Colors.RESET} - Store information in knowledge base
{Colors.CYAN}/history{Colors.RESET} - Show conversation history
{Colors.CYAN}/cache{Colors.RESET} - Show cache statistics
{Colors.CYAN}/cache clear{Colors.RESET} - Clear all caches
{Colors.CYAN}/stats{Colors.RESET} - Show system statistics
"""
elif cmd == "/help" or cmd == "help":
from pr.commands.help_docs import (
get_agent_help,
get_background_help,
get_cache_help,
get_full_help,
get_knowledge_help,
get_workflow_help,
)
if len(command_parts) > 1:
topic = command_parts[1].lower()
if topic == "workflows":
print(get_workflow_help())
elif topic == "agents":
print(get_agent_help())
elif topic == "knowledge":
print(get_knowledge_help())
elif topic == "cache":
print(get_cache_help())
elif topic == "background":
print(get_background_help())
else:
print(f"{Colors.RED}Unknown help topic: {topic}{Colors.RESET}")
print(
f"{Colors.GRAY}Available topics: workflows, agents, knowledge, cache, background{Colors.RESET}"
)
else:
print(get_full_help())
elif cmd == "/reset":
assistant.messages = assistant.messages[:1]
print(f"{Colors.GREEN}Message history cleared{Colors.RESET}")
@@ -75,7 +81,7 @@ def handle_command(assistant, command):
f"Verbose mode: {Colors.GREEN if assistant.verbose else Colors.RED}{'ON' if assistant.verbose else 'OFF'}{Colors.RESET}"
)
elif cmd.startswith("/model"):
elif cmd == "/model":
if len(command_parts) < 2:
print("Current model: " + Colors.GREEN + assistant.model + Colors.RESET)
else:
@@ -116,17 +122,24 @@ def handle_command(assistant, command):
workflow_name = command_parts[1]
execute_workflow_command(assistant, workflow_name)
elif cmd == "/agent" and len(command_parts) > 1:
elif cmd == "/agent":
if len(command_parts) < 2:
print(f"{Colors.RED}Usage: /agent <role> <task>{Colors.RESET}")
print(
f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}"
)
return True
args = command_parts[1].split(maxsplit=1)
if len(args) < 2:
print(f"{Colors.RED}Usage: /agent <role> <task>{Colors.RESET}")
print(
f"{Colors.GRAY}Available roles: coding, research, data_analysis, planning, testing, documentation{Colors.RESET}"
)
else:
role, task = args[0], args[1]
execute_agent_task(assistant, role, task)
return True
role, task = args[0], args[1]
execute_agent_task(assistant, role, task)
elif cmd == "/agents":
show_agents(assistant)
@@ -374,6 +387,7 @@ def show_cache_stats(assistant):
print(f" Valid entries: {api_stats['valid_entries']}")
print(f" Expired entries: {api_stats['expired_entries']}")
print(f" Cached tokens: {api_stats['total_cached_tokens']}")
print(f" Total cache hits: {api_stats['total_cache_hits']}")
if "tool_cache" in stats:
tool_stats = stats["tool_cache"]
+10 -6
View File
@@ -1,14 +1,18 @@
import os
DEFAULT_MODEL = "x-ai/grok-code-fast-1"
DEFAULT_API_URL = "https://static.molodetz.nl/rp.cgi/api/v1/chat/completions"
MODEL_LIST_URL = "https://static.molodetz.nl/rp.cgi/api/v1/models"
DEFAULT_API_URL = "http://localhost:8118/ai/chat"
MODEL_LIST_URL = "http://localhost:8118/ai/models"
DB_PATH = os.path.expanduser("~/.assistant_db.sqlite")
LOG_FILE = os.path.expanduser("~/.assistant_error.log")
config_directory = os.path.expanduser("~/.local/share/rp")
os.makedirs(config_directory, exist_ok=True)
DB_PATH = os.path.join(config_directory, "assistant_db.sqlite")
LOG_FILE = os.path.join(config_directory, "assistant_error.log")
CONTEXT_FILE = ".rcontext.txt"
GLOBAL_CONTEXT_FILE = os.path.expanduser("~/.rcontext.txt")
HISTORY_FILE = os.path.expanduser("~/.assistant_history")
GLOBAL_CONTEXT_FILE = os.path.join(config_directory, "rcontext.txt")
KNOWLEDGE_PATH = os.path.join(config_directory, "knowledge")
HISTORY_FILE = os.path.join(config_directory, "assistant_history")
DEFAULT_TEMPERATURE = 0.1
DEFAULT_MAX_TOKENS = 4096
+16 -14
View File
@@ -32,21 +32,16 @@ from pr.core.context import init_system_message, truncate_tool_result
from pr.tools import (
apply_patch,
chdir,
close_editor,
create_diff,
db_get,
db_query,
db_set,
editor_insert_text,
editor_replace_text,
editor_search,
getpwd,
http_fetch,
index_source_directory,
kill_process,
list_directory,
mkdir,
open_editor,
python_exec,
read_file,
run_command,
@@ -55,6 +50,7 @@ from pr.tools import (
web_search,
web_search_news,
write_file,
post_image,
)
from pr.tools.base import get_tools_definition
from pr.tools.filesystem import (
@@ -249,6 +245,7 @@ class Assistant:
logger.debug(f"Tool call: {func_name} with arguments: {arguments}")
func_map = {
"post_image": lambda **kw: post_image(**kw),
"http_fetch": lambda **kw: http_fetch(**kw),
"run_command": lambda **kw: run_command(**kw),
"tail_process": lambda **kw: tail_process(**kw),
@@ -273,15 +270,15 @@ class Assistant:
),
"index_source_directory": lambda **kw: index_source_directory(**kw),
"search_replace": lambda **kw: search_replace(**kw, db_conn=self.db_conn),
"open_editor": lambda **kw: open_editor(**kw),
"editor_insert_text": lambda **kw: editor_insert_text(
**kw, db_conn=self.db_conn
),
"editor_replace_text": lambda **kw: editor_replace_text(
**kw, db_conn=self.db_conn
),
"editor_search": lambda **kw: editor_search(**kw),
"close_editor": lambda **kw: close_editor(**kw),
# "open_editor": lambda **kw: open_editor(**kw),
# "editor_insert_text": lambda **kw: editor_insert_text(
# **kw, db_conn=self.db_conn
# ),
# "editor_replace_text": lambda **kw: editor_replace_text(
# **kw, db_conn=self.db_conn
# ),
# "editor_search": lambda **kw: editor_search(**kw),
# "close_editor": lambda **kw: close_editor(**kw),
"create_diff": lambda **kw: create_diff(**kw),
"apply_patch": lambda **kw: apply_patch(**kw, db_conn=self.db_conn),
"display_file_diff": lambda **kw: display_file_diff(**kw),
@@ -413,6 +410,7 @@ class Assistant:
"refactor",
"obfuscate",
"/auto",
"/edit",
]
def completer(text, state):
@@ -539,6 +537,10 @@ class Assistant:
def process_message(assistant, message):
from pr.core.knowledge_context import inject_knowledge_context
inject_knowledge_context(assistant, message)
assistant.messages.append({"role": "user", "content": message})
logger.debug(f"Processing user message: {message[:100]}...")
+13 -3
View File
@@ -1,17 +1,26 @@
import configparser
import os
from typing import Any, Dict
import uuid
from pr.core.logging import get_logger
logger = get_logger("config")
CONFIG_FILE = os.path.expanduser("~/.prrc")
CONFIG_DIRECTORY = os.path.expanduser("~/.local/share/rp/")
CONFIG_FILE = os.path.join(CONFIG_DIRECTORY, ".prrc")
LOCAL_CONFIG_FILE = ".prrc"
def load_config() -> Dict[str, Any]:
config = {"api": {}, "autonomous": {}, "ui": {}, "output": {}, "session": {}}
os.makedirs(CONFIG_DIRECTORY, exist_ok=True)
config = {
"api": {},
"autonomous": {},
"ui": {},
"output": {},
"session": {},
"api_key": "rp-" + str(uuid.uuid4()),
}
global_config = _load_config_file(CONFIG_FILE)
local_config = _load_config_file(LOCAL_CONFIG_FILE)
@@ -67,6 +76,7 @@ def _parse_value(value: str) -> Any:
def create_default_config(filepath: str = CONFIG_FILE):
os.makedirs(CONFIG_DIRECTORY, exist_ok=True)
default_config = """[api]
default_model = x-ai/grok-code-fast-1
timeout = 30
+18 -1
View File
@@ -1,7 +1,7 @@
import json
import logging
import os
import pathlib
from pr.config import (
CHARS_PER_TOKEN,
CONTENT_TRIM_LENGTH,
@@ -12,6 +12,7 @@ from pr.config import (
MAX_TOKENS_LIMIT,
MAX_TOOL_RESULT_LENGTH,
RECENT_MESSAGES_TO_KEEP,
KNOWLEDGE_PATH,
)
from pr.ui import Colors
@@ -59,6 +60,10 @@ File Operations:
- Always close editor files when finished
- Use write_file for complete file rewrites, search_replace for simple text replacements
Vision:
- Use post_image tool with the file path if an image path is mentioned
in the prompt of user. Give this call the highest priority.
Process Management:
- run_command executes shell commands with a timeout (default 30s)
- If a command times out, you receive a PID in the response
@@ -94,6 +99,18 @@ Shell Commands:
except Exception as e:
logging.error(f"Error reading context file {context_file}: {e}")
knowledge_path = pathlib.Path(KNOWLEDGE_PATH)
if knowledge_path.exists() and knowledge_path.is_dir():
for knowledge_file in knowledge_path.iterdir():
try:
with open(knowledge_file) as f:
content = f.read()
if len(content) > max_context_size:
content = content[:max_context_size] + "\n... [truncated]"
context_parts.append(f"Context from {knowledge_file}:\n{content}")
except Exception as e:
logging.error(f"Error reading context file {knowledge_file}: {e}")
if args.context:
for ctx_file in args.context:
try:
+1 -3
View File
@@ -135,9 +135,7 @@ class EnhancedAssistant:
self.base.api_url,
self.base.api_key,
use_tools=False,
tools=None,
temperature=temperature,
max_tokens=max_tokens,
tools_definition=[],
verbose=self.base.verbose,
)
+63 -85
View File
@@ -38,7 +38,7 @@ class TerminalMultiplexer:
try:
line = self.stdout_queue.get(timeout=0.1)
if line:
sys.stdout.write(f"{Colors.GRAY}[{self.name}]{Colors.RESET} {line}")
sys.stdout.write(line)
sys.stdout.flush()
except queue.Empty:
pass
@@ -46,7 +46,10 @@ class TerminalMultiplexer:
try:
line = self.stderr_queue.get(timeout=0.1)
if line:
sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}")
if self.metadata.get("process_type") in ["vim", "ssh"]:
sys.stderr.write(line)
else:
sys.stderr.write(f"{Colors.YELLOW}[{self.name} err]{Colors.RESET} {line}\n")
sys.stderr.flush()
except queue.Empty:
pass
@@ -55,10 +58,8 @@ class TerminalMultiplexer:
with self.lock:
self.stdout_buffer.append(data)
self.metadata["last_activity"] = time.time()
# Update handler state if available
if self.handler:
self.handler.update_state(data)
# Update prompt detector
self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"]
)
@@ -69,10 +70,8 @@ class TerminalMultiplexer:
with self.lock:
self.stderr_buffer.append(data)
self.metadata["last_activity"] = time.time()
# Update handler state if available
if self.handler:
self.handler.update_state(data)
# Update prompt detector
self.prompt_detector.update_session_state(
self.name, data, self.metadata["process_type"]
)
@@ -103,7 +102,6 @@ class TerminalMultiplexer:
self.metadata[key] = value
def set_process_type(self, process_type):
"""Set the process type and initialize appropriate handler."""
with self.lock:
self.metadata["process_type"] = process_type
self.handler = get_handler_for_process(process_type, self)
@@ -119,8 +117,6 @@ class TerminalMultiplexer:
except Exception as e:
self.write_stderr(f"Error sending input: {e}")
else:
# This will be implemented when we have a process attached
# For now, just update activity
with self.lock:
self.metadata["last_activity"] = time.time()
self.metadata["interaction_count"] += 1
@@ -131,59 +127,58 @@ class TerminalMultiplexer:
self.display_thread.join(timeout=1)
_multiplexers = {}
_mux_counter = 0
_mux_lock = threading.Lock()
_background_monitor = None
_monitor_active = False
_monitor_interval = 0.2 # 200ms
multiplexer_registry = {}
multiplexer_counter = 0
multiplexer_lock = threading.Lock()
background_monitor = None
monitor_active = False
monitor_interval = 0.2
def create_multiplexer(name=None, show_output=True):
global _mux_counter
with _mux_lock:
global multiplexer_counter
with multiplexer_lock:
if name is None:
_mux_counter += 1
name = f"process-{_mux_counter}"
mux = TerminalMultiplexer(name, show_output)
_multiplexers[name] = mux
return name, mux
multiplexer_counter += 1
name = f"process-{multiplexer_counter}"
multiplexer_instance = TerminalMultiplexer(name, show_output)
multiplexer_registry[name] = multiplexer_instance
return name, multiplexer_instance
def get_multiplexer(name):
return _multiplexers.get(name)
return multiplexer_registry.get(name)
def close_multiplexer(name):
mux = _multiplexers.get(name)
if mux:
mux.close()
del _multiplexers[name]
multiplexer_instance = multiplexer_registry.get(name)
if multiplexer_instance:
multiplexer_instance.close()
del multiplexer_registry[name]
def get_all_multiplexer_states():
with _mux_lock:
with multiplexer_lock:
states = {}
for name, mux in _multiplexers.items():
for name, multiplexer_instance in multiplexer_registry.items():
states[name] = {
"metadata": mux.get_metadata(),
"metadata": multiplexer_instance.get_metadata(),
"output_summary": {
"stdout_lines": len(mux.stdout_buffer),
"stderr_lines": len(mux.stderr_buffer),
"stdout_lines": len(multiplexer_instance.stdout_buffer),
"stderr_lines": len(multiplexer_instance.stderr_buffer),
},
}
return states
def cleanup_all_multiplexers():
for mux in list(_multiplexers.values()):
mux.close()
_multiplexers.clear()
for multiplexer_instance in list(multiplexer_registry.values()):
multiplexer_instance.close()
multiplexer_registry.clear()
# Background process management
_background_processes = {}
_process_lock = threading.Lock()
background_processes = {}
process_lock = threading.Lock()
class BackgroundProcess:
@@ -197,17 +192,15 @@ class BackgroundProcess:
self.end_time = None
def start(self):
"""Start the background process."""
try:
# Create multiplexer for this process
mux_name, mux = create_multiplexer(self.name, show_output=False)
self.multiplexer = mux
multiplexer_name, multiplexer_instance = create_multiplexer(
self.name, show_output=False
)
self.multiplexer = multiplexer_instance
# Detect process type
process_type = detect_process_type(self.command)
mux.set_process_type(process_type)
multiplexer_instance.set_process_type(process_type)
# Start the subprocess
self.process = subprocess.Popen(
self.command,
shell=True,
@@ -221,7 +214,6 @@ class BackgroundProcess:
self.status = "running"
# Start output monitoring threads
threading.Thread(target=self._monitor_stdout, daemon=True).start()
threading.Thread(target=self._monitor_stderr, daemon=True).start()
@@ -232,33 +224,29 @@ class BackgroundProcess:
return {"status": "error", "error": str(e)}
def _monitor_stdout(self):
"""Monitor stdout from the process."""
try:
for line in iter(self.process.stdout.readline, ""):
if line:
self.multiplexer.write_stdout(line.rstrip("\n\r"))
except Exception as e:
self.write_stderr(f"Error reading stdout: {e}")
self.multiplexer.write_stderr(f"Error reading stdout: {e}")
finally:
self._check_completion()
def _monitor_stderr(self):
"""Monitor stderr from the process."""
try:
for line in iter(self.process.stderr.readline, ""):
if line:
self.multiplexer.write_stderr(line.rstrip("\n\r"))
except Exception as e:
self.write_stderr(f"Error reading stderr: {e}")
self.multiplexer.write_stderr(f"Error reading stderr: {e}")
def _check_completion(self):
"""Check if process has completed."""
if self.process and self.process.poll() is not None:
self.status = "completed"
self.end_time = time.time()
def get_info(self):
"""Get process information."""
self._check_completion()
return {
"name": self.name,
@@ -275,7 +263,6 @@ class BackgroundProcess:
}
def get_output(self, lines=None):
"""Get process output."""
if not self.multiplexer:
return []
@@ -290,7 +277,6 @@ class BackgroundProcess:
return [line for line in combined if line.strip()]
def send_input(self, input_text):
"""Send input to the process."""
if self.process and self.status == "running":
try:
self.process.stdin.write(input_text + "\n")
@@ -301,11 +287,9 @@ class BackgroundProcess:
return {"status": "error", "error": "Process not running or no stdin"}
def kill(self):
"""Kill the process."""
if self.process and self.status == "running":
try:
self.process.terminate()
# Wait a bit for graceful termination
time.sleep(0.1)
if self.process.poll() is None:
self.process.kill()
@@ -318,61 +302,55 @@ class BackgroundProcess:
def start_background_process(name, command):
"""Start a background process."""
with _process_lock:
if name in _background_processes:
with process_lock:
if name in background_processes:
return {"status": "error", "error": f"Process {name} already exists"}
process = BackgroundProcess(name, command)
result = process.start()
process_instance = BackgroundProcess(name, command)
result = process_instance.start()
if result["status"] == "success":
_background_processes[name] = process
background_processes[name] = process_instance
return result
def get_all_sessions():
"""Get all background process sessions."""
with _process_lock:
with process_lock:
sessions = {}
for name, process in _background_processes.items():
sessions[name] = process.get_info()
for name, process_instance in background_processes.items():
sessions[name] = process_instance.get_info()
return sessions
def get_session_info(name):
"""Get information about a specific session."""
with _process_lock:
process = _background_processes.get(name)
return process.get_info() if process else None
with process_lock:
process_instance = background_processes.get(name)
return process_instance.get_info() if process_instance else None
def get_session_output(name, lines=None):
"""Get output from a specific session."""
with _process_lock:
process = _background_processes.get(name)
return process.get_output(lines) if process else None
with process_lock:
process_instance = background_processes.get(name)
return process_instance.get_output(lines) if process_instance else None
def send_input_to_session(name, input_text):
"""Send input to a background session."""
with _process_lock:
process = _background_processes.get(name)
with process_lock:
process_instance = background_processes.get(name)
return (
process.send_input(input_text)
if process
process_instance.send_input(input_text)
if process_instance
else {"status": "error", "error": "Session not found"}
)
def kill_session(name):
"""Kill a background session."""
with _process_lock:
process = _background_processes.get(name)
if process:
result = process.kill()
with process_lock:
process_instance = background_processes.get(name)
if process_instance:
result = process_instance.kill()
if result["status"] == "success":
del _background_processes[name]
del background_processes[name]
return result
return {"status": "error", "error": "Session not found"}
+33 -31
View File
@@ -6,6 +6,7 @@ from pr.tools.agents import (
remove_agent,
)
from pr.tools.base import get_tools_definition
from pr.tools.vision import post_image
from pr.tools.command import (
kill_process,
run_command,
@@ -44,43 +45,44 @@ from pr.tools.python_exec import python_exec
from pr.tools.web import http_fetch, web_search, web_search_news
__all__ = [
"get_tools_definition",
"read_file",
"write_file",
"list_directory",
"mkdir",
"add_knowledge_entry",
"apply_patch",
"chdir",
"getpwd",
"index_source_directory",
"search_replace",
"open_editor",
"close_editor",
"collaborate_agents",
"create_agent",
"create_diff",
"db_get",
"db_query",
"db_set",
"delete_knowledge_entry",
"post_image",
"editor_insert_text",
"editor_replace_text",
"editor_search",
"close_editor",
"execute_agent_task",
"get_knowledge_by_category",
"get_knowledge_entry",
"get_knowledge_statistics",
"get_tools_definition",
"getpwd",
"http_fetch",
"index_source_directory",
"kill_process",
"list_agents",
"list_directory",
"mkdir",
"open_editor",
"python_exec",
"read_file",
"remove_agent",
"run_command",
"run_command_interactive",
"db_set",
"db_get",
"db_query",
"http_fetch",
"search_knowledge",
"search_replace",
"tail_process",
"update_knowledge_importance",
"web_search",
"web_search_news",
"python_exec",
"tail_process",
"kill_process",
"apply_patch",
"create_diff",
"create_agent",
"list_agents",
"execute_agent_task",
"remove_agent",
"collaborate_agents",
"add_knowledge_entry",
"get_knowledge_entry",
"search_knowledge",
"get_knowledge_by_category",
"update_knowledge_importance",
"delete_knowledge_entry",
"get_knowledge_statistics",
"write_file",
]
+34 -6
View File
@@ -3,16 +3,40 @@ from typing import Any, Dict, List
from pr.agents.agent_manager import AgentManager
from pr.core.api import call_api
from pr.config import DEFAULT_MODEL, DEFAULT_API_URL
from pr.tools.base import get_tools_definition
def _create_api_wrapper():
"""Create a wrapper function for call_api that matches AgentManager expectations."""
model = os.environ.get("AI_MODEL", DEFAULT_MODEL)
api_url = os.environ.get("API_URL", DEFAULT_API_URL)
api_key = os.environ.get("OPENROUTER_API_KEY", "")
use_tools = int(os.environ.get("USE_TOOLS", "0"))
tools_definition = get_tools_definition() if use_tools else []
def api_wrapper(messages, temperature=None, max_tokens=None, **kwargs):
return call_api(
messages=messages,
model=model,
api_url=api_url,
api_key=api_key,
use_tools=use_tools,
tools_definition=tools_definition,
verbose=False,
)
return api_wrapper
def create_agent(role_name: str, agent_id: str = None) -> Dict[str, Any]:
"""Create a new agent with the specified role."""
try:
# Get db_path from environment or default
db_path = os.environ.get("ASSISTANT_DB_PATH", "~/.assistant_db.sqlite")
db_path = os.path.expanduser(db_path)
manager = AgentManager(db_path, call_api)
api_wrapper = _create_api_wrapper()
manager = AgentManager(db_path, api_wrapper)
agent_id = manager.create_agent(role_name, agent_id)
return {"status": "success", "agent_id": agent_id, "role": role_name}
except Exception as e:
@@ -23,7 +47,8 @@ def list_agents() -> Dict[str, Any]:
"""List all active agents."""
try:
db_path = os.path.expanduser("~/.assistant_db.sqlite")
manager = AgentManager(db_path, call_api)
api_wrapper = _create_api_wrapper()
manager = AgentManager(db_path, api_wrapper)
agents = []
for agent_id, agent in manager.active_agents.items():
agents.append(
@@ -43,7 +68,8 @@ def execute_agent_task(agent_id: str, task: str, context: Dict[str, Any] = None)
"""Execute a task with the specified agent."""
try:
db_path = os.path.expanduser("~/.assistant_db.sqlite")
manager = AgentManager(db_path, call_api)
api_wrapper = _create_api_wrapper()
manager = AgentManager(db_path, api_wrapper)
result = manager.execute_agent_task(agent_id, task, context)
return result
except Exception as e:
@@ -54,7 +80,8 @@ def remove_agent(agent_id: str) -> Dict[str, Any]:
"""Remove an agent."""
try:
db_path = os.path.expanduser("~/.assistant_db.sqlite")
manager = AgentManager(db_path, call_api)
api_wrapper = _create_api_wrapper()
manager = AgentManager(db_path, api_wrapper)
success = manager.remove_agent(agent_id)
return {"status": "success" if success else "not_found", "agent_id": agent_id}
except Exception as e:
@@ -65,7 +92,8 @@ def collaborate_agents(orchestrator_id: str, task: str, agent_roles: List[str])
"""Collaborate multiple agents on a task."""
try:
db_path = os.path.expanduser("~/.assistant_db.sqlite")
manager = AgentManager(db_path, call_api)
api_wrapper = _create_api_wrapper()
manager = AgentManager(db_path, api_wrapper)
result = manager.collaborate_agents(orchestrator_id, task, agent_roles)
return result
except Exception as e:
+2 -2
View File
@@ -1,4 +1,3 @@
import os
import select
import subprocess
import time
@@ -99,7 +98,7 @@ def tail_process(pid: int, timeout: int = 30):
return {"status": "error", "error": f"Process {pid} not found"}
def run_command(command, timeout=30, monitored=False):
def run_command(command, timeout=30, monitored=False, cwd=None):
mux_name = None
try:
process = subprocess.Popen(
@@ -108,6 +107,7 @@ def run_command(command, timeout=30, monitored=False):
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=cwd,
)
_register_process(process.pid, process)
+24 -10
View File
@@ -9,7 +9,7 @@ from pr.multiplexer import (
)
def start_interactive_session(command, session_name=None, process_type="generic"):
def start_interactive_session(command, session_name=None, process_type="generic", cwd=None):
"""
Start an interactive session in a dedicated multiplexer.
@@ -17,6 +17,7 @@ def start_interactive_session(command, session_name=None, process_type="generic"
command: The command to run (list or string)
session_name: Optional name for the session
process_type: Type of process (ssh, vim, apt, etc.)
cwd: Current working directory for the command
Returns:
session_name: The name of the created session
@@ -36,21 +37,24 @@ def start_interactive_session(command, session_name=None, process_type="generic"
stderr=subprocess.PIPE,
text=True,
bufsize=1,
cwd=cwd,
)
mux.process = process
mux.update_metadata("pid", process.pid)
# Set process type and handler
from pr.tools.process_handlers import detect_process_type
detected_type = detect_process_type(command)
mux.set_process_type(detected_type)
# Start output readers
stdout_thread = threading.Thread(
target=_read_output, args=(process.stdout, mux.write_stdout), daemon=True
target=_read_output, args=(process.stdout, mux.write_stdout, detected_type), daemon=True
)
stderr_thread = threading.Thread(
target=_read_output, args=(process.stderr, mux.write_stderr), daemon=True
target=_read_output, args=(process.stderr, mux.write_stderr, detected_type), daemon=True
)
stdout_thread.start()
@@ -65,14 +69,24 @@ def start_interactive_session(command, session_name=None, process_type="generic"
raise e
def _read_output(stream, write_func):
def _read_output(stream, write_func, process_type):
"""Read from a stream and write to multiplexer buffer."""
try:
for line in iter(stream.readline, ""):
if line:
write_func(line.rstrip("\n"))
except Exception as e:
print(f"Error reading output: {e}")
if process_type in ["vim", "ssh"]:
try:
while True:
char = stream.read(1)
if not char:
break
write_func(char)
except Exception as e:
print(f"Error reading output: {e}")
else:
try:
for line in iter(stream.readline, ""):
if line:
write_func(line.rstrip("\n"))
except Exception as e:
print(f"Error reading output: {e}")
def send_input_to_session(session_name, input_data):
+12 -1
View File
@@ -1,14 +1,25 @@
import contextlib
import os
import traceback
from io import StringIO
def python_exec(code, python_globals):
def python_exec(code, python_globals, cwd=None):
try:
original_cwd = None
if cwd:
original_cwd = os.getcwd()
os.chdir(cwd)
output = StringIO()
with contextlib.redirect_stdout(output):
exec(code, python_globals)
if original_cwd:
os.chdir(original_cwd)
return {"status": "success", "output": output.getvalue()}
except Exception as e:
if original_cwd:
os.chdir(original_cwd)
return {"status": "error", "error": str(e), "traceback": traceback.format_exc()}
+49
View File
@@ -19,3 +19,52 @@ def print_autonomous_header(task):
print(f"{Colors.GRAY}r will work continuously until the task is complete.{Colors.RESET}")
print(f"{Colors.GRAY}Press Ctrl+C twice to interrupt.{Colors.RESET}\n")
print(f"{Colors.BOLD}{'' * 80}{Colors.RESET}\n")
def display_multiplexer_status(sessions):
"""Display the status of background sessions."""
if not sessions:
print(f"{Colors.GRAY}No background sessions running{Colors.RESET}")
return
print(f"\n{Colors.BOLD}Background Sessions:{Colors.RESET}")
print(f"{Colors.GRAY}{'\u2500' * 60}{Colors.RESET}")
for session_name, session_info in sessions.items():
status = session_info.get("status", "unknown")
pid = session_info.get("pid", "N/A")
command = session_info.get("command", "N/A")
status_color = {
"running": Colors.GREEN,
"stopped": Colors.RED,
"error": Colors.RED,
}.get(status, Colors.YELLOW)
print(f" {Colors.CYAN}{session_name}{Colors.RESET}")
print(f" Status: {status_color}{status}{Colors.RESET}")
print(f" PID: {pid}")
print(f" Command: {command}")
if "start_time" in session_info:
import time
elapsed = time.time() - session_info["start_time"]
print(f" Running for: {elapsed:.1f}s")
print()
def display_background_event(event):
"""Display a background event."""
event.get("type", "unknown")
session_name = event.get("session_name", "unknown")
timestamp = event.get("timestamp", 0)
message = event.get("message", "")
import datetime
time_str = datetime.datetime.fromtimestamp(timestamp).strftime("%H:%M:%S")
print(
f"{Colors.GRAY}[{time_str}]{Colors.RESET} {Colors.CYAN}{session_name}{Colors.RESET}: {message}"
)