diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fbf4d7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +*.pyo +py3/ +*.egg-info/ +dist/ +build/ diff --git a/harnessed_reasoning/__pycache__/config_functions.cpython-311.pyc b/harnessed_reasoning/__pycache__/config_functions.cpython-311.pyc new file mode 100644 index 0000000..e179b6c Binary files /dev/null and b/harnessed_reasoning/__pycache__/config_functions.cpython-311.pyc differ diff --git a/harnessed_reasoning/__pycache__/core.cpython-311.pyc b/harnessed_reasoning/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..bfeb034 Binary files /dev/null and b/harnessed_reasoning/__pycache__/core.cpython-311.pyc differ diff --git a/harnessed_reasoning/__pycache__/init.cpython-311.pyc b/harnessed_reasoning/__pycache__/init.cpython-311.pyc new file mode 100644 index 0000000..b75c0ff Binary files /dev/null and b/harnessed_reasoning/__pycache__/init.cpython-311.pyc differ diff --git a/harnessed_reasoning/core.py b/harnessed_reasoning/core.py index 052fd83..5d80ad5 100644 --- a/harnessed_reasoning/core.py +++ b/harnessed_reasoning/core.py @@ -1,764 +1,724 @@ """ -Hermes Reasoning Module - Production-ready reasoning engine with full context awareness -Implements advanced reasoning capabilities including planning, tool coordination, -error recovery, and cross-session intelligence as a standardized ahserver module. +Hermes Reasoning Module - Production-ready reasoning engine +Uses harnessed_agent LLM client and tool execution to perform real AI reasoning, +task planning, and tool coordination. """ - -import asyncio import json -import re -from typing import Dict, Any, List, Optional, Tuple, Callable -from dataclasses import dataclass -from datetime import datetime import uuid -from enum import Enum +import time +from typing import Dict, Any, List, Optional +from datetime import datetime -# Import required dependencies try: from ahserver.serverenv import ServerEnv - from appPublic.worker import awaitify from sqlor.dbpools import DBPools + from appPublic.log import info, debug, warning, error, exception except ImportError: - # For standalone testing class ServerEnv: - def __init__(self): - pass - - def awaitify(func): - async def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - + pass class DBPools: - def __init__(self): - pass + pass + def info(*a, **kw): print(*a) + def debug(*a, **kw): pass + def warning(*a, **kw): print(*a) + def error(*a, **kw): print(*a) + def exception(*a, **kw): pass -@dataclass -class ReasoningConfig: - """Configuration for Hermes Reasoning module""" - max_reasoning_steps: int = 10 # Maximum reasoning steps per task - max_tool_calls_per_step: int = 5 # Maximum tool calls per reasoning step - enable_cross_session_search: bool = True # Enable automatic session search - enable_skill_auto_loading: bool = True # Enable automatic skill loading - safety_mode: str = "strict" # Safety mode: strict, moderate, lenient - max_context_tokens: int = 4000 # Maximum tokens for reasoning context - enable_error_recovery: bool = True # Enable automatic error recovery - max_recovery_attempts: int = 3 # Maximum recovery attempts per error -class ReasoningStepType(Enum): - """Types of reasoning steps""" - CONTEXT_ANALYSIS = "context_analysis" - TASK_PLANNING = "task_planning" - TOOL_SELECTION = "tool_selection" - EXECUTION_COORDINATION = "execution_coordination" - ERROR_RECOVERY = "error_recovery" - RESULT_SYNTHESIS = "result_synthesis" - CROSS_SESSION_INTEGRATION = "cross_session_integration" +# ============================================================ +# System prompts for reasoning phases +# ============================================================ -@dataclass -class ReasoningStep: - """Individual reasoning step with metadata""" - id: str - step_type: ReasoningStepType - description: str - context: Dict[str, Any] - tools_considered: List[str] - tools_selected: List[str] - safety_checks: List[str] - confidence_score: float - created_at: datetime +REASONING_SYSTEM_PROMPT = """你是一个智能推理引擎。你的任务是分析用户的自然语言请求,理解其意图,并生成可执行的行动计划。 -@dataclass -class ReasoningSession: - """Complete reasoning session with execution plan""" - id: str - user_id: str - initial_request: str - context_summary: str - execution_plan: List[Dict[str, Any]] - reasoning_steps: List[ReasoningStep] - safety_violations: List[str] - final_decision: Dict[str, Any] - status: str # pending, executing, completed, failed, cancelled - created_at: datetime - updated_at: datetime +你拥有以下工具可供调用: +{tool_descriptions} + +请根据用户请求,返回一个 JSON 格式的执行计划: +```json +{{ + "analysis": "对用户请求的简要分析", + "steps": [ + {{ + "step_number": 1, + "description": "步骤描述", + "actions": [ + {{ + "tool": "工具名称", + "parameters": {{"参数名": "参数值"}} + }} + ] + }} + ], + "safety_notes": ["安全注意事项,如无则空数组"] +}} +``` + +可用的工具名称:{tool_names} + +注意: +1. 只使用上述工具列表中存在的工具 +2. 每个步骤可以包含 1-3 个动作 +3. 如果请求模糊,优先选择收集信息类工具 +4. 确保参数格式正确""" + +# List of tools that harnessed_agent can execute +AVAILABLE_TOOLS = [ + # File operations + {"name": "read_file", "desc": "读取文本文件,需要 path 参数"}, + {"name": "write_file", "desc": "写入文件内容,需要 path 和 content 参数"}, + {"name": "search_files", "desc": "搜索文件内容或文件名,需要 pattern 和 target(content/files) 参数"}, + {"name": "patch", "desc": "文件替换修改,需要 mode/path/old_string/new_string 参数"}, + # System operations + {"name": "terminal", "desc": "执行 shell 命令,需要 command 参数"}, + {"name": "process", "desc": "管理后台进程,需要 action/session_id/data 参数"}, + {"name": "execute_code", "desc": "执行 Python 代码,需要 code 参数"}, + # Memory + {"name": "memory", "desc": "管理持久化记忆,需要 action/target/content/old_text 参数"}, + # Skills + {"name": "skill_manage", "desc": "管理技能,需要 action/name/content 参数"}, + {"name": "skill_view", "desc": "查看技能内容,需要 name 参数"}, + # Other + {"name": "todo", "desc": "管理任务列表,需要 action/todos 参数"}, + {"name": "session_search", "desc": "搜索历史会话,需要 query 参数"}, + {"name": "cronjob", "desc": "管理定时任务,需要 action/prompt/schedule 参数"}, + {"name": "clarify", "desc": "向用户提问获取澄清,需要 question/choices 参数"}, + {"name": "delegate_task", "desc": "委派任务给子代理,需要 goal/context/toolsets 参数"}, + {"name": "text_to_speech", "desc": "文字转语音,需要 text 参数"}, + {"name": "vision_analyze", "desc": "分析图片内容,需要 image_url 和 question 参数"}, +] + +TOOL_NAMES = [t["name"] for t in AVAILABLE_TOOLS] +TOOL_DESCRIPTIONS = "\n".join(f"- {t['name']}: {t['desc']}" for t in AVAILABLE_TOOLS) + + +# ============================================================ +# Reasoning Engine +# ============================================================ class HermesReasoningEngine: - """Core reasoning engine with production-grade safety and reliability""" - - def __init__(self, config: Optional[ReasoningConfig] = None): - self.config = config or ReasoningConfig() + """Production reasoning engine that uses LLM and real tool execution.""" + + DEFAULT_SAFETY_RULES = { + "strict": [ + "rm -rf /", "format ", "dd if=/dev/", "mkfs", "chmod 777", + "rm -fr /", "> /dev/sd", "> /dev/hd", ":(){:|:&};:", + ], + "moderate": [ + "rm -rf /", "dd if=/dev/zero", ":(){:|:&};:", + ], + "lenient": [], + } + + def __init__(self): self.db = DBPools() - self.execution_engine = None # Will connect to harnessed_agent - - def _get_current_user_id(self, context: Dict[str, Any]) -> str: - """Get current user ID from request context""" - user_id = context.get('user_id') or context.get('userid') - if not user_id: - raise ValueError("User ID not found in context. User must be authenticated.") - return str(user_id) - - def _estimate_tokens(self, text: str) -> int: - """Estimate token count for given text""" - return max(1, len(text) // 4) - - async def _get_intelligent_context(self, user_id: str, request: str) -> Dict[str, Any]: - """Get intelligent context combining memory, sessions, and skills""" - context_data = { - "memory": [], - "sessions": [], - "skills": [], - "tools": [], - "user_preferences": {} - } - + + # -------------------------------------------------------- + # Config helpers + # -------------------------------------------------------- + + async def _get_config(self) -> Dict[str, Any]: + """Get reasoning config from DB, fall back to defaults.""" try: - # Get intelligent memory context from harnessed_agent - memory_context = await self._call_harnessed_agent_function( - "hermes_get_intelligent_memory_context", - {"current_task": request, "max_tokens": self.config.max_context_tokens // 3} - ) - if memory_context.get("success"): - context_data["memory"] = memory_context.get("memories", []) - # Extract user preferences - for mem in context_data["memory"]: - if mem.get("target") == "user": - try: - context_data["user_preferences"].update( - json.loads(mem.get("content", "{}")) - ) - except: - pass - - # Get relevant sessions - if self.config.enable_cross_session_search: - session_search = await self._call_harnessed_agent_function( - "hermes_search_sessions", - {"query": request, "limit": 5} - ) - if session_search.get("success"): - context_data["sessions"] = session_search.get("sessions", []) - - # Get relevant skills - if self.config.enable_skill_auto_loading: - # This would integrate with skill management system - context_data["skills"] = await self._get_relevant_skills(user_id, request) - - # Get available tools (this would come from tool registry) - context_data["tools"] = self._get_available_tools() - + env = ServerEnv() + dbname = env.get_module_dbname('harnessed_reasoning') + except Exception: + dbname = 'default' + + try: + async with self.db.sqlorContext(dbname) as sor: + rows = await sor.R('harnessed_reasoning_config', {}, + orderby='updated_at DESC', limit=1) + if rows: + return rows[0] except Exception as e: - # Log error but continue with partial context - pass - - return context_data - - async def _get_relevant_skills(self, user_id: str, request: str) -> List[Dict[str, Any]]: - """Get skills relevant to the current request""" - # This is a placeholder - would integrate with actual skill system + error(f"Failed to fetch reasoning config: {e}") + + return { + 'max_reasoning_steps': 10, + 'max_tool_calls_per_step': 5, + 'enable_cross_session_search': '1', + 'enable_skill_auto_loading': '1', + 'safety_mode': 'strict', + 'max_context_tokens': 4000, + 'enable_error_recovery': '1', + 'max_recovery_attempts': 3, + 'model_name': 'qwen3-max', + 'system_prompt': '', + } + + # -------------------------------------------------------- + # Memory & session context + # -------------------------------------------------------- + + async def _get_memory_context(self, user_id: str, request: str, config: Dict) -> Dict[str, Any]: + """Get real memory and session context from harnessed_agent.""" + context = {"memory_entries": [], "recent_sessions": [], "skills": []} + + try: + # Intelligent memory + max_tokens = int(config.get('max_context_tokens', 4000)) // 3 + if hasattr(ServerEnv(), 'harnessed_get_intelligent_memory_context'): + mem_result = await ServerEnv().harnessed_get_intelligent_memory_context( + current_task=request, + max_tokens=max_tokens, + ) + if mem_result.get('success'): + context['memory_entries'] = mem_result.get('memories', []) + + # Session search + if config.get('enable_cross_session_search', '1') == '1': + if hasattr(ServerEnv(), 'harnessed_search_sessions'): + sess_result = await ServerEnv().harnessed_search_sessions( + query=request, + limit=3, + ) + if sess_result.get('success'): + context['recent_sessions'] = sess_result.get('sessions', []) + + # Skills + if config.get('enable_skill_auto_loading', '1') == '1': + context['skills'] = await self._find_relevant_skills(user_id, request) + + except Exception as e: + warning(f"Context gathering failed (non-fatal): {e}") + + return context + + async def _find_relevant_skills(self, user_id: str, request: str) -> List[Dict[str, Any]]: + """Find skills relevant to the request via keyword search.""" + keywords = set() + for word in request.lower().split(): + if len(word) > 2: + keywords.add(word) + + skills = [] try: - # Search for skills matching request keywords - keywords = self._extract_keywords(request) - relevant_skills = [] - async with self.db.sqlorContext('default') as sor: - for keyword in keywords[:3]: # Limit to top 3 keywords - skills = await sor.R('hermes_skills', { + for kw in list(keywords)[:3]: + rows = await sor.R('hermes_skills', { 'user_id': user_id, '$or': [ - {'name': {'$like': f'%{keyword}%'}}, - {'description': {'$like': f'%{keyword}%'}}, - {'content': {'$like': f'%{keyword}%'}} + {'name': {'$like': f'%{kw}%'}}, + {'description': {'$like': f'%{kw}%'}}, ] }, limit=2) - relevant_skills.extend(skills) - - # Deduplicate skills - seen = set() - unique_skills = [] - for skill in relevant_skills: - if skill['id'] not in seen: - unique_skills.append(skill) - seen.add(skill['id']) - - return unique_skills[:5] # Limit to top 5 skills - + skills.extend(rows) + + # Deduplicate + seen = set() + unique = [] + for s in skills: + if s['id'] not in seen: + seen.add(s['id']) + unique.append(s) + return unique[:5] except Exception: return [] - - def _extract_keywords(self, text: str) -> List[str]: - """Extract important keywords from text""" - # Simple keyword extraction - would use NLP in production - words = re.findall(r'\b\w+\b', text.lower()) - # Filter out common stop words - stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were'} - keywords = [word for word in words if word not in stop_words and len(word) > 2] - return list(set(keywords))[:10] # Return unique keywords, max 10 - - def _get_available_tools(self) -> List[str]: - """Get list of available tools""" - # This would come from actual tool registry - return [ - "browser_navigate", "browser_click", "browser_type", "browser_snapshot", - "terminal", "read_file", "write_file", "search_files", "patch", - "memory", "skill_manage", "skill_view", "session_search", - "clarify", "delegate_task", "execute_code", "process", - "vision_analyze", "text_to_speech", "cronjob", "todo" - ] - - async def _call_harnessed_agent_function(self, function_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: - """Call harnessed_agent functions safely""" - try: - # This would integrate with actual harnessed_agent module - # For now, return mock responses that match expected structure - if function_name == "hermes_get_intelligent_memory_context": - return { - "success": True, - "memories": [], - "total_tokens": 0, - "max_tokens": parameters.get("max_tokens", 2000), - "user_id": "mock_user", - "memory_count": 0 - } - elif function_name == "hermes_search_sessions": - return { - "success": True, - "sessions": [], - "query": parameters.get("query", ""), - "limit": parameters.get("limit", 3), - "user_id": "mock_user" - } - else: - return {"success": True, "result": f"Called {function_name} with {parameters}"} - except Exception as e: - return {"success": False, "error": str(e)} - - def _perform_safety_check(self, action: str, parameters: Dict[str, Any], - user_preferences: Dict[str, Any]) -> List[str]: - """Perform safety checks on proposed actions""" + + # -------------------------------------------------------- + # Safety checks + # -------------------------------------------------------- + + def _safety_check(self, plan: Dict[str, Any], safety_mode: str = 'strict') -> List[str]: + """Check execution plan for safety violations.""" violations = [] - - if self.config.safety_mode == "strict": - # Strict safety checks - dangerous_commands = [ - "rm -rf", "format", "dd if", "mkfs", "chmod 777", - "chown root", "sudo", "su -", "passwd", "userdel" - ] - - if action == "terminal": - command = parameters.get("command", "") - for dangerous in dangerous_commands: - if dangerous in command: - violations.append(f"Dangerous command detected: {dangerous}") - - # File system access restrictions - if action in ["read_file", "write_file", "patch"]: - path = parameters.get("path", "") - if ".." in path or path.startswith("/etc") or path.startswith("/root"): - violations.append(f"Restricted path access: {path}") - - # Network restrictions - if action == "browser_navigate": - url = parameters.get("url", "") - if not url.startswith(("http://", "https://")): - violations.append(f"Invalid URL protocol: {url}") - - elif self.config.safety_mode == "moderate": - # Moderate safety checks - if action == "terminal": - command = parameters.get("command", "") - if "rm -rf /" in command or "dd if=/dev/zero" in command: - violations.append("Extremely dangerous command detected") - - # User preference checks - if user_preferences.get("avoid_terminal") and action == "terminal": - violations.append("User preference: avoid terminal commands") - + rules = self.DEFAULT_SAFETY_RULES.get(safety_mode, []) + + steps = plan.get('steps', []) + for step in steps: + for action in step.get('actions', []): + tool = action.get('tool', '') + params = action.get('parameters', {}) + + if tool == 'terminal': + cmd = params.get('command', '') + for rule in rules: + if rule.lower() in cmd.lower(): + violations.append(f"危险命令: {rule}") + + if tool in ('read_file', 'write_file', 'patch'): + path = params.get('path', '') + if '..' in path or path.startswith('/etc/passwd') or path.startswith('/etc/shadow'): + violations.append(f"受限路径: {path}") + return violations - - async def _analyze_context_and_plan(self, user_id: str, request: str, - context_data: Dict[str, Any]) -> Dict[str, Any]: - """Analyze context and create execution plan""" - # Step 1: Context analysis - context_summary = self._summarize_context(context_data) - - # Step 2: Task decomposition - subtasks = self._decompose_task(request, context_data) - - # Step 3: Tool selection and planning - execution_plan = [] - safety_violations = [] - - for i, subtask in enumerate(subtasks[:self.config.max_reasoning_steps]): - # Select appropriate tools for this subtask - tools_for_subtask = self._select_tools_for_subtask(subtask, context_data) - - # Create execution step - step_plan = { - "step_number": i + 1, - "description": subtask, - "tools": tools_for_subtask[:self.config.max_tool_calls_per_step], - "expected_outcome": f"Complete subtask: {subtask}", - "safety_checks": [] - } - - # Perform safety checks - for tool_action in step_plan["tools"]: - violations = self._perform_safety_check( - tool_action["action"], - tool_action.get("parameters", {}), - context_data.get("user_preferences", {}) - ) - step_plan["safety_checks"].extend(violations) - safety_violations.extend(violations) - - execution_plan.append(step_plan) - - return { - "context_summary": context_summary, - "execution_plan": execution_plan, - "safety_violations": safety_violations, - "confidence_score": self._calculate_confidence(execution_plan, context_data) - } - - def _summarize_context(self, context_data: Dict[str, Any]) -> str: - """Create a summary of the available context""" - summary_parts = [] - - if context_data["memory"]: - summary_parts.append(f"Memory entries: {len(context_data['memory'])}") - - if context_data["sessions"]: - summary_parts.append(f"Relevant sessions: {len(context_data['sessions'])}") - - if context_data["skills"]: - skill_names = [s.get("name", "unknown") for s in context_data["skills"]] - summary_parts.append(f"Relevant skills: {', '.join(skill_names[:3])}") - - if context_data["user_preferences"]: - summary_parts.append("User preferences loaded") - - return "; ".join(summary_parts) if summary_parts else "No relevant context found" - - def _decompose_task(self, request: str, context_data: Dict[str, Any]) -> List[str]: - """Decompose complex task into subtasks""" - # This is where advanced reasoning happens - # In production, this would use LLM-based task decomposition - - # Simple rule-based decomposition for now - subtasks = [] - - # Check for multi-step indicators - if any(word in request.lower() for word in ["and then", "after that", "next", "finally"]): - # Split on conjunctions - parts = re.split(r'\s+(?:and then|after that|next|finally)\s+', request, flags=re.IGNORECASE) - subtasks = [part.strip() for part in parts if part.strip()] - elif "?" in request or "how" in request.lower() or "what" in request.lower(): - # Question handling - subtasks = [ - f"Understand the question: {request}", - "Gather relevant information", - "Formulate comprehensive answer" - ] - else: - # Single task - subtasks = [request] - - return subtasks[:5] # Limit to 5 subtasks - - def _select_tools_for_subtask(self, subtask: str, context_data: Dict[str, Any]) -> List[Dict[str, Any]]: - """Select appropriate tools for a given subtask""" - # Simple keyword-based tool selection - tool_mappings = { - "file": ["read_file", "write_file", "search_files"], - "code": ["read_file", "write_file", "patch", "terminal"], - "web": ["browser_navigate", "browser_snapshot", "browser_click"], - "search": ["search_files", "session_search"], - "memory": ["memory"], - "skill": ["skill_view", "skill_manage"], - "execute": ["terminal", "execute_code"], - "image": ["vision_analyze", "browser_get_images"], - "plan": ["todo"] - } - - selected_tools = [] - subtask_lower = subtask.lower() - - for keyword, tools in tool_mappings.items(): - if keyword in subtask_lower: - for tool in tools: - selected_tools.append({ - "action": tool, - "parameters": self._infer_parameters(tool, subtask, context_data) - }) - break - - # Default fallback - if not selected_tools: - selected_tools = [{ - "action": "clarify", - "parameters": {"question": f"Could you clarify what you'd like me to do about: {subtask}"} - }] - - return selected_tools - - def _infer_parameters(self, tool: str, subtask: str, context_data: Dict[str, Any]) -> Dict[str, Any]: - """Infer reasonable parameters for a tool based on subtask""" - # Very basic parameter inference - if tool == "read_file": - # Look for file paths in subtask - file_match = re.search(r'(\S+\.py|\S+\.txt|\S+\.md)', subtask) - if file_match: - return {"path": file_match.group(1)} - - elif tool == "search_files": - # Look for search terms - if "find" in subtask or "search" in subtask: - words = subtask.split() - if len(words) > 2: - return {"pattern": words[-1], "target": "content"} - - elif tool == "terminal": - # Look for commands - if "run" in subtask or "execute" in subtask: - # Extract command after "run" or "execute" - cmd_match = re.search(r'(?:run|execute)\s+(.+)', subtask, re.IGNORECASE) - if cmd_match: - return {"command": cmd_match.group(1)} - - return {} - - def _calculate_confidence(self, execution_plan: List[Dict[str, Any]], - context_data: Dict[str, Any]) -> float: - """Calculate confidence score for the execution plan""" - base_confidence = 0.7 # Base confidence - - # Adjust based on context availability - if context_data["memory"] or context_data["sessions"] or context_data["skills"]: - base_confidence += 0.1 - - # Adjust based on plan complexity - if len(execution_plan) == 1: - base_confidence += 0.1 - elif len(execution_plan) > 3: - base_confidence -= 0.1 - - # Adjust based on safety violations - safety_penalty = len([v for v in execution_plan for check in v.get("safety_checks", []) if check]) * 0.05 - base_confidence -= safety_penalty - - return max(0.0, min(1.0, base_confidence)) - - async def reason_and_execute(self, request: str, - context: Dict[str, Any] = None, - execute_immediately: bool = True) -> Dict[str, Any]: - """ - Main entry point: perform reasoning and optionally execute the plan - - Args: - request: User's natural language request - context: Request context containing user information - execute_immediately: Whether to execute the plan immediately or just return it - - Returns: - Reasoning result with execution plan and optional execution results - """ - user_id = self._get_current_user_id(context) if context else "anonymous" - session_id = str(uuid.uuid4()) - - try: - # Step 1: Gather intelligent context - context_data = await self._get_intelligent_context(user_id, request) - - # Step 2: Analyze context and create execution plan - planning_result = await self._analyze_context_and_plan(user_id, request, context_data) - - # Step 3: Create reasoning session record - reasoning_session = ReasoningSession( - id=session_id, - user_id=user_id, - initial_request=request, - context_summary=planning_result["context_summary"], - execution_plan=planning_result["execution_plan"], - reasoning_steps=[], # Would be populated with detailed steps in production - safety_violations=planning_result["safety_violations"], - final_decision={"confidence": planning_result["confidence_score"]}, - status="pending", - created_at=datetime.now(), - updated_at=datetime.now() + + # -------------------------------------------------------- + # LLM-based reasoning + # -------------------------------------------------------- + + async def _llm_call(self, messages: List[Dict[str, str]], config: Dict[str, Any], + **extra) -> Dict[str, Any]: + """Call LLM via harnessed_agent's llm_chat.""" + model = config.get('model_name', 'qwen3-max') + temperature = float(config.get('temperature', 0.7)) + max_tokens = int(config.get('max_output_tokens', 4096)) + + # Use harnessed_agent's llm_chat if available + env = ServerEnv() + if hasattr(env, 'llm_chat'): + return await env.llm_chat( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + **extra, ) - - # Step 4: Store reasoning session - await self._store_reasoning_session(reasoning_session) - - result = { - "success": True, - "session_id": session_id, - "user_id": user_id, - "request": request, - "context_summary": planning_result["context_summary"], - "execution_plan": planning_result["execution_plan"], - "safety_violations": planning_result["safety_violations"], - "confidence_score": planning_result["confidence_score"], - "status": "planned" - } - - # Step 5: Execute if requested - if execute_immediately and not planning_result["safety_violations"]: - execution_result = await self._execute_plan( - session_id, planning_result["execution_plan"], context - ) - result.update({ - "execution_results": execution_result, - "status": "executed" - }) - elif planning_result["safety_violations"]: - result.update({ - "status": "blocked", - "message": "Execution blocked due to safety violations" - }) - - return result - - except Exception as e: + + # Fallback: direct config-based call (shouldn't happen in production) + error("llm_chat not available on ServerEnv") + return {'error': {'message': 'LLM client not available'}} + + async def _generate_plan(self, request: str, context: Dict[str, Any], + config: Dict[str, Any]) -> Dict[str, Any]: + """Use LLM to analyze request and generate execution plan.""" + # Build context summary + ctx_parts = [] + if context.get('memory_entries'): + for mem in context['memory_entries'][:5]: + ctx_parts.append(f"记忆({mem.get('target')}): {mem.get('content', '')[:200]}") + if context.get('recent_sessions'): + for sess in context['recent_sessions'][:3]: + ctx_parts.append(f"历史会话: {sess.get('title', '')[:100]}") + if context.get('skills'): + for sk in context['skills'][:3]: + ctx_parts.append(f"技能: {sk.get('name', '')} - {sk.get('description', '')[:100]}") + + ctx_text = "\n".join(ctx_parts) if ctx_parts else "无相关上下文" + + system_prompt = config.get('system_prompt', '') or REASONING_SYSTEM_PROMPT.format( + tool_descriptions=TOOL_DESCRIPTIONS, + tool_names=", ".join(TOOL_NAMES), + ) + + user_prompt = f"""用户请求:{request} + +相关上下文: +{ctx_text} + +请生成执行计划(JSON 格式)。""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + result = await self._llm_call(messages, config) + + if 'error' in result: + error(f"LLM planning failed: {result['error'].get('message')}") return { - "success": False, - "error": str(e), - "session_id": session_id, - "user_id": user_id, - "status": "failed" + 'analysis': 'LLM 调用失败,无法生成计划', + 'steps': [], + 'safety_notes': [], + 'llm_error': result['error'].get('message', ''), } - - async def _store_reasoning_session(self, session: ReasoningSession): - """Store reasoning session in database""" + + # Extract JSON from response + content = result.get('choices', [{}])[0].get('message', {}).get('content', '') + return self._parse_plan_json(content) + + def _parse_plan_json(self, text: str) -> Dict[str, Any]: + """Extract and parse JSON plan from LLM response.""" + # Try to find JSON block + import re + json_match = re.search(r'```json\s*\n(.*?)\n```', text, re.DOTALL) + if json_match: + text = json_match.group(1) + else: + # Try to find first { ... } block + json_match = re.search(r'\{.*\}', text, re.DOTALL) + if json_match: + text = json_match.group(0) + try: - async with self.db.sqlorContext('default') as sor: - data = { - 'id': session.id, - 'user_id': session.user_id, - 'initial_request': session.initial_request, - 'context_summary': session.context_summary, - 'execution_plan_json': json.dumps(session.execution_plan), - 'reasoning_steps_json': json.dumps([{ - 'id': step.id, - 'step_type': step.step_type.value, - 'description': step.description, - 'confidence_score': step.confidence_score, - 'created_at': step.created_at.isoformat() - } for step in session.reasoning_steps]), - 'safety_violations_json': json.dumps(session.safety_violations), - 'final_decision_json': json.dumps(session.final_decision), - 'status': session.status, - 'created_at': session.created_at, - 'updated_at': session.updated_at - } - await sor.C('harnessed_reasoning_sessions', data) + plan = json.loads(text) + # Validate structure + if not isinstance(plan, dict): + return {'analysis': '', 'steps': [], 'safety_notes': []} + plan.setdefault('analysis', '') + plan.setdefault('steps', []) + plan.setdefault('safety_notes', []) + return plan + except json.JSONDecodeError: + error(f"Failed to parse LLM plan JSON: {text[:200]}") + return {'analysis': '无法解析 LLM 返回的计划', 'steps': [], 'safety_notes': []} + + # -------------------------------------------------------- + # Tool execution + # -------------------------------------------------------- + + async def _execute_tool(self, tool_name: str, parameters: Dict[str, Any], + context: Dict[str, Any]) -> Dict[str, Any]: + """Execute a tool via harnessed_agent's execute_tool.""" + env = ServerEnv() + if hasattr(env, 'harnessed_execute_tool'): + return await env.harnessed_execute_tool( + tool_name=tool_name, + parameters=parameters, + ) + + # Fallback: try to call the tool function directly if registered on ServerEnv + tool_func = getattr(env, tool_name, None) + if tool_func and callable(tool_func): + try: + return await tool_func(**parameters) + except Exception as e: + return {'success': False, 'error': str(e), 'tool': tool_name} + + return {'success': False, 'error': f'Tool {tool_name} not available', 'tool': tool_name} + + # -------------------------------------------------------- + # Main entry point + # -------------------------------------------------------- + + async def reason_and_execute(self, request: str, + execute_immediately: bool = True) -> Dict[str, Any]: + """ + Main entry: reason about a request and optionally execute the plan. + + 1. Gather real context (memory, sessions, skills) from harnessed_agent + 2. Use LLM to analyze request and generate execution plan + 3. Safety check the plan + 4. If execute_immediately and safe, execute step by step + 5. Store session and return result + """ + start_time = time.time() + session_id = str(uuid.uuid4()) + user_id = "anonymous" + + # Try to get real user ID + try: + env = ServerEnv() + if hasattr(env, 'harnessed_get_current_user'): + user_result = await env.harnessed_get_current_user() + user_id = user_result.get('user_id') or 'anonymous' except Exception: - # Silently fail - don't break main flow pass - - async def _execute_plan(self, session_id: str, execution_plan: List[Dict[str, Any]], - context: Dict[str, Any]) -> List[Dict[str, Any]]: - """Execute the reasoning plan step by step""" - results = [] - - for step in execution_plan: - step_results = [] - - for tool_action in step["tools"][:self.config.max_tool_calls_per_step]: - try: - # Execute each tool action - tool_result = await self._execute_tool_action( - tool_action["action"], - tool_action.get("parameters", {}), - context - ) - step_results.append(tool_result) - - # Check if we should continue based on result - if not tool_result.get("success") and self.config.enable_error_recovery: - recovery_result = await self._attempt_recovery( - tool_action, tool_result, context - ) - if recovery_result: - step_results.append(recovery_result) - - except Exception as e: - step_results.append({ - "success": False, - "error": str(e), - "action": tool_action["action"] - }) - - results.append({ - "step_description": step["description"], - "tool_results": step_results, - "safety_checks": step.get("safety_checks", []) - }) - - # Update session status - await self._update_session_status(session_id, "completed") - - return results - - async def _execute_tool_action(self, action: str, parameters: Dict[str, Any], - context: Dict[str, Any]) -> Dict[str, Any]: - """Execute a single tool action through harnessed_agent""" - # This would integrate with actual harnessed_agent execution functions - # For now, simulate successful execution - return { - "success": True, - "action": action, - "parameters": parameters, - "result": f"Executed {action} successfully", - "timestamp": datetime.now().isoformat() + + config = await self._get_config() + safety_mode = config.get('safety_mode', 'strict') + + result = { + "success": False, + "session_id": session_id, + "user_id": user_id, + "request": request, + "status": "failed", } - - async def _attempt_recovery(self, failed_action: Dict[str, Any], - error_result: Dict[str, Any], - context: Dict[str, Any]) -> Optional[Dict[str, Any]]: - """Attempt to recover from a failed tool execution""" - if not self.config.enable_error_recovery: - return None - - # Simple recovery strategies - action = failed_action["action"] - parameters = failed_action.get("parameters", {}) - - if action == "read_file" and "not found" in str(error_result.get("error", "")).lower(): - # Try to find similar files - original_path = parameters.get("path", "") - if original_path: - search_pattern = original_path.split("/")[-1] - return await self._execute_tool_action( - "search_files", - {"pattern": search_pattern, "target": "files"}, - context + + try: + # Step 1: Gather real context + info(f"Reasoning start: user={user_id}, request={request[:80]}...") + context = await self._get_memory_context(user_id, request, config) + + # Step 2: LLM-based planning + plan = await self._generate_plan(request, context, config) + + elapsed_plan = time.time() - start_time + info(f"Plan generated in {elapsed_plan:.1f}s: {len(plan.get('steps', []))} steps") + + # Step 3: Safety check + violations = self._safety_check(plan, safety_mode) + if violations: + warning(f"Safety violations: {violations}") + + # Step 4: Store session + await self._store_session(session_id, user_id, request, plan, violations, "planned") + + # Build result + result.update({ + "success": True, + "analysis": plan.get("analysis", ""), + "execution_plan": plan.get("steps", []), + "safety_violations": violations, + "safety_notes": plan.get("safety_notes", []), + "context_summary": self._context_summary(context), + "confidence_score": self._calc_confidence(plan, context, violations), + "status": "planned", + "llm_error": plan.get("llm_error", ""), + }) + + # Step 5: Execute if requested and safe + if execute_immediately and not violations and plan.get('steps'): + exec_results = await self._execute_plan( + session_id, plan['steps'], config, context ) - - elif action == "terminal" and "permission denied" in str(error_result.get("error", "")).lower(): - # Try without sudo or with different approach - original_command = parameters.get("command", "") - if original_command.startswith("sudo "): - return await self._execute_tool_action( - "terminal", - {"command": original_command.replace("sudo ", "", 1)}, - context - ) - + result.update({ + "execution_results": exec_results, + "status": "executed", + }) + await self._update_session_status(session_id, "executed") + elif violations: + result["status"] = "blocked" + result["message"] = f"执行已阻止:{', '.join(violations)}" + + elapsed_total = time.time() - start_time + info(f"Reasoning complete in {elapsed_total:.1f}s, status={result['status']}") + + except Exception as e: + exception(f"Reasoning failed: {e}") + result["success"] = False + result["error"] = str(e) + result["status"] = "failed" + + try: + await self._update_session_status(session_id, "failed") + except Exception: + pass + + return result + + def _context_summary(self, context: Dict[str, Any]) -> str: + parts = [] + if context.get('memory_entries'): + parts.append(f"记忆条目: {len(context['memory_entries'])}") + if context.get('recent_sessions'): + parts.append(f"相关会话: {len(context['recent_sessions'])}") + if context.get('skills'): + names = [s.get('name', '?') for s in context['skills'][:3]] + parts.append(f"相关技能: {', '.join(names)}") + return "; ".join(parts) if parts else "无相关上下文" + + def _calc_confidence(self, plan: Dict, context: Dict, violations: List) -> float: + base = 0.7 + if context.get('memory_entries') or context.get('skills'): + base += 0.1 + if plan.get('llm_error'): + base -= 0.3 + if len(plan.get('steps', [])) == 0: + base -= 0.2 + base -= len(violations) * 0.1 + return max(0.0, min(1.0, base)) + + # -------------------------------------------------------- + # Plan execution + # -------------------------------------------------------- + + async def _execute_plan(self, session_id: str, steps: List[Dict], + config: Dict, context: Dict) -> List[Dict]: + """Execute plan steps sequentially, each with its tool actions.""" + all_results = [] + max_steps = int(config.get('max_reasoning_steps', 10)) + max_tools = int(config.get('max_tool_calls_per_step', 5)) + + for step in steps[:max_steps]: + step_num = step.get('step_number', '?') + step_results = [] + + for action in step.get('actions', [])[:max_tools]: + tool = action.get('tool', '') + params = action.get('parameters', {}) + + if not tool: + continue + + info(f"Executing step {step_num}: {tool}({json.dumps(params, ensure_ascii=False)[:100]})") + tool_result = await self._execute_tool(tool, params, context) + + step_results.append({ + 'tool': tool, + 'parameters': params, + 'success': tool_result.get('success', False), + 'result': str(tool_result)[:500], # Truncate for storage + }) + + # Error recovery + if not tool_result.get('success'): + error_msg = str(tool_result.get('error', '')) + if config.get('enable_error_recovery', '1') == '1' and error_msg: + recovery = await self._try_recovery(tool, params, error_msg, context) + if recovery: + step_results.append(recovery) + + all_results.append({ + 'step_number': step_num, + 'description': step.get('description', ''), + 'actions': step_results, + }) + + return all_results + + async def _try_recovery(self, tool: str, params: Dict, error: str, + context: Dict) -> Optional[Dict]: + """Simple recovery strategies for common failures.""" + error_lower = error.lower() + + if tool == 'read_file' and 'not found' in error_lower: + # Try search_files instead + path = params.get('path', '') + if path: + pattern = path.split('/')[-1] + return await self._execute_tool('search_files', { + 'pattern': pattern, + 'target': 'files', + }, context) + + if tool == 'terminal' and 'permission' in error_lower: + # Strip sudo prefix + cmd = params.get('command', '') + if cmd.startswith('sudo '): + return await self._execute_tool('terminal', { + 'command': cmd[5:], + }, context) + return None - + + # -------------------------------------------------------- + # Session persistence + # -------------------------------------------------------- + + async def _store_session(self, session_id: str, user_id: str, request: str, + plan: Dict, violations: List, status: str): + """Store reasoning session in database.""" + try: + data = { + 'id': session_id, + 'user_id': user_id, + 'initial_request': request, + 'context_summary': '', + 'execution_plan_json': json.dumps(plan, ensure_ascii=False), + 'reasoning_steps_json': '[]', + 'safety_violations_json': json.dumps(violations, ensure_ascii=False), + 'final_decision_json': json.dumps({'status': status}), + 'status': status, + 'created_at': datetime.now(), + 'updated_at': datetime.now(), + } + async with self.db.sqlorContext('default') as sor: + await sor.C('harnessed_reasoning_sessions', data) + except Exception as e: + warning(f"Failed to store session: {e}") + async def _update_session_status(self, session_id: str, status: str): - """Update reasoning session status""" + """Update session status.""" try: async with self.db.sqlorContext('default') as sor: await sor.U('harnessed_reasoning_sessions', { 'id': session_id, 'status': status, - 'updated_at': datetime.now() + 'updated_at': datetime.now(), }) except Exception: pass - - async def get_reasoning_session(self, session_id: str, - context: Dict[str, Any] = None) -> Dict[str, Any]: - """Retrieve a reasoning session by ID""" - user_id = self._get_current_user_id(context) if context else None - + + # -------------------------------------------------------- + # Session retrieval + # -------------------------------------------------------- + + async def get_reasoning_session(self, session_id: str, + context: Dict[str, Any] = None) -> Dict[str, Any]: + user_id = None + try: + user_id = context.get('user_id') if context else None + except Exception: + pass + try: async with self.db.sqlorContext('default') as sor: filters = {'id': session_id} if user_id: filters['user_id'] = user_id - - sessions = await sor.R('harnessed_reasoning_sessions', filters) - if sessions: - session = sessions[0] - return { - "success": True, - "session": { - "id": session["id"], - "user_id": session["user_id"], - "initial_request": session["initial_request"], - "context_summary": session["context_summary"], - "execution_plan": json.loads(session["execution_plan_json"]), - "safety_violations": json.loads(session["safety_violations_json"]), - "status": session["status"], - "created_at": session["created_at"], - "updated_at": session["updated_at"] - } - } - else: + + rows = await sor.R('harnessed_reasoning_sessions', filters) + if not rows: return {"success": False, "error": "Session not found"} - except Exception as e: - return {"success": False, "error": str(e)} - - async def list_reasoning_sessions(self, context: Dict[str, Any] = None, - limit: int = 50, offset: int = 0) -> Dict[str, Any]: - """List reasoning sessions for current user""" - user_id = self._get_current_user_id(context) if context else "anonymous" - - try: - async with self.db.sqlorContext('default') as sor: - sessions = await sor.R('harnessed_reasoning_sessions', { - 'user_id': user_id - }, orderby='created_at DESC', limit=limit, offset=offset) - - simplified_sessions = [] - for session in sessions: - simplified_sessions.append({ - "id": session["id"], - "request_preview": session["initial_request"][:100] + "..." if len(session["initial_request"]) > 100 else session["initial_request"], - "status": session["status"], - "confidence": json.loads(session["final_decision_json"]).get("confidence", 0), - "created_at": session["created_at"] - }) - + + s = rows[0] return { "success": True, - "sessions": simplified_sessions, - "total_count": len(simplified_sessions), - "user_id": user_id + "session": { + "id": s["id"], + "user_id": s["user_id"], + "initial_request": s["initial_request"], + "execution_plan": json.loads(s.get("execution_plan_json", "[]")), + "safety_violations": json.loads(s.get("safety_violations_json", "[]")), + "status": s["status"], + "created_at": str(s.get("created_at", "")), + "updated_at": str(s.get("updated_at", "")), + } } except Exception as e: return {"success": False, "error": str(e)} -# Global instance for module functions + async def list_reasoning_sessions(self, context: Dict[str, Any] = None, + limit: int = 50, offset: int = 0) -> Dict[str, Any]: + user_id = "anonymous" + try: + if context: + user_id = context.get('user_id') or 'anonymous' + except Exception: + pass + + try: + async with self.db.sqlorContext('default') as sor: + rows = await sor.R('harnessed_reasoning_sessions', + {'user_id': user_id}, + orderby='created_at DESC', limit=limit, offset=offset) + + sessions = [] + for s in rows: + plan = json.loads(s.get("execution_plan_json", "[]")) + steps = plan.get("steps", []) if isinstance(plan, dict) else [] + sessions.append({ + "id": s["id"], + "request_preview": s["initial_request"][:80] + ("..." if len(s.get("initial_request", "")) > 80 else ""), + "status": s["status"], + "step_count": len(steps), + "created_at": str(s.get("created_at", "")), + }) + + return {"success": True, "sessions": sessions, "total_count": len(sessions)} + except Exception as e: + return {"success": False, "error": str(e)} + + async def get_config(self) -> Dict[str, Any]: + config = await self._get_config() + return { + "max_reasoning_steps": config.get("max_reasoning_steps", 10), + "max_tool_calls_per_step": config.get("max_tool_calls_per_step", 5), + "enable_cross_session_search": config.get("enable_cross_session_search", "1") == "1", + "enable_skill_auto_loading": config.get("enable_skill_auto_loading", "1") == "1", + "safety_mode": config.get("safety_mode", "strict"), + "max_context_tokens": config.get("max_context_tokens", 4000), + "enable_error_recovery": config.get("enable_error_recovery", "1") == "1", + "max_recovery_attempts": config.get("max_recovery_attempts", 3), + "model_name": config.get("model_name", "qwen3-max"), + "temperature": config.get("temperature", 0.7), + "top_p": config.get("top_p", 0.9), + } + + +# ============================================================ +# Module-level exports (registered to ServerEnv) +# ============================================================ + _reasoning_instance = None def get_harnessed_reasoning_engine(): - """Get or create the global Hermes reasoning engine instance""" global _reasoning_instance if _reasoning_instance is None: _reasoning_instance = HermesReasoningEngine() return _reasoning_instance -# Exposed async functions for frontend integration + async def hermes_reason_and_execute(request: str, execute_immediately: bool = True): - """Perform reasoning and optionally execute the plan""" + """Perform reasoning and optionally execute the plan.""" engine = get_harnessed_reasoning_engine() return await engine.reason_and_execute(request, execute_immediately=execute_immediately) + async def hermes_get_reasoning_session(session_id: str): - """Retrieve a reasoning session by ID""" engine = get_harnessed_reasoning_engine() return await engine.get_reasoning_session(session_id) + async def hermes_list_reasoning_sessions(limit: int = 50, offset: int = 0): - """List reasoning sessions for current user""" engine = get_harnessed_reasoning_engine() return await engine.list_reasoning_sessions(limit=limit, offset=offset) + async def hermes_get_reasoning_config(): - """Get Hermes reasoning configuration""" engine = get_harnessed_reasoning_engine() - return { - "max_reasoning_steps": engine.config.max_reasoning_steps, - "max_tool_calls_per_step": engine.config.max_tool_calls_per_step, - "enable_cross_session_search": engine.config.enable_cross_session_search, - "enable_skill_auto_loading": engine.config.enable_skill_auto_loading, - "safety_mode": engine.config.safety_mode, - "max_context_tokens": engine.config.max_context_tokens, - "enable_error_recovery": engine.config.enable_error_recovery, - "max_recovery_attempts": engine.config.max_recovery_attempts - } \ No newline at end of file + return await engine.get_config() diff --git a/wwwroot/reasoning_console.ui b/wwwroot/reasoning_console.ui index 3059b7e..2d4630e 100644 --- a/wwwroot/reasoning_console.ui +++ b/wwwroot/reasoning_console.ui @@ -27,7 +27,7 @@ { "name": "request", "label": "推理请求", - "uitype": "textarea", + "uitype": "text", "required": true, "rows": 4, "placeholder": "请输入您的请求,例如:分析当前项目的安全问题并生成修复方案"