@@ -145,6 +145,44 @@ class Agent:
|
|||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
return self.config.name
|
return self.config.name
|
||||||
|
|
||||||
|
def is_alive(self) -> bool:
|
||||||
|
"""Check if the agent process is still running."""
|
||||||
|
# First check if we have a subprocess object
|
||||||
|
if self.process is not None:
|
||||||
|
return self.process.poll() is None
|
||||||
|
|
||||||
|
# If no process object, search for cagent process with our config file
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
work_dir = Path(self.config.working_directory)
|
||||||
|
agent_config_path = work_dir / f"agent_{self.id}.yaml"
|
||||||
|
|
||||||
|
# Search for cagent processes
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||||
|
try:
|
||||||
|
if proc.info['name'] == 'cagent' and proc.info['cmdline']:
|
||||||
|
cmdline = ' '.join(proc.info['cmdline'])
|
||||||
|
if str(agent_config_path) in cmdline:
|
||||||
|
return True
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
except ImportError:
|
||||||
|
# psutil not available, fall back to checking self.process only
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_status(self) -> None:
|
||||||
|
"""Update the agent status based on process state."""
|
||||||
|
if self.is_alive():
|
||||||
|
# Process is running, update status to RUNNING
|
||||||
|
if self.status in (AgentStatus.STOPPED, AgentStatus.ERROR, AgentStatus.STARTING):
|
||||||
|
self.status = AgentStatus.RUNNING
|
||||||
|
else:
|
||||||
|
# Process is not running, update status to STOPPED
|
||||||
|
if self.status == AgentStatus.RUNNING:
|
||||||
|
self.status = AgentStatus.STOPPED
|
||||||
|
|
||||||
def on(self, event: str, callback: Callable) -> None:
|
def on(self, event: str, callback: Callable) -> None:
|
||||||
"""Register an event callback."""
|
"""Register an event callback."""
|
||||||
if event in self._callbacks:
|
if event in self._callbacks:
|
||||||
@@ -177,14 +215,33 @@ class Agent:
|
|||||||
agent_config_path = work_dir / f"agent_{self.id}.yaml"
|
agent_config_path = work_dir / f"agent_{self.id}.yaml"
|
||||||
self._write_cagent_config(agent_config_path)
|
self._write_cagent_config(agent_config_path)
|
||||||
|
|
||||||
# Start cagent process
|
# Check if model is available, download if needed
|
||||||
|
# Get the model ID from the config we just wrote
|
||||||
|
with open(agent_config_path) as f:
|
||||||
|
config_data = yaml.safe_load(f)
|
||||||
|
model_id = config_data["agents"]["root"]["model"]
|
||||||
|
|
||||||
|
if not self._check_model_available(model_id):
|
||||||
|
logger.info(f"Model {model_id} not found locally, downloading...")
|
||||||
|
if not self._pull_model(model_id):
|
||||||
|
raise Exception(f"Failed to download model {model_id}")
|
||||||
|
|
||||||
|
# Start cagent API server
|
||||||
|
# Use 'cagent api' to run agent as HTTP API service
|
||||||
|
# Each agent gets a unique port based on hash of agent ID
|
||||||
|
port = 8000 + (hash(self.id) % 1000)
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"cagent",
|
"cagent",
|
||||||
"--config", str(agent_config_path),
|
"api",
|
||||||
"--model", self.config.model_id,
|
str(agent_config_path),
|
||||||
"--interactive" if self.config.interactive else "--daemon",
|
"--listen", f":{port}",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Log the command for debugging
|
||||||
|
logger.debug(f"Starting agent with command: {' '.join(cmd)}")
|
||||||
|
logger.info(f"Agent API will listen on port {port}")
|
||||||
|
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
@@ -194,6 +251,15 @@ class Agent:
|
|||||||
env={**os.environ, **self.config.environment},
|
env={**os.environ, **self.config.environment},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Give it a moment to start
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
# Check if process is still running
|
||||||
|
if self.process.poll() is not None:
|
||||||
|
# Process died immediately
|
||||||
|
stderr = self.process.stderr.read().decode() if self.process.stderr else ""
|
||||||
|
raise Exception(f"Agent process exited immediately: {stderr}")
|
||||||
|
|
||||||
self.status = AgentStatus.RUNNING
|
self.status = AgentStatus.RUNNING
|
||||||
self._emit("on_start", self)
|
self._emit("on_start", self)
|
||||||
logger.info(f"Agent {self.name} started successfully")
|
logger.info(f"Agent {self.name} started successfully")
|
||||||
@@ -208,10 +274,14 @@ class Agent:
|
|||||||
async def stop(self) -> bool:
|
async def stop(self) -> bool:
|
||||||
"""Stop the agent."""
|
"""Stop the agent."""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if self.status == AgentStatus.STOPPED:
|
# Update status first to check actual process state
|
||||||
|
self.update_status()
|
||||||
|
|
||||||
|
if self.status == AgentStatus.STOPPED and not self.is_alive():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Try to stop via subprocess object first
|
||||||
if self.process:
|
if self.process:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
try:
|
try:
|
||||||
@@ -219,6 +289,32 @@ class Agent:
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
self.process.kill()
|
self.process.kill()
|
||||||
self.process.wait()
|
self.process.wait()
|
||||||
|
else:
|
||||||
|
# No subprocess object, find and kill the process using psutil
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
work_dir = Path(self.config.working_directory)
|
||||||
|
agent_config_path = work_dir / f"agent_{self.id}.yaml"
|
||||||
|
|
||||||
|
# Search for cagent processes
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||||
|
try:
|
||||||
|
if proc.info['name'] == 'cagent' and proc.info['cmdline']:
|
||||||
|
cmdline = ' '.join(proc.info['cmdline'])
|
||||||
|
if str(agent_config_path) in cmdline:
|
||||||
|
logger.info(f"Terminating cagent process PID {proc.info['pid']}")
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=10)
|
||||||
|
except psutil.TimeoutExpired:
|
||||||
|
logger.warning(f"Process {proc.info['pid']} didn't terminate, killing")
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
break
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
except ImportError:
|
||||||
|
logger.warning("psutil not available, cannot stop agent without process object")
|
||||||
|
|
||||||
self.status = AgentStatus.STOPPED
|
self.status = AgentStatus.STOPPED
|
||||||
self._emit("on_stop", self)
|
self._emit("on_stop", self)
|
||||||
@@ -237,25 +333,60 @@ class Agent:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
|
||||||
# Record user message
|
# Record user message
|
||||||
user_msg = AgentMessage(role="user", content=content)
|
user_msg = AgentMessage(role="user", content=content)
|
||||||
self.message_history.append(user_msg)
|
self.message_history.append(user_msg)
|
||||||
|
|
||||||
# Send to agent process
|
# Get the temporary agent config path
|
||||||
if self.process and self.process.stdin:
|
agent_config_path = Path(tempfile.gettempdir()) / "debai" / f"agent_{self.id}.yaml"
|
||||||
self.process.stdin.write(f"{content}\n".encode())
|
|
||||||
self.process.stdin.flush()
|
|
||||||
|
|
||||||
# Read response
|
if not agent_config_path.exists():
|
||||||
if self.process and self.process.stdout:
|
logger.error(f"Agent config file not found: {agent_config_path}")
|
||||||
response = self.process.stdout.readline().decode().strip()
|
return None
|
||||||
assistant_msg = AgentMessage(role="assistant", content=response)
|
|
||||||
self.message_history.append(assistant_msg)
|
|
||||||
self._emit("on_message", self, assistant_msg)
|
|
||||||
return assistant_msg
|
|
||||||
|
|
||||||
|
# Run cagent with the message
|
||||||
|
cmd = [
|
||||||
|
"cagent", "run", str(agent_config_path), content,
|
||||||
|
"--tui=false"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Execute cagent run command
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
*cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
cwd=str(agent_config_path.parent)
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=120.0)
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
response_content = stdout.decode('utf-8').strip()
|
||||||
|
# Remove the "--- Agent: root ---" header if present
|
||||||
|
if response_content.startswith("--- Agent:"):
|
||||||
|
lines = response_content.split('\n')
|
||||||
|
if len(lines) > 1:
|
||||||
|
response_content = '\n'.join(lines[1:]).strip()
|
||||||
|
|
||||||
|
if response_content:
|
||||||
|
assistant_msg = AgentMessage(role="assistant", content=response_content)
|
||||||
|
self.message_history.append(assistant_msg)
|
||||||
|
self._emit("on_message", self, assistant_msg)
|
||||||
|
return assistant_msg
|
||||||
|
else:
|
||||||
|
logger.error(f"Empty response from agent {self.name}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
error_text = stderr.decode('utf-8')
|
||||||
|
logger.error(f"Agent {self.name} error: {error_text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logger.error(f"Timeout waiting for response from agent {self.name}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error sending message to agent {self.name}: {e}")
|
logger.error(f"Error sending message to agent {self.name}: {e}")
|
||||||
return None
|
return None
|
||||||
@@ -288,29 +419,71 @@ class Agent:
|
|||||||
result["duration_seconds"] = (datetime.now() - start_time).total_seconds()
|
result["duration_seconds"] = (datetime.now() - start_time).total_seconds()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _check_model_available(self, model_name: str) -> bool:
|
||||||
|
"""Check if a model is available locally in docker-model."""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker-model", "list"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Parse output and check if model exists
|
||||||
|
# Model name might be in format "dmr/model" or just "model"
|
||||||
|
check_name = model_name.replace("dmr/", "")
|
||||||
|
return check_name in result.stdout
|
||||||
|
return False
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||||
|
logger.warning("Could not check model availability (docker-model not found)")
|
||||||
|
return True # Assume available if we can't check
|
||||||
|
|
||||||
|
def _pull_model(self, model_name: str) -> bool:
|
||||||
|
"""Pull a model using docker-model."""
|
||||||
|
try:
|
||||||
|
# Remove dmr/ prefix if present
|
||||||
|
pull_name = model_name.replace("dmr/", "")
|
||||||
|
logger.info(f"Downloading model {pull_name}...")
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
["docker-model", "pull", pull_name],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300, # 5 minutes timeout for download
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
logger.info(f"Successfully downloaded model {pull_name}")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"Failed to download model {pull_name}: {result.stderr}")
|
||||||
|
return False
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error(f"Timeout while downloading model {model_name}")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.error("docker-model command not found")
|
||||||
|
return False
|
||||||
|
|
||||||
def _write_cagent_config(self, path: Path) -> None:
|
def _write_cagent_config(self, path: Path) -> None:
|
||||||
"""Write cagent configuration file."""
|
"""Write cagent configuration file."""
|
||||||
|
# cagent v2 format
|
||||||
|
# Use the exact model requested by the user
|
||||||
|
model_id = self.config.model_id
|
||||||
|
|
||||||
|
# Add dmr/ prefix if not already present and no other provider is specified
|
||||||
|
if "/" not in model_id:
|
||||||
|
model_id = f"dmr/{model_id}"
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"agent": {
|
"version": "2",
|
||||||
"name": self.config.name,
|
"agents": {
|
||||||
"description": self.config.description,
|
"root": {
|
||||||
"type": self.config.agent_type,
|
"description": self.config.description or self.config.name,
|
||||||
},
|
"instruction": self.config.system_prompt,
|
||||||
"model": {
|
"model": model_id,
|
||||||
"id": self.config.model_id,
|
}
|
||||||
"provider": "docker-model",
|
}
|
||||||
},
|
|
||||||
"capabilities": list(self.config.capabilities),
|
|
||||||
"system_prompt": self.config.system_prompt,
|
|
||||||
"limits": {
|
|
||||||
"max_memory_mb": self.config.max_memory_mb,
|
|
||||||
"max_cpu_percent": self.config.max_cpu_percent,
|
|
||||||
"timeout_seconds": self.config.timeout_seconds,
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"allowed_commands": self.config.allowed_commands,
|
|
||||||
"denied_commands": self.config.denied_commands,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
@@ -368,6 +541,10 @@ class AgentManager:
|
|||||||
agent_type: Optional[AgentType] = None,
|
agent_type: Optional[AgentType] = None,
|
||||||
) -> list[Agent]:
|
) -> list[Agent]:
|
||||||
"""List agents with optional filtering."""
|
"""List agents with optional filtering."""
|
||||||
|
# Update all agent statuses first
|
||||||
|
for agent in self.agents.values():
|
||||||
|
agent.update_status()
|
||||||
|
|
||||||
agents = list(self.agents.values())
|
agents = list(self.agents.values())
|
||||||
|
|
||||||
if status:
|
if status:
|
||||||
|
|||||||
@@ -51,7 +51,13 @@ class DebaiApplication(Adw.Application):
|
|||||||
def do_activate(self):
|
def do_activate(self):
|
||||||
"""Called when the application is activated."""
|
"""Called when the application is activated."""
|
||||||
if not self.window:
|
if not self.window:
|
||||||
self.window = DebaiWindow(application=self)
|
self.window = DebaiWindow(
|
||||||
|
application=self,
|
||||||
|
agent_manager=self.agent_manager,
|
||||||
|
model_manager=self.model_manager,
|
||||||
|
task_manager=self.task_manager,
|
||||||
|
resource_monitor=self.resource_monitor
|
||||||
|
)
|
||||||
self.window.present()
|
self.window.present()
|
||||||
|
|
||||||
def do_startup(self):
|
def do_startup(self):
|
||||||
@@ -142,6 +148,14 @@ class DebaiApplication(Adw.Application):
|
|||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
margin: 4px 12px 4px 48px;
|
margin: 4px 12px 4px 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-message {
|
||||||
|
background: alpha(@accent_bg_color, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-message {
|
||||||
|
background: alpha(@card_bg_color, 0.9);
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
provider = Gtk.CssProvider()
|
provider = Gtk.CssProvider()
|
||||||
@@ -200,12 +214,221 @@ class DebaiApplication(Adw.Application):
|
|||||||
self.window.show_new_task_dialog()
|
self.window.show_new_task_dialog()
|
||||||
|
|
||||||
|
|
||||||
|
class ChatWindow(Adw.Window):
|
||||||
|
"""Chat window for interacting with an agent."""
|
||||||
|
|
||||||
|
def __init__(self, agent, agent_manager, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
self.agent = agent
|
||||||
|
self.agent_manager = agent_manager
|
||||||
|
self.messages = []
|
||||||
|
|
||||||
|
# Window setup
|
||||||
|
self.set_title(f"Chat: {agent.name}")
|
||||||
|
self.set_default_size(700, 600)
|
||||||
|
|
||||||
|
# Main box
|
||||||
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
||||||
|
self.set_content(main_box)
|
||||||
|
|
||||||
|
# Header bar
|
||||||
|
header = Adw.HeaderBar()
|
||||||
|
main_box.append(header)
|
||||||
|
|
||||||
|
# Agent info
|
||||||
|
info_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
info_box.set_margin_start(12)
|
||||||
|
info_box.set_margin_end(12)
|
||||||
|
info_box.set_margin_top(8)
|
||||||
|
info_box.set_margin_bottom(8)
|
||||||
|
|
||||||
|
status_icon = Gtk.Image.new_from_icon_name("emblem-ok-symbolic")
|
||||||
|
status_icon.add_css_class("success")
|
||||||
|
info_box.append(status_icon)
|
||||||
|
|
||||||
|
agent_label = Gtk.Label(label=f"{agent.name} ({agent.config.model_id})")
|
||||||
|
agent_label.add_css_class("caption")
|
||||||
|
info_box.append(agent_label)
|
||||||
|
|
||||||
|
main_box.append(info_box)
|
||||||
|
|
||||||
|
# Separator
|
||||||
|
main_box.append(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL))
|
||||||
|
|
||||||
|
# Chat area
|
||||||
|
scrolled = Gtk.ScrolledWindow()
|
||||||
|
scrolled.set_vexpand(True)
|
||||||
|
scrolled.set_hexpand(True)
|
||||||
|
main_box.append(scrolled)
|
||||||
|
|
||||||
|
self.chat_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
||||||
|
self.chat_box.set_margin_start(12)
|
||||||
|
self.chat_box.set_margin_end(12)
|
||||||
|
self.chat_box.set_margin_top(12)
|
||||||
|
self.chat_box.set_margin_bottom(12)
|
||||||
|
scrolled.set_child(self.chat_box)
|
||||||
|
|
||||||
|
# Store scrolled window for auto-scroll
|
||||||
|
self.scrolled = scrolled
|
||||||
|
|
||||||
|
# Welcome message
|
||||||
|
welcome_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
||||||
|
welcome_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
welcome_box.set_valign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
welcome_icon = Gtk.Image.new_from_icon_name("user-available-symbolic")
|
||||||
|
welcome_icon.set_pixel_size(48)
|
||||||
|
welcome_icon.add_css_class("dim-label")
|
||||||
|
welcome_box.append(welcome_icon)
|
||||||
|
|
||||||
|
welcome_label = Gtk.Label(label=f"Chat with {agent.name}")
|
||||||
|
welcome_label.add_css_class("title-2")
|
||||||
|
welcome_box.append(welcome_label)
|
||||||
|
|
||||||
|
welcome_subtitle = Gtk.Label(label="Ask questions or give commands to the agent")
|
||||||
|
welcome_subtitle.add_css_class("dim-label")
|
||||||
|
welcome_box.append(welcome_subtitle)
|
||||||
|
|
||||||
|
self.chat_box.append(welcome_box)
|
||||||
|
|
||||||
|
# Input area
|
||||||
|
input_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
input_box.set_margin_start(12)
|
||||||
|
input_box.set_margin_end(12)
|
||||||
|
input_box.set_margin_top(8)
|
||||||
|
input_box.set_margin_bottom(12)
|
||||||
|
main_box.append(input_box)
|
||||||
|
|
||||||
|
# Message entry
|
||||||
|
self.message_entry = Gtk.Entry()
|
||||||
|
self.message_entry.set_placeholder_text("Type your message...")
|
||||||
|
self.message_entry.set_hexpand(True)
|
||||||
|
self.message_entry.connect("activate", self._on_send_message)
|
||||||
|
input_box.append(self.message_entry)
|
||||||
|
|
||||||
|
# Send button
|
||||||
|
send_btn = Gtk.Button(label="Send")
|
||||||
|
send_btn.add_css_class("suggested-action")
|
||||||
|
send_btn.connect("clicked", self._on_send_message)
|
||||||
|
input_box.append(send_btn)
|
||||||
|
|
||||||
|
# Store send button to enable/disable
|
||||||
|
self.send_btn = send_btn
|
||||||
|
|
||||||
|
def _add_message(self, role, content):
|
||||||
|
"""Add a message to the chat."""
|
||||||
|
message_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
||||||
|
message_box.set_halign(Gtk.Align.START if role == "assistant" else Gtk.Align.END)
|
||||||
|
|
||||||
|
# Message bubble
|
||||||
|
bubble = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||||
|
bubble.add_css_class("card")
|
||||||
|
bubble.set_margin_start(8)
|
||||||
|
bubble.set_margin_end(8)
|
||||||
|
bubble.set_margin_top(4)
|
||||||
|
bubble.set_margin_bottom(4)
|
||||||
|
|
||||||
|
if role == "user":
|
||||||
|
bubble.add_css_class("user-message")
|
||||||
|
else:
|
||||||
|
bubble.add_css_class("assistant-message")
|
||||||
|
|
||||||
|
# Role label
|
||||||
|
role_label = Gtk.Label(label="You" if role == "user" else self.agent.name)
|
||||||
|
role_label.add_css_class("caption")
|
||||||
|
role_label.add_css_class("dim-label")
|
||||||
|
role_label.set_halign(Gtk.Align.START)
|
||||||
|
role_label.set_margin_start(12)
|
||||||
|
role_label.set_margin_end(12)
|
||||||
|
role_label.set_margin_top(8)
|
||||||
|
bubble.append(role_label)
|
||||||
|
|
||||||
|
# Message content
|
||||||
|
content_label = Gtk.Label(label=content)
|
||||||
|
content_label.set_wrap(True)
|
||||||
|
content_label.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
|
||||||
|
content_label.set_max_width_chars(60)
|
||||||
|
content_label.set_halign(Gtk.Align.START)
|
||||||
|
content_label.set_margin_start(12)
|
||||||
|
content_label.set_margin_end(12)
|
||||||
|
content_label.set_margin_bottom(8)
|
||||||
|
content_label.set_selectable(True)
|
||||||
|
bubble.append(content_label)
|
||||||
|
|
||||||
|
message_box.append(bubble)
|
||||||
|
self.chat_box.append(message_box)
|
||||||
|
|
||||||
|
# Auto-scroll to bottom
|
||||||
|
GLib.idle_add(self._scroll_to_bottom)
|
||||||
|
|
||||||
|
def _scroll_to_bottom(self):
|
||||||
|
"""Scroll chat to bottom."""
|
||||||
|
adj = self.scrolled.get_vadjustment()
|
||||||
|
adj.set_value(adj.get_upper() - adj.get_page_size())
|
||||||
|
|
||||||
|
def _on_send_message(self, widget):
|
||||||
|
"""Handle send message."""
|
||||||
|
message = self.message_entry.get_text().strip()
|
||||||
|
if not message:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Clear input
|
||||||
|
self.message_entry.set_text("")
|
||||||
|
|
||||||
|
# Disable input while processing
|
||||||
|
self.message_entry.set_sensitive(False)
|
||||||
|
self.send_btn.set_sensitive(False)
|
||||||
|
|
||||||
|
# Add user message
|
||||||
|
self._add_message("user", message)
|
||||||
|
|
||||||
|
# Send to agent in background thread
|
||||||
|
def send_async():
|
||||||
|
try:
|
||||||
|
response = asyncio.run(self.agent.send_message(message))
|
||||||
|
|
||||||
|
if response:
|
||||||
|
GLib.idle_add(self._handle_response, response.content)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(self._handle_error, "No response from agent")
|
||||||
|
except Exception as e:
|
||||||
|
GLib.idle_add(self._handle_error, str(e))
|
||||||
|
|
||||||
|
thread = threading.Thread(target=send_async, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _handle_response(self, content):
|
||||||
|
"""Handle agent response."""
|
||||||
|
self._add_message("assistant", content)
|
||||||
|
|
||||||
|
# Re-enable input
|
||||||
|
self.message_entry.set_sensitive(True)
|
||||||
|
self.send_btn.set_sensitive(True)
|
||||||
|
self.message_entry.grab_focus()
|
||||||
|
|
||||||
|
def _handle_error(self, error_msg):
|
||||||
|
"""Handle error."""
|
||||||
|
self._add_message("assistant", f"Error: {error_msg}")
|
||||||
|
|
||||||
|
# Re-enable input
|
||||||
|
self.message_entry.set_sensitive(True)
|
||||||
|
self.send_btn.set_sensitive(True)
|
||||||
|
self.message_entry.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
class DebaiWindow(Adw.ApplicationWindow):
|
class DebaiWindow(Adw.ApplicationWindow):
|
||||||
"""Main application window."""
|
"""Main application window."""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, agent_manager=None, model_manager=None, task_manager=None, resource_monitor=None, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
# Store managers
|
||||||
|
self.agent_manager = agent_manager or AgentManager()
|
||||||
|
self.model_manager = model_manager or ModelManager()
|
||||||
|
self.task_manager = task_manager or TaskManager()
|
||||||
|
self.resource_monitor = resource_monitor or ResourceMonitor()
|
||||||
|
|
||||||
self.set_title("Debai")
|
self.set_title("Debai")
|
||||||
self.set_default_size(1400, 900)
|
self.set_default_size(1400, 900)
|
||||||
self.set_icon_name("debai")
|
self.set_icon_name("debai")
|
||||||
@@ -385,13 +608,12 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
metrics_box.append(self.memory_metric)
|
metrics_box.append(self.memory_metric)
|
||||||
|
|
||||||
# Agents metric
|
# Agents metric
|
||||||
app = self.get_application()
|
agent_count = len(self.agent_manager.list_agents())
|
||||||
agent_count = len(app.agent_manager.list_agents()) if app else 0
|
|
||||||
self.agents_metric = self._create_metric_card("Agents", str(agent_count), "system-users-symbolic")
|
self.agents_metric = self._create_metric_card("Agents", str(agent_count), "system-users-symbolic")
|
||||||
metrics_box.append(self.agents_metric)
|
metrics_box.append(self.agents_metric)
|
||||||
|
|
||||||
# Tasks metric
|
# Tasks metric
|
||||||
task_count = len(app.task_manager.list_tasks()) if app else 0
|
task_count = len(self.task_manager.list_tasks()) if self.task_manager else 0
|
||||||
self.tasks_metric = self._create_metric_card("Tasks", str(task_count), "view-list-symbolic")
|
self.tasks_metric = self._create_metric_card("Tasks", str(task_count), "view-list-symbolic")
|
||||||
metrics_box.append(self.tasks_metric)
|
metrics_box.append(self.tasks_metric)
|
||||||
|
|
||||||
@@ -487,6 +709,26 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
def _update_dashboard_metrics(self):
|
||||||
|
"""Update dashboard metrics with current values."""
|
||||||
|
app = self.get_application()
|
||||||
|
if not app:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update agents count
|
||||||
|
agent_count = len(self.agent_manager.list_agents())
|
||||||
|
for child in self.agents_metric:
|
||||||
|
if isinstance(child, Gtk.Label) and "metric-value" in child.get_css_classes():
|
||||||
|
child.set_label(str(agent_count))
|
||||||
|
break
|
||||||
|
|
||||||
|
# Update tasks count
|
||||||
|
task_count = len(self.task_manager.list_tasks())
|
||||||
|
for child in self.tasks_metric:
|
||||||
|
if isinstance(child, Gtk.Label) and "metric-value" in child.get_css_classes():
|
||||||
|
child.set_label(str(task_count))
|
||||||
|
break
|
||||||
|
|
||||||
def _create_agents_page(self) -> Gtk.Widget:
|
def _create_agents_page(self) -> Gtk.Widget:
|
||||||
"""Create the agents page."""
|
"""Create the agents page."""
|
||||||
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
page = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
@@ -549,11 +791,9 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
break
|
break
|
||||||
self.agents_list.remove(row)
|
self.agents_list.remove(row)
|
||||||
|
|
||||||
app = self.get_application()
|
# Load agents from manager
|
||||||
if not app:
|
self.agent_manager.load_agents()
|
||||||
return
|
agents = self.agent_manager.list_agents()
|
||||||
|
|
||||||
agents = app.agent_manager.list_agents()
|
|
||||||
|
|
||||||
if not agents:
|
if not agents:
|
||||||
# Empty state
|
# Empty state
|
||||||
@@ -958,8 +1198,7 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
app = self.get_application()
|
success = loop.run_until_complete(self.agent_manager.start_agent(agent_id))
|
||||||
success = loop.run_until_complete(app.agent_manager.start_agent(agent_id))
|
|
||||||
if success:
|
if success:
|
||||||
GLib.idle_add(self._show_agent_started, agent_id)
|
GLib.idle_add(self._show_agent_started, agent_id)
|
||||||
else:
|
else:
|
||||||
@@ -981,8 +1220,7 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
app = self.get_application()
|
success = loop.run_until_complete(self.agent_manager.stop_agent(agent_id))
|
||||||
success = loop.run_until_complete(app.agent_manager.stop_agent(agent_id))
|
|
||||||
if success:
|
if success:
|
||||||
GLib.idle_add(self._show_agent_stopped, agent_id)
|
GLib.idle_add(self._show_agent_stopped, agent_id)
|
||||||
else:
|
else:
|
||||||
@@ -999,8 +1237,7 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
def _on_delete_agent(self, button):
|
def _on_delete_agent(self, button):
|
||||||
"""Delete an agent."""
|
"""Delete an agent."""
|
||||||
agent_id = button.agent_id
|
agent_id = button.agent_id
|
||||||
app = self.get_application()
|
agent = self.agent_manager.get_agent(agent_id)
|
||||||
agent = app.agent_manager.get_agent(agent_id)
|
|
||||||
|
|
||||||
if not agent:
|
if not agent:
|
||||||
return
|
return
|
||||||
@@ -1022,8 +1259,7 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
app = self.get_application()
|
success = loop.run_until_complete(self.agent_manager.delete_agent(agent_id))
|
||||||
success = loop.run_until_complete(app.agent_manager.delete_agent(agent_id))
|
|
||||||
if success:
|
if success:
|
||||||
GLib.idle_add(self._show_agent_deleted, agent_id)
|
GLib.idle_add(self._show_agent_deleted, agent_id)
|
||||||
else:
|
else:
|
||||||
@@ -1040,14 +1276,34 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
def _on_chat_agent(self, button):
|
def _on_chat_agent(self, button):
|
||||||
"""Open chat with agent."""
|
"""Open chat with agent."""
|
||||||
agent_id = button.agent_id
|
agent_id = button.agent_id
|
||||||
# TODO: Implement chat interface
|
agent = self.agent_manager.get_agent(agent_id)
|
||||||
dialog = Adw.MessageDialog(
|
|
||||||
transient_for=self,
|
if not agent:
|
||||||
heading="Chat",
|
dialog = Adw.MessageDialog(
|
||||||
body="Chat interface coming soon!",
|
transient_for=self,
|
||||||
)
|
heading="Error",
|
||||||
dialog.add_response("ok", "OK")
|
body="Agent not found",
|
||||||
dialog.present()
|
)
|
||||||
|
dialog.add_response("ok", "OK")
|
||||||
|
dialog.present()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if agent is running
|
||||||
|
agent.update_status()
|
||||||
|
if not agent.is_alive():
|
||||||
|
dialog = Adw.MessageDialog(
|
||||||
|
transient_for=self,
|
||||||
|
heading="Agent Not Running",
|
||||||
|
body=f"Agent '{agent.name}' is not running. Please start it first.",
|
||||||
|
)
|
||||||
|
dialog.add_response("ok", "OK")
|
||||||
|
dialog.present()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Open chat window
|
||||||
|
chat_window = ChatWindow(agent=agent, agent_manager=self.agent_manager)
|
||||||
|
chat_window.set_transient_for(self)
|
||||||
|
chat_window.present()
|
||||||
|
|
||||||
def _show_agent_started(self, agent_id):
|
def _show_agent_started(self, agent_id):
|
||||||
"""Show agent started notification and refresh."""
|
"""Show agent started notification and refresh."""
|
||||||
@@ -1077,8 +1333,7 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
|
|
||||||
def show_new_agent_dialog(self):
|
def show_new_agent_dialog(self):
|
||||||
"""Show dialog to create a new agent."""
|
"""Show dialog to create a new agent."""
|
||||||
app = self.get_application()
|
dialog = NewAgentDialog(transient_for=self, agent_manager=self.agent_manager, main_window=self)
|
||||||
dialog = NewAgentDialog(transient_for=self, agent_manager=app.agent_manager)
|
|
||||||
dialog.present()
|
dialog.present()
|
||||||
|
|
||||||
def show_new_task_dialog(self):
|
def show_new_task_dialog(self):
|
||||||
@@ -1095,10 +1350,11 @@ class DebaiWindow(Adw.ApplicationWindow):
|
|||||||
class NewAgentDialog(Adw.Window):
|
class NewAgentDialog(Adw.Window):
|
||||||
"""Dialog for creating a new agent."""
|
"""Dialog for creating a new agent."""
|
||||||
|
|
||||||
def __init__(self, agent_manager: AgentManager, **kwargs):
|
def __init__(self, agent_manager: AgentManager, main_window, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.agent_manager = agent_manager
|
self.agent_manager = agent_manager
|
||||||
|
self.main_window = main_window
|
||||||
self.set_title("Create Agent")
|
self.set_title("Create Agent")
|
||||||
self.set_default_size(500, 600)
|
self.set_default_size(500, 600)
|
||||||
self.set_modal(True)
|
self.set_modal(True)
|
||||||
@@ -1278,6 +1534,11 @@ class NewAgentDialog(Adw.Window):
|
|||||||
|
|
||||||
def _show_success(self, name):
|
def _show_success(self, name):
|
||||||
"""Show success dialog."""
|
"""Show success dialog."""
|
||||||
|
# Refresh the agents list in the main window
|
||||||
|
if self.main_window:
|
||||||
|
self.main_window._refresh_agents()
|
||||||
|
self.main_window._update_dashboard_metrics()
|
||||||
|
|
||||||
success_dialog = Adw.MessageDialog(
|
success_dialog = Adw.MessageDialog(
|
||||||
transient_for=self,
|
transient_for=self,
|
||||||
heading="Success",
|
heading="Success",
|
||||||
|
|||||||
Referencia en una nueva incidencia
Block a user