first commit

This commit is contained in:
DigiJ
2026-03-13 12:56:43 -07:00
commit 159cf9fcfe
309 changed files with 64584 additions and 0 deletions

944
agent_dhal/hal.py Normal file
View File

@@ -0,0 +1,944 @@
#!/usr/bin/env python3
"""
Hal - Primary AI Agent for AgentDhal Framework
Hal is the main AI assistant agent providing:
- Multi-turn conversations
- Function/tool calling capabilities
- Code execution and analysis
- Team collaboration
- Memory and context management
- Customizable behavior and prompts
Based on AutoGen AssistantAgent with DarkHal-specific enhancements.
"""
import asyncio
import subprocess
import sys
import os
import json
import requests
from typing import Any, Dict, List, Optional, Callable, Union
from dataclasses import dataclass
import platform
import shutil
import shlex
import ctypes
import tempfile
# Import actual working AgentDhal components
from .agentdhal_core import (
Agent,
AgentId,
MessageContext,
RoutedAgent,
message_handler,
default_subscription,
SingleThreadedAgentRuntime
)
try:
from .agentdhal_core.models import (
ChatCompletionClient,
LLMMessage,
SystemMessage,
UserMessage,
)
except ImportError:
# Define basic message types if not available
class LLMMessage:
def __init__(self, content: str):
self.content = content
class SystemMessage(LLMMessage):
def __init__(self, content: str):
super().__init__(content)
self.role = "system"
class UserMessage(LLMMessage):
def __init__(self, content: str):
super().__init__(content)
self.role = "user"
# Define ChatCompletionClient as a basic class
class ChatCompletionClient:
pass
try:
from .agentdhal_core.tools import FunctionTool, Tool
except ImportError:
# Create working tool implementation compatible with add_function(func, description)
class FunctionTool:
def __init__(self, func: Callable, description: str = ""):
self.func = func
self.description = description
Tool = FunctionTool # alias for typing
class HalModelClient:
"""Model client that integrates with DarkHal's LLM runtime."""
def __init__(self, model_name: str = "gpt-4"):
self.model_name = model_name
self.llm_model = None
self._initialize_model()
def _initialize_model(self):
"""Initialize the LLM model using DarkHal's runtime."""
try:
# Import from the main application's LLM runtime
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from llm_runtime import load_model
# Load model using existing runtime
self.llm_model = load_model(
source=self.model_name,
device="auto",
quantization="4bit"
)
print(f"[Hal] Initialized model: {self.model_name}")
except Exception as e:
print(f"[Hal] Could not initialize model {self.model_name}: {e}")
self.llm_model = None
async def create_chat_completion(self, messages: List[LLMMessage], **kwargs):
"""Create chat completion using the loaded model (supports streaming)."""
try:
if not self.llm_model:
raise Exception("No model loaded")
# Convert messages to text format
conversation = ""
for msg in messages:
if hasattr(msg, 'role') and hasattr(msg, 'content'):
if msg.role == "system":
conversation += f"System: {msg.content}\n\n"
elif msg.role == "user":
conversation += f"User: {msg.content}\n\n"
elif msg.role == "assistant":
conversation += f"Assistant: {msg.content}\n\n"
else:
conversation += f"{msg.content}\n\n"
conversation += "Assistant: "
# Callbacks and options
stream = bool(kwargs.get('stream', False))
on_token = kwargs.get('on_token')
on_complete = kwargs.get('on_complete')
on_error = kwargs.get('on_error')
temperature = kwargs.get('temperature', 0.7)
max_tokens = kwargs.get('max_tokens', 2000)
# Try to build a config if available (optional)
cfg = None
try:
from llm_runtime import GenerateConfig # type: ignore
cfg = GenerateConfig(
max_tokens=max_tokens,
temperature=temperature
)
except Exception:
cfg = None
full_text = ""
# Streaming if supported
if stream and hasattr(self.llm_model, 'stream'):
try:
iterator = self.llm_model.stream(conversation, cfg) if cfg is not None else self.llm_model.stream(conversation)
for chunk in iterator:
if chunk:
full_text += str(chunk)
if on_token:
try:
on_token("request-1", str(chunk))
except Exception:
pass
if on_complete:
try:
on_complete("request-1", full_text, {"finish_reason": "stop"})
except Exception:
pass
except Exception as se:
if on_error:
try:
on_error("request-1", se)
except Exception:
pass
raise
else:
# Non-streaming path
if hasattr(self.llm_model, 'generate'):
full_text = self.llm_model.generate(conversation, cfg) if cfg is not None else self.llm_model.generate(conversation)
elif hasattr(self.llm_model, '__call__'):
full_text = self.llm_model(conversation)
else:
# Fallback for different model interfaces
full_text = str(self.llm_model)
if on_complete:
try:
on_complete("request-1", full_text, {"finish_reason": "stop"})
except Exception:
pass
# Create response object
class CompletionResponse:
def __init__(self, content):
self.content = content
self.function_calls = None
return CompletionResponse(full_text)
except Exception as e:
print(f"[Hal] Error generating response: {e}")
# Return error response
class CompletionResponse:
def __init__(self, content):
self.content = content
self.function_calls = None
if kwargs.get('on_error'):
try:
kwargs['on_error']("request-1", e)
except Exception:
pass
return CompletionResponse(f"I apologize, I encountered an error: {str(e)}")
def is_available(self) -> bool:
"""Check if the model client is available."""
return self.llm_model is not None
@dataclass
class DhalConfig:
"""Configuration for Hal agent."""
name: str = "Hal"
system_message: str = "You are Hal, an advanced AI assistant integrated into DarkHal 2.0. You help users with coding, analysis, security testing, and general AI tasks."
model: str = "gpt-4"
temperature: float = 0.7
max_tokens: int = 2000
tools: List[FunctionTool] = None # type: ignore
memory_limit: int = 10000
def __post_init__(self):
if self.tools is None:
self.tools = []
class Dhal(RoutedAgent):
"""
Dhal - The primary AI agent for DarkHal 2.0
Dhal provides advanced conversational AI capabilities with:
- Natural language understanding and generation
- Function calling and tool integration
- Code execution and analysis
- Memory and context management
- Team collaboration capabilities
"""
def __init__(
self,
config: DhalConfig,
model_client: ChatCompletionClient,
agent_id: Optional[AgentId] = None
):
"""Initialize Dhal agent."""
if agent_id is None:
agent_id = AgentId(config.name, "dhal")
super().__init__(config.name)
self.config = config
self.model_client = model_client
self.agent_id = agent_id
# Initialize conversation memory
self.conversation_history: List[LLMMessage] = []
if config.system_message:
self.conversation_history.append(SystemMessage(content=config.system_message))
# Tools and functions
self.tools = config.tools or []
self.function_map: Dict[str, Callable] = {}
# Agent state
self.is_active = False
self.current_task = None
self._register_default_tools()
def _register_default_tools(self):
"""Register default tools available to Hal."""
# Code execution tool
def execute_python(code: str) -> str:
"""Execute Python code and return the result."""
try:
import tempfile
import subprocess
# Create a temporary file to execute the code
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
# Execute the code and capture output
result = subprocess.run([sys.executable, temp_file],
capture_output=True, text=True, timeout=30, cwd=os.getcwd())
output = ""
if result.stdout:
output += f"Output:\n{result.stdout}"
if result.stderr:
output += f"\nErrors:\n{result.stderr}"
if result.returncode != 0:
output += f"\nReturn code: {result.returncode}"
return output or "Code executed successfully (no output)"
finally:
# Always clean up temp file
try:
os.unlink(temp_file)
except:
pass
except subprocess.TimeoutExpired:
return "Error: Code execution timed out (30 second limit)"
except Exception as e:
return f"Error executing code: {str(e)}"
# Web search tool
def web_search(query: str) -> str:
"""Search the web for information using DuckDuckGo API."""
try:
import requests
# Use DuckDuckGo instant answer API
url = "https://api.duckduckgo.com/"
params = {
'q': query,
'format': 'json',
'no_html': '1',
'skip_disambig': '1'
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
result = f"Search query: {query}\n\n"
# Extract useful information
if data.get('AbstractText'):
result += f"Summary: {data['AbstractText']}\n\n"
if data.get('RelatedTopics'):
result += "Related information:\n"
for i, topic in enumerate(data['RelatedTopics'][:5]): # Limit to first 5
if isinstance(topic, dict) and 'Text' in topic:
result += f"{i + 1}. {topic['Text']}\n"
elif isinstance(topic, dict) and 'Topics' in topic:
# Handle nested topics
for subtopic in topic['Topics'][:2]:
if 'Text' in subtopic:
result += f"{i + 1}. {subtopic['Text']}\n"
if data.get('Answer'):
result += f"\nDirect answer: {data['Answer']}\n"
if data.get('Definition'):
result += f"\nDefinition: {data['Definition']}\n"
return result if result.strip() != f"Search query: {query}" else f"No specific results found for: {query}"
except Exception as e:
return f"Error performing web search: {str(e)}"
# Unrestricted file read
def read_file(filepath: str) -> str:
try:
abs_path = os.path.abspath(filepath)
with open(abs_path, 'r', encoding='utf-8') as f:
content = f.read()
return f"File: {abs_path}\nSize: {len(content)} characters\n\n{content}"
except UnicodeDecodeError:
# Fallback for binary files: return first 4096 bytes
try:
with open(abs_path, 'rb') as f:
data = f.read(4096)
return f"Binary file: {abs_path}\nFirst 4096 bytes:\n{data}"
except Exception as e:
return f"Error reading binary file: {abs_path}\n{e}"
except Exception as e:
return f"Error reading file: {abs_path}\n{e}"
# Unrestricted file write (creates parent dirs)
def write_file(filepath: str, content: str) -> str:
try:
abs_path = os.path.abspath(filepath)
os.makedirs(os.path.dirname(abs_path) or ".", exist_ok=True)
with open(abs_path, 'w', encoding='utf-8') as f:
f.write(content)
return f"Wrote {len(content)} bytes to {abs_path}"
except Exception as e:
return f"Error writing file: {abs_path}\n{e}"
# Unrestricted directory list
def list_files(directory: str = ".") -> str:
try:
abs_dir = os.path.abspath(directory)
items = []
for name in sorted(os.listdir(abs_dir)):
p = os.path.join(abs_dir, name)
if os.path.isdir(p):
items.append(f"[DIR] {name}/")
else:
items.append(f"[FILE] {name} ({os.path.getsize(p)} bytes)")
return f"Directory: {abs_dir}\n\n" + "\n".join(items)
except Exception as e:
return f"Error listing {directory}\n{e}"
# Unrestricted shell command (optionally with cwd/timeout)
def run_shell_command(command: str, cwd: str = None, timeout: int = 300) -> str:
try:
r = subprocess.run(
command,
shell=True,
cwd=cwd,
capture_output=True,
text=True,
timeout=timeout
)
out = r.stdout
if r.stderr:
out += f"\n[stderr]\n{r.stderr}"
out += f"\n[exit_code] {r.returncode}"
return out.strip()
except subprocess.TimeoutExpired:
return f"Error: timed out after {timeout}s"
except Exception as e:
return f"Error: {e}"
# --- NEW: PowerShell (Windows), Bash (Linux/macOS), and Ruby runner ---
def powershell(command: str, cwd: str = None, timeout: int = 120) -> str:
"""Run a PowerShell command on Windows (prefers pwsh, falls back to powershell)."""
exe = shutil.which("pwsh") or shutil.which("powershell")
if not exe:
return "Error: PowerShell not found"
cmd = [exe, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command]
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
out = r.stdout
if r.stderr:
out += f"\n[stderr]\n{r.stderr}"
out += f"\n[exit_code] {r.returncode}"
return out.strip()
def bash(command: str, cwd: str = None, timeout: int = 120) -> str:
"""Run a Bash command on Linux/macOS."""
exe = shutil.which("bash")
if not exe:
return "Error: bash not found"
cmd = [exe, "-lc", command]
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
out = r.stdout
if r.stderr:
out += f"\n[stderr]\n{r.stderr}"
out += f"\n[exit_code] {r.returncode}"
return out.strip()
def ruby_run(script_path: str, args: str = "", cwd: str = None, timeout: int = 180,
use_bundler: bool = False) -> str:
"""Run a Ruby script. If use_bundler=True and Gemfile+bundle present → `bundle exec ruby`."""
ruby = shutil.which("ruby")
if not ruby:
return "Error: ruby not found in PATH"
argv = shlex.split(args) if args else []
if use_bundler and shutil.which("bundle") and os.path.exists(os.path.join(cwd or os.getcwd(), "Gemfile")):
cmd = ["bundle", "exec", ruby, script_path] + argv
else:
cmd = [ruby, script_path] + argv
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
out = r.stdout
if r.stderr:
out += f"\n[stderr]\n{r.stderr}"
out += f"\n[exit_code] {r.returncode}"
return out.strip()
# --- NEW: Elevated commands ---
def powershell_admin(command: str, cwd: str = None, timeout: int = 300) -> str:
"""
Run an elevated (UAC) PowerShell command on Windows.
Captures output by running a temporary elevated script and reading its output file.
"""
exe = shutil.which("pwsh") or shutil.which("powershell")
if not exe:
return "Error: PowerShell not found"
if platform.system() != "Windows":
return "Error: Admin PowerShell is Windows-only"
# Prepare temp script and output file
with tempfile.TemporaryDirectory() as td:
out_file = os.path.join(td, "out.txt")
script_file = os.path.join(td, "script.ps1")
# PS script: run the user's command and redirect ALL streams to file
script = f"$ErrorActionPreference='Continue'; & {{ {command} }} *> '{out_file}'"
with open(script_file, "w", encoding="utf-8") as f:
f.write(script)
# Build a PS command that elevates a new PowerShell instance to run the script
# We pass '-File "<script_file>"' to the elevated process and wait for it.
args_str = f"-NoProfile -NonInteractive -ExecutionPolicy Bypass -File \"{script_file}\""
ps_cmd = f"Start-Process -Verb RunAs -FilePath '{exe}' -ArgumentList '{args_str}' -Wait"
# Launch the elevation prompt
r = subprocess.run(
[exe, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps_cmd],
cwd=cwd, capture_output=True, text=True, timeout=timeout
)
# Read output written by elevated process
out = ""
try:
if os.path.exists(out_file):
with open(out_file, "r", encoding="utf-8", errors="replace") as f:
out = f.read().strip()
except Exception as e:
out += f"\n[read_error] {e}"
if r.stderr:
out += f"\n[launcher_stderr]\n{r.stderr.strip()}"
out += f"\n[launcher_exit_code] {r.returncode}"
return out.strip()
def sudo(command: str, cwd: str = None, timeout: int = 300) -> str:
"""
Run a command with elevated privileges on Linux (and most *nix).
Prefers pkexec (GUI polkit prompt); falls back to sudo (TTY or policykit may prompt).
"""
if platform.system() == "Windows":
return "Error: sudo is not available on Windows"
if shutil.which("pkexec"):
cmd = ["pkexec", "bash", "-lc", command]
elif shutil.which("sudo"):
# This will require a TTY or desktop prompt depending on environment
cmd = ["sudo", "bash", "-lc", command]
else:
return "Error: Neither pkexec nor sudo found"
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout)
out = r.stdout
if r.stderr:
out += f"\n[stderr]\n{r.stderr}"
out += f"\n[exit_code] {r.returncode}"
return out.strip()
# --- Mouse control (Windows + Linux via xdotool) ---
def mouse_move(x: int, y: int) -> str:
"""Move mouse to absolute screen coordinates (x, y)."""
sysname = platform.system()
if sysname == "Windows":
ctypes.windll.user32.SetCursorPos(int(x), int(y))
return "ok"
elif sysname in ("Linux", "FreeBSD"):
if not shutil.which("xdotool"):
return "Error: xdotool not found (Wayland may not support it)"
r = subprocess.run(["xdotool", "mousemove", str(int(x)), str(int(y))],
capture_output=True, text=True)
return "ok" if r.returncode == 0 else (r.stderr or r.stdout)
else:
return "Unsupported platform"
def mouse_click(button: str = "left") -> str:
"""Click mouse button: left|middle|right."""
sysname = platform.system()
if sysname == "Windows":
btn = button.lower()
if btn == "left":
down, up = 0x0002, 0x0004
elif btn == "right":
down, up = 0x0008, 0x0010
elif btn == "middle":
down, up = 0x0020, 0x0040
else:
return "Error: unknown button"
ctypes.windll.user32.mouse_event(down, 0, 0, 0, 0)
ctypes.windll.user32.mouse_event(up, 0, 0, 0, 0)
return "ok"
elif sysname in ("Linux", "FreeBSD"):
if not shutil.which("xdotool"):
return "Error: xdotool not found"
map_btn = {"left": "1", "middle": "2", "right": "3"}
code = map_btn.get(button.lower())
if not code:
return "Error: unknown button"
r = subprocess.run(["xdotool", "click", code], capture_output=True, text=True)
return "ok" if r.returncode == 0 else (r.stderr or r.stdout)
else:
return "Unsupported platform"
def mouse_scroll(lines: int = 1) -> str:
"""Scroll vertically: positive = up, negative = down."""
sysname = platform.system()
if sysname == "Windows":
# 120 units per wheel click
delta = int(lines) * 120
ctypes.windll.user32.mouse_event(0x0800, 0, 0, delta, 0)
return "ok"
elif sysname in ("Linux", "FreeBSD"):
if not shutil.which("xdotool"):
return "Error: xdotool not found"
clicks = abs(int(lines))
button = "4" if int(lines) > 0 else "5"
for _ in range(clicks):
subprocess.run(["xdotool", "click", button], capture_output=True)
return "ok"
else:
return "Unsupported platform"
# =========================
# WINDOWS XINPUT (DirectX)
# =========================
# Tools: xinput_list, xinput_get_state, xinput_set_vibration
def _load_xinput_dll():
if platform.system() != "Windows":
return None
for dll in ("XInput1_4.dll", "XInput1_3.dll", "XInput9_1_0.dll"):
try:
return ctypes.WinDLL(dll)
except OSError:
continue
return None
# Structures from XInput
class XINPUT_GAMEPAD(ctypes.Structure):
_fields_ = [
("wButtons", ctypes.c_ushort),
("bLeftTrigger", ctypes.c_ubyte),
("bRightTrigger", ctypes.c_ubyte),
("sThumbLX", ctypes.c_short),
("sThumbLY", ctypes.c_short),
("sThumbRX", ctypes.c_short),
("sThumbRY", ctypes.c_short),
]
class XINPUT_STATE(ctypes.Structure):
_fields_ = [
("dwPacketNumber", ctypes.c_uint),
("Gamepad", XINPUT_GAMEPAD),
]
class XINPUT_VIBRATION(ctypes.Structure):
_fields_ = [
("wLeftMotorSpeed", ctypes.c_ushort),
("wRightMotorSpeed", ctypes.c_ushort),
]
# Button bitmasks
XINPUT_BUTTONS = {
"DPAD_UP": 0x0001,
"DPAD_DOWN": 0x0002,
"DPAD_LEFT": 0x0004,
"DPAD_RIGHT": 0x0008,
"START": 0x0010,
"BACK": 0x0020,
"LEFT_THUMB": 0x0040,
"RIGHT_THUMB": 0x0080,
"LEFT_SHOULDER": 0x0100,
"RIGHT_SHOULDER": 0x0200,
"A": 0x1000,
"B": 0x2000,
"X": 0x4000,
"Y": 0x8000,
}
def xinput_list() -> str:
"""List connected XInput (Xbox-compatible) controllers on Windows."""
if platform.system() != "Windows":
return "Unsupported platform (XInput is Windows/DirectX)"
dll = _load_xinput_dll()
if not dll:
return "Error: XInput DLL not found"
XInputGetState = dll.XInputGetState
XInputGetState.argtypes = [ctypes.c_uint, ctypes.POINTER(XINPUT_STATE)]
XInputGetState.restype = ctypes.c_uint # 0 = success
found = []
for i in range(4):
state = XINPUT_STATE()
rc = XInputGetState(i, ctypes.byref(state))
found.append({"id": i, "connected": (rc == 0)})
return json.dumps(found)
def xinput_get_state(controller_id: int = 0) -> str:
"""Get state for a specific controller id (0-3). Returns JSON with buttons/axes/triggers."""
if platform.system() != "Windows":
return "Unsupported platform"
dll = _load_xinput_dll()
if not dll:
return "Error: XInput DLL not found"
XInputGetState = dll.XInputGetState
XInputGetState.argtypes = [ctypes.c_uint, ctypes.POINTER(XINPUT_STATE)]
XInputGetState.restype = ctypes.c_uint
state = XINPUT_STATE()
rc = XInputGetState(int(controller_id), ctypes.byref(state))
if rc != 0:
return f"Error: controller {controller_id} not connected"
gp = state.Gamepad
buttons = gp.wButtons
pressed = [name for name, mask in XINPUT_BUTTONS.items() if buttons & mask]
data = {
"id": int(controller_id),
"packet": state.dwPacketNumber,
"buttons": pressed,
"left_trigger": int(gp.bLeftTrigger),
"right_trigger": int(gp.bRightTrigger),
"thumb_lx": int(gp.sThumbLX),
"thumb_ly": int(gp.sThumbLY),
"thumb_rx": int(gp.sThumbRX),
"thumb_ry": int(gp.sThumbRY),
}
return json.dumps(data)
def xinput_set_vibration(controller_id: int = 0,
left_motor: int = 0,
right_motor: int = 0) -> str:
"""Set rumble (0-65535 per motor)."""
if platform.system() != "Windows":
return "Unsupported platform"
dll = _load_xinput_dll()
if not dll:
return "Error: XInput DLL not found"
XInputSetState = dll.XInputSetState
XInputSetState.argtypes = [ctypes.c_uint, ctypes.POINTER(XINPUT_VIBRATION)]
XInputSetState.restype = ctypes.c_uint
vib = XINPUT_VIBRATION(
wLeftMotorSpeed=max(0, min(65535, int(left_motor))),
wRightMotorSpeed=max(0, min(65535, int(right_motor)))
)
rc = XInputSetState(int(controller_id), ctypes.byref(vib))
return "ok" if rc == 0 else f"Error: rc={rc}"
# Register tools
self.add_function("execute_python", execute_python, "Execute Python code and return results")
self.add_function("web_search", web_search, "Search the web for information using DuckDuckGo")
self.add_function("read_file", read_file, "Read contents of a file")
self.add_function("write_file", write_file, "Write content to a file")
self.add_function("list_files", list_files, "List files and directories")
self.add_function("run_shell_command", run_shell_command, "Run shell commands")
# NEW registrations (standard shells and Ruby)
self.add_function("powershell", powershell, "Run a PowerShell command on Windows")
self.add_function("bash", bash, "Run a Bash command on Linux/macOS")
self.add_function("ruby_run", ruby_run, "Run a Ruby script (optionally via bundler)")
# NEW registrations (elevated)
self.add_function("powershell_admin", powershell_admin, "Run elevated PowerShell with UAC; captures output")
self.add_function("sudo", sudo, "Run a command with sudo or pkexec; user must approve")
# NEW registrations (input control)
self.add_function("mouse_move", mouse_move, "Move mouse to absolute screen coordinates")
self.add_function("mouse_click", mouse_click, "Click mouse button: left|middle|right")
self.add_function("mouse_scroll", mouse_scroll, "Scroll vertically by N lines (±)")
# Windows XInput (DirectX) gamepad tools
self.add_function("xinput_list", xinput_list, "List XInput controllers (Windows/DirectX)")
self.add_function("xinput_get_state", xinput_get_state, "Get XInput state for controller id (0-3)")
self.add_function("xinput_set_vibration", xinput_set_vibration, "Set XInput vibration (rumble)")
def add_function(self, name: str, func: Callable, description: str):
"""Add a function tool to Hal's capabilities."""
self.function_map[name] = func
tool = FunctionTool(func, description=description)
self.tools.append(tool)
@message_handler
async def handle_user_message(self, message: str, ctx: MessageContext) -> str:
"""Handle user messages and generate responses."""
try:
# Add user message to conversation history
user_msg = UserMessage(content=message)
self.conversation_history.append(user_msg)
# Prepare messages for model
messages = self._prepare_messages()
# Generate response using model client
response = await self.model_client.create_chat_completion(
messages=messages,
model=self.config.model,
temperature=self.config.temperature,
max_tokens=self.config.max_tokens,
tools=self.tools if self.tools else None
)
# Process response
assistant_message = response.content
# Handle function calls if present
if hasattr(response, 'function_calls') and response.function_calls:
assistant_message = await self._handle_function_calls(response.function_calls)
# Add assistant response to history
self.conversation_history.append(UserMessage(content=assistant_message))
# Manage memory limits
self._manage_memory()
return assistant_message
except Exception as e:
error_msg = f"Hal encountered an error: {str(e)}"
self.conversation_history.append(UserMessage(content=error_msg))
return error_msg
def send_dhal_message(
self,
conversation_id: str,
message: str,
stream: bool = False,
on_token: Optional[Callable[[str, str], None]] = None,
on_complete: Optional[Callable[[str, str, Dict[str, Any]], None]] = None,
on_error: Optional[Callable[[str, Exception], None]] = None
) -> None:
"""
Public entry to send a message to the agent with optional streaming callbacks.
Runs in a background thread to avoid blocking the UI thread.
"""
# Add user message to history
self.conversation_history.append(UserMessage(content=message))
messages = self._prepare_messages()
def _worker():
import asyncio
async def _run():
try:
# Delegate to model client with callbacks
resp = await self.model_client.create_chat_completion(
messages=messages,
model=self.config.model,
temperature=self.config.temperature,
max_tokens=self.config.max_tokens,
tools=self.tools if self.tools else None,
stream=stream,
on_token=(lambda req_id, delta: on_token and on_token(conversation_id, delta)),
on_complete=(lambda req_id, full, meta: (
self.conversation_history.append(UserMessage(content=full)),
self._manage_memory(),
on_complete and on_complete(conversation_id, full, meta)
)),
on_error=(lambda req_id, err: on_error and on_error(conversation_id, err))
)
# If not streaming, we need to append and call on_complete here
if not stream and resp and hasattr(resp, "content"):
assistant_message = resp.content
self.conversation_history.append(UserMessage(content=assistant_message))
self._manage_memory()
if on_complete:
on_complete(conversation_id, assistant_message, {"finish_reason": "stop"})
except Exception as e:
if on_error:
on_error(conversation_id, e)
asyncio.run(_run())
import threading as _threading
t = _threading.Thread(target=_worker, daemon=True)
t.start()
async def _handle_function_calls(self, function_calls: List[Dict]) -> str:
"""Handle function calls from the model."""
results = []
for call in function_calls:
func_name = call.get('name')
func_args = call.get('arguments', {})
if func_name in self.function_map:
try:
if asyncio.iscoroutinefunction(self.function_map[func_name]):
result = await self.function_map[func_name](**func_args)
else:
result = self.function_map[func_name](**func_args)
results.append(f"[{func_name}] {result}")
except Exception as e:
results.append(f"[{func_name}] Error: {str(e)}")
else:
results.append(f"[{func_name}] Function not found")
return "\n".join(results)
def _prepare_messages(self) -> List[LLMMessage]:
"""Prepare messages for the model, respecting context limits."""
# For now, return all messages. In production, implement smart truncation
return self.conversation_history.copy()
def _manage_memory(self):
"""Manage conversation memory to stay within limits."""
if len(self.conversation_history) > self.config.memory_limit:
# Keep system message and recent messages
system_msgs = [msg for msg in self.conversation_history if isinstance(msg, SystemMessage)]
recent_msgs = self.conversation_history[-(self.config.memory_limit - len(system_msgs)):]
self.conversation_history = system_msgs + recent_msgs
def get_status(self) -> Dict[str, Any]:
"""Get current status of Hal agent."""
return {
"name": self.config.name,
"active": self.is_active,
"model": self.config.model,
"conversation_length": len(self.conversation_history),
"available_tools": len(self.tools),
"current_task": self.current_task
}
def reset_conversation(self):
"""Reset conversation history while keeping system message."""
system_msgs = [msg for msg in self.conversation_history if isinstance(msg, SystemMessage)]
self.conversation_history = system_msgs
def update_config(self, **kwargs):
"""Update Hal's configuration."""
for key, value in kwargs.items():
if hasattr(self.config, key):
setattr(self.config, key, value)
# Convenience function to create Hal agent
def create_dhal(
name: str = "Dhal",
system_message: str = None,
model: str = "gpt-4",
model_client: ChatCompletionClient = None,
**kwargs
) -> Dhal:
"""Create a Dhal agent with specified configuration."""
if system_message is None:
system_message = f"You are {name}, an advanced AI assistant integrated into DarkHal 2.0. You help users with coding, analysis, security testing, and general AI tasks."
config = DhalConfig(
name=name,
system_message=system_message,
model=model,
**kwargs
)
# Initialize model client if not provided
if model_client is None:
try:
model_client = HalModelClient(model_name=model)
except Exception as e:
print(f"[Dhal] Warning: Could not initialize model client: {e}")
model_client = None
return Dhal(config, model_client)