"""Chat and Agent API routes — Hal chat with Agent system for module creation.""" import json import threading import time import uuid from pathlib import Path from flask import Blueprint, request, jsonify, Response from web.auth import login_required chat_bp = Blueprint('chat', __name__, url_prefix='/api') _agent_runs: dict = {} # run_id -> {'steps': [], 'done': bool, 'stop': threading.Event} _system_prompt = None def _get_system_prompt(): """Load the Hal system prompt from data/hal_system_prompt.txt.""" global _system_prompt if _system_prompt is None: prompt_path = Path(__file__).parent.parent.parent / 'data' / 'hal_system_prompt.txt' if prompt_path.exists(): _system_prompt = prompt_path.read_text(encoding='utf-8') else: _system_prompt = ( "You are Hal, the AI agent for AUTARCH. You can create new modules, " "run shell commands, read and write files. When asked to create a module, " "use the create_module tool." ) return _system_prompt def _ensure_model_loaded(): """Load the LLM model if not already loaded. Returns (llm, error).""" from core.llm import get_llm, LLMError llm = get_llm() if not llm.is_loaded: try: llm.load_model(verbose=False) except LLMError as e: return None, str(e) return llm, None @chat_bp.route('/chat', methods=['POST']) @login_required def chat(): """Handle chat messages — uses Agent system for tool-using tasks, direct chat for simple questions. Streams response via SSE.""" data = request.get_json(silent=True) or {} message = data.get('message', '').strip() if not message: return jsonify({'error': 'No message provided'}) # Always use agent mode so Hal can use tools including create_module run_id = str(uuid.uuid4()) stop_event = threading.Event() steps = [] _agent_runs[run_id] = {'steps': steps, 'done': False, 'stop': stop_event} def worker(): try: from core.agent import Agent from core.tools import get_tool_registry from core.llm import get_llm, LLMError llm = get_llm() if not llm.is_loaded: steps.append({'type': 'status', 'content': 'Loading model...'}) try: llm.load_model(verbose=False) except LLMError as e: steps.append({'type': 'error', 'content': f'Failed to load model: {e}'}) return tools = get_tool_registry() agent = Agent(llm=llm, tools=tools, max_steps=20, verbose=False) # Inject system prompt into agent system_prompt = _get_system_prompt() agent.SYSTEM_PROMPT = system_prompt + "\n\n{tools_description}" def on_step(step): if step.thought: steps.append({'type': 'thought', 'content': step.thought}) if step.tool_name and step.tool_name not in ('task_complete', 'ask_user'): steps.append({'type': 'action', 'content': f"{step.tool_name}({json.dumps(step.tool_args or {})})"}) if step.tool_result: # Truncate long results for display result = step.tool_result if len(result) > 800: result = result[:800] + '...' steps.append({'type': 'result', 'content': result}) result = agent.run(message, step_callback=on_step) if result.success: steps.append({'type': 'answer', 'content': result.summary}) else: steps.append({'type': 'error', 'content': result.error or result.summary}) except Exception as e: steps.append({'type': 'error', 'content': str(e)}) finally: _agent_runs[run_id]['done'] = True threading.Thread(target=worker, daemon=True).start() # Stream the agent steps as SSE def generate(): run = _agent_runs.get(run_id) if not run: yield f"data: {json.dumps({'error': 'Run not found'})}\n\n" return sent = 0 while True: current_steps = run['steps'] while sent < len(current_steps): yield f"data: {json.dumps(current_steps[sent])}\n\n" sent += 1 if run['done']: yield f"data: {json.dumps({'done': True})}\n\n" return time.sleep(0.15) return Response(generate(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) @chat_bp.route('/chat/reset', methods=['POST']) @login_required def chat_reset(): """Clear LLM conversation history.""" try: from core.llm import get_llm llm = get_llm() if hasattr(llm, 'clear_history'): llm.clear_history() elif hasattr(llm, 'reset'): llm.reset() elif hasattr(llm, 'conversation_history'): llm.conversation_history = [] except Exception: pass return jsonify({'ok': True}) @chat_bp.route('/chat/status') @login_required def chat_status(): """Get LLM model status.""" try: from core.llm import get_llm llm = get_llm() return jsonify({ 'loaded': llm.is_loaded, 'model': llm.model_name if llm.is_loaded else None, }) except Exception as e: return jsonify({'loaded': False, 'error': str(e)}) @chat_bp.route('/agent/run', methods=['POST']) @login_required def agent_run(): """Start an autonomous agent run in a background thread. Returns run_id.""" data = request.get_json(silent=True) or {} task = data.get('task', '').strip() if not task: return jsonify({'error': 'No task provided'}) run_id = str(uuid.uuid4()) stop_event = threading.Event() steps = [] _agent_runs[run_id] = {'steps': steps, 'done': False, 'stop': stop_event} def worker(): try: from core.agent import Agent from core.tools import get_tool_registry from core.llm import get_llm, LLMError llm = get_llm() if not llm.is_loaded: try: llm.load_model(verbose=False) except LLMError as e: steps.append({'type': 'error', 'content': f'Failed to load model: {e}'}) return tools = get_tool_registry() agent = Agent(llm=llm, tools=tools, verbose=False) # Inject system prompt system_prompt = _get_system_prompt() agent.SYSTEM_PROMPT = system_prompt + "\n\n{tools_description}" def on_step(step): steps.append({'type': 'thought', 'content': step.thought}) if step.tool_name and step.tool_name not in ('task_complete', 'ask_user'): steps.append({'type': 'action', 'content': f"{step.tool_name}({json.dumps(step.tool_args or {})})"}) if step.tool_result: steps.append({'type': 'result', 'content': step.tool_result[:800]}) agent.run(task, step_callback=on_step) except Exception as e: steps.append({'type': 'error', 'content': str(e)}) finally: _agent_runs[run_id]['done'] = True threading.Thread(target=worker, daemon=True).start() return jsonify({'run_id': run_id}) @chat_bp.route('/agent/stream/') @login_required def agent_stream(run_id): """SSE stream of agent steps for a given run_id.""" def generate(): run = _agent_runs.get(run_id) if not run: yield f"data: {json.dumps({'error': 'Run not found'})}\n\n" return sent = 0 while True: current_steps = run['steps'] while sent < len(current_steps): yield f"data: {json.dumps(current_steps[sent])}\n\n" sent += 1 if run['done']: yield f"data: {json.dumps({'done': True})}\n\n" return time.sleep(0.15) return Response(generate(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) @chat_bp.route('/agent/stop/', methods=['POST']) @login_required def agent_stop(run_id): """Signal a running agent to stop.""" run = _agent_runs.get(run_id) if run: run['stop'].set() run['done'] = True return jsonify({'stopped': bool(run)})