Spaces:
Running
Running
| """Custom ReACT engine using LiteLLM's native function calling with Z.ai GLM-5. | |
| CrewAI doesn't reliably route tools through to Z.ai via LiteLLM, so this module | |
| bypasses CrewAI entirely for the tool-calling loop. | |
| """ | |
| import json | |
| import logging | |
| import time | |
| import litellm | |
| from code_tribunal.config import TribunalConfig | |
| log = logging.getLogger("code_tribunal.react") | |
| _MAX_RETRIES = 5 | |
| _BASE_DELAY = 4.0 | |
| def _completion_with_retry(**kwargs): | |
| """Call litellm.completion with exponential backoff on rate-limit errors.""" | |
| for attempt in range(_MAX_RETRIES): | |
| try: | |
| return litellm.completion(**kwargs) | |
| except litellm.RateLimitError: | |
| if attempt == _MAX_RETRIES - 1: | |
| raise | |
| delay = _BASE_DELAY * (2 ** attempt) | |
| log.warning("[RETRY] Rate limited (attempt %d/%d), waiting %.0fs...", attempt + 1, _MAX_RETRIES, delay) | |
| time.sleep(delay) | |
| def _build_tool_schemas(tools: list) -> list[dict]: | |
| """Convert CrewAI BaseTool instances to OpenAI function calling schema.""" | |
| schemas = [] | |
| for tool in tools: | |
| schema = tool.args_schema.model_json_schema() | |
| properties = schema.get("properties", {}) | |
| for prop in properties.values(): | |
| prop.pop("title", None) | |
| schemas.append({ | |
| "type": "function", | |
| "function": { | |
| "name": tool.name, | |
| "description": tool.description.split("\n")[0], | |
| "parameters": { | |
| "type": "object", | |
| "properties": properties, | |
| "required": schema.get("required", []), | |
| }, | |
| }, | |
| }) | |
| return schemas | |
| def _execute_tool(tools: list, tool_name: str, arguments: dict) -> str: | |
| """Find and execute a tool by name.""" | |
| for tool in tools: | |
| if tool.name == tool_name: | |
| return tool._run(**arguments) | |
| return f"Error: Unknown tool '{tool_name}'" | |
| def react_loop( | |
| config: TribunalConfig, | |
| task_description: str, | |
| agent_role: str, | |
| agent_goal: str, | |
| tools: list, | |
| max_iterations: int = 10, | |
| ) -> str: | |
| """Run a full ReACT loop using function calling. | |
| The agent receives tools, decides which to call, observes the results, | |
| and iterates until it has enough information to answer. | |
| Returns the final text output. | |
| """ | |
| tool_schemas = _build_tool_schemas(tools) | |
| system_prompt = ( | |
| f"You are {agent_role}. {agent_goal}\n\n" | |
| "You have access to tools for investigating code. Use them actively:\n" | |
| "- Call file_reader to read specific files\n" | |
| "- Call pattern_search to run GritQL patterns\n" | |
| "- Call code_graph_query to trace call chains and dependencies\n" | |
| "- Call finding_context to see surrounding code for a finding\n\n" | |
| "Always call at least one tool before giving your final answer. " | |
| "After gathering information, provide a detailed analysis." | |
| ) | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": task_description}, | |
| ] | |
| for iteration in range(max_iterations): | |
| response = _completion_with_retry( | |
| model=config.model_name, | |
| messages=messages, | |
| tools=tool_schemas, | |
| tool_choice="auto", | |
| api_key=config.api_key, | |
| api_base=config.api_base, | |
| temperature=config.temperature, | |
| ) | |
| message = response.choices[0].message | |
| messages.append(message.model_dump()) | |
| if not message.tool_calls: | |
| return message.content or "" | |
| for tool_call in message.tool_calls: | |
| func_name = tool_call.function.name | |
| try: | |
| func_args = json.loads(tool_call.function.arguments) | |
| except json.JSONDecodeError: | |
| func_args = {} | |
| log.debug(" [ReACT %d] %s(%s)", iteration + 1, func_name, func_args) | |
| result = _execute_tool(tools, func_name, func_args) | |
| messages.append({ | |
| "role": "tool", | |
| "content": str(result), | |
| "tool_call_id": tool_call.id, | |
| }) | |
| response = _completion_with_retry( | |
| model=config.model_name, | |
| messages=messages, | |
| api_key=config.api_key, | |
| api_base=config.api_base, | |
| temperature=config.temperature, | |
| ) | |
| return response.choices[0].message.content or "" | |
| def react_loop_stream( | |
| config: TribunalConfig, | |
| task_description: str, | |
| agent_role: str, | |
| agent_goal: str, | |
| tools: list, | |
| max_iterations: int = 10, | |
| ): | |
| """Streaming ReACT loop — yields (role, delta_text, is_tool_call) tuples. | |
| Each tool call is reported as a small delta. The final answer is yielded | |
| as a large delta. | |
| """ | |
| tool_schemas = _build_tool_schemas(tools) | |
| system_prompt = ( | |
| f"You are {agent_role}. {agent_goal}\n\n" | |
| "You have access to tools for investigating code. Use them actively:\n" | |
| "- Call file_reader to read specific files\n" | |
| "- Call pattern_search to run GritQL patterns\n" | |
| "- Call code_graph_query to trace call chains and dependencies\n" | |
| "- Call finding_context to see surrounding code for a finding\n\n" | |
| "Always call at least one tool before giving your final answer. " | |
| "After gathering information, provide a detailed analysis." | |
| ) | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": task_description}, | |
| ] | |
| for iteration in range(max_iterations): | |
| response = _completion_with_retry( | |
| model=config.model_name, | |
| messages=messages, | |
| tools=tool_schemas, | |
| tool_choice="auto", | |
| api_key=config.api_key, | |
| api_base=config.api_base, | |
| temperature=config.temperature, | |
| ) | |
| message = response.choices[0].message | |
| messages.append(message.model_dump()) | |
| if message.tool_calls: | |
| for tool_call in message.tool_calls: | |
| func_name = tool_call.function.name | |
| try: | |
| func_args = json.loads(tool_call.function.arguments) | |
| except json.JSONDecodeError: | |
| func_args = {} | |
| yield (agent_role, f"\n[Using tool: {func_name}({json.dumps(func_args)})]\n", True) | |
| result = _execute_tool(tools, func_name, func_args) | |
| messages.append({ | |
| "role": "tool", | |
| "content": str(result), | |
| "tool_call_id": tool_call.id, | |
| }) | |
| if not message.tool_calls: | |
| if message.content: | |
| yield (agent_role, message.content, False) | |
| break | |
| else: | |
| response = _completion_with_retry( | |
| model=config.model_name, | |
| messages=messages, | |
| api_key=config.api_key, | |
| api_base=config.api_base, | |
| temperature=config.temperature, | |
| ) | |
| yield (agent_role, response.choices[0].message.content or "", False) | |