From 9a1147c5690a3527d2c8dc027d5d49f48e23a8a7 Mon Sep 17 00:00:00 2001 From: cyclop-bot <178948048+cyclop-bot@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:37:27 -0500 Subject: [PATCH 1/4] feat: Create api_helper.py for external copilot communication --- api_helper.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 api_helper.py diff --git a/api_helper.py b/api_helper.py new file mode 100644 index 0000000..501bc4b --- /dev/null +++ b/api_helper.py @@ -0,0 +1,59 @@ + +import http.server +import socketserver +import os +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +COPILOT_HOST = os.getenv("COPILOT_HOST", "0.0.0.0") +COPILOT_PORT = int(os.getenv("COPILOT_PORT", 8000)) +COPILOT_PATH = "/copilot" + +class CopilotRequestHandler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + if self.path == COPILOT_PATH: + content_length = int(self.headers['Content-Length']) + post_data_bytes = self.rfile.read(content_length) + post_data_str = post_data_bytes.decode('utf-8') + + logging.info(f"Received data from {self.client_address[0]}: {post_data_str}") + + # In a real scenario, you would pass post_data_str to your AI model + # and get a response. For now, we just echo it back. + response_text = f"Copilot received: {post_data_str}" + + self.send_response(200) + self.send_header('Content-type', 'text/plain; charset=utf-8') + self.end_headers() + self.wfile.write(response_text.encode('utf-8')) + else: + self.send_response(404) + self.end_headers() + self.wfile.write(b"Not Found") + + def do_GET(self): + if self.path == "/health": + self.send_response(200) + self.send_header('Content-type', 'text/plain; charset=utf-8') + self.end_headers() + self.wfile.write(b"API Helper is running") + else: + self.send_response(404) + self.end_headers() + self.wfile.write(b"Not Found") + +def run_server(server_class=http.server.HTTPServer, handler_class=CopilotRequestHandler, host=COPILOT_HOST, port=COPILOT_PORT): + server_address = (host, port) + httpd = server_class(server_address, handler_class) + logging.info(f"Starting Copilot API helper on http://{host}:{port}{COPILOT_PATH}") + logging.info(f"Health check available at http://{host}:{port}/health") + try: + httpd.serve_forever() + except KeyboardInterrupt: + logging.info("Server shutting down...") + httpd.server_close() + +if __name__ == '__main__': + run_server() From 2396e82b1b1ec39dd354f39a7b54783f1e434266 Mon Sep 17 00:00:00 2001 From: cyclop-bot <178948048+cyclop-bot@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:38:25 -0500 Subject: [PATCH 2/4] feat: Add call_external_copilot tool and update StandaloneLLMTool --- tools/standalone_llm_tool.py | 115 +++++++++++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/tools/standalone_llm_tool.py b/tools/standalone_llm_tool.py index 92ecf11..3c13291 100644 --- a/tools/standalone_llm_tool.py +++ b/tools/standalone_llm_tool.py @@ -3,6 +3,8 @@ import os import json import logging from openai import OpenAI +import urllib.request +import urllib.error class StandaloneLLMTool(BaseTool): def __init__(self): @@ -15,7 +17,7 @@ class StandaloneLLMTool(BaseTool): return [ { "type": "function", - "function": { + "function": { "name": "call_external_llm", "description": "Call an external language model", "parameters": { @@ -29,34 +31,121 @@ class StandaloneLLMTool(BaseTool): "type": "string", "description": "The model to use for generating the detailed instructions. Use mini for most coding tasks, preview when needing sophisticated reasoning", "enum": ["mini", "max"], - "default": "o1-mini" + "default": "mini" # Set default to 'mini' as per spec }, "max_tokens": { "type": "integer", "description": "The maximum number of tokens to use for generating the detailed instructions. Default is 16384.", + "default": 16384 } }, "required": ["prompt"] } }, "_tags": ["llm", "external"] + }, + { + "type": "function", + "function": { + "name": "call_external_copilot", + "description": "Calls a separate AI copilot instance over HTTP to get a response.", + "parameters": { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "The plain text prompt to send to the external copilot." + }, + "url": { + "type": "string", + "description": "The URL of the external copilot's API endpoint (e.g., 'http://localhost:8000/copilot')." + } + }, + "required": ["prompt", "url"] + } + }, + "_tags": ["copilot", "external", "http"] } ] + def _call_external_copilot(self, prompt: str, url: str): + logging.info(f"Calling external copilot at URL: {url} with prompt: {prompt[:50]}...") + if not url.startswith('http://') and not url.startswith('https://'): + error_message = f"Invalid URL scheme for external copilot: {url}. URL must start with http:// or https://" + logging.error(error_message) + return error_message + try: + req = urllib.request.Request( + url, + data=prompt.encode('utf-8'), + headers={'Content-Type': 'text/plain; charset=utf-8', 'User-Agent': 'DualAICopilot/0.1'}, + method='POST' + ) + with urllib.request.urlopen(req, timeout=60) as response: + if response.status == 200: + response_data = response.read().decode('utf-8') + logging.info(f"Received response from external copilot: {response_data[:100]}...") + return response_data + else: + error_message = f"External copilot at {url} returned an error: {response.status} {response.reason}" + logging.error(error_message) + return error_message # Return error as string + except urllib.error.HTTPError as e: + error_body = "" + try: + error_body = e.read().decode('utf-8', 'replace') # Added error decoding fallback + except Exception: + pass + error_message = f"HTTP Error {e.code} calling external copilot at {url}: {e.reason}. Response: {error_body}" + logging.error(error_message) + return error_message + except urllib.error.URLError as e: + error_message = f"URL Error calling external copilot at {url}: {e.reason}" + logging.error(error_message) + return error_message + except Exception as e: + error_message = f"An unexpected error occurred while calling external copilot at {url}: {str(e)}" + logging.error(error_message) + return error_message + def execute(self, function_name, **kwargs): if function_name == "call_external_llm": - return self.call_external_llm(kwargs.get("prompt"), kwargs.get("model"), kwargs.get("max_tokens")) + model = kwargs.get("model", "mini") # Default from spec + max_tokens = kwargs.get("max_tokens", 16384) # Default from spec + return self.call_external_llm(kwargs.get("prompt"), model, max_tokens) + elif function_name == "call_external_copilot": + return self._call_external_copilot(kwargs.get("prompt"), kwargs.get("url")) else: error_message = f"Unknown function: {function_name}" logging.error(error_message) + return error_message - def call_external_llm(self, prompt, model="o1-mini", max_tokens=16384): - logging.info(f"Calling external model: {model}") - response = self.client.completions.create( - model=model, - prompt=prompt, - max_tokens=max_tokens - ) - token_amount = response.summary["total_tokens"] - logging.info("Response generated, {token_amount} tokens used.") - return response.choices[0].text \ No newline at end of file + def call_external_llm(self, prompt, model="mini", max_tokens=16384): + logging.info(f"Calling external LLM model: {model} with max_tokens: {max_tokens}") + try: + actual_model_name = model + if model == "mini": + actual_model_name = "o1-mini" + # Add mapping for "max" if its name for OpenAI API is different + # elif model == "max": + # actual_model_name = "some-other-openai-model-name" + + response = self.client.completions.create( + model=actual_model_name, + prompt=prompt, + max_tokens=max_tokens + ) + + tokens_used = "unknown" # Default if token info isn't where expected + if hasattr(response, 'summary') and isinstance(response.summary, dict) and "total_tokens" in response.summary: + tokens_used = response.summary["total_tokens"] + elif hasattr(response, 'usage') and hasattr(response.usage, 'total_tokens'): + tokens_used = response.usage.total_tokens + + logging.info(f"LLM response generated, {tokens_used} tokens used.") + return response.choices[0].text + + except Exception as e: + error_message = f"Error calling external LLM: {str(e)}" + logging.error(error_message) + return error_message # Return error as string From 3d111dce7d2f410c6460479837c980966146384e Mon Sep 17 00:00:00 2001 From: cyclop-bot <178948048+cyclop-bot@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:43:05 -0500 Subject: [PATCH 3/4] feat: Flesh out api_helper.py with command handling --- api_helper.py | 136 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 113 insertions(+), 23 deletions(-) diff --git a/api_helper.py b/api_helper.py index 501bc4b..e59b871 100644 --- a/api_helper.py +++ b/api_helper.py @@ -1,8 +1,32 @@ - import http.server import socketserver import os import logging +import asyncio +# Assuming InferenceBot is available in the same environment or can be imported +# For demonstration, we'll use a placeholder if not explicitly provided. +try: + from inference_bot import InferenceBot +except ImportError: + logging.warning("InferenceBot not found. Using a placeholder for APIHelper.") + class InferenceBot: + def __init__(self): + self.history = {} + self.status_message = "Bot is operational." + self.processing_status = {} + async def start(self): return "Placeholder Bot started." + def clear_conversation_history(self, user_id): self.history[user_id] = [] + def get_bot_status(self): return self.status_message + async def switch_model(self): return "Placeholder model switched." + async def handle_message(self, user_id, message): + self.history.setdefault(user_id, []).append(f"User: {message}") + response = f"Placeholder Bot received: {message}" + self.history[user_id].append(f"Bot: {response}") + return response + async def abort_processing(self, user_id): return "Placeholder processing aborted." + def set_processing_status(self, user_id, message_id): self.processing_status[user_id] = message_id + def clear_processing_status(self, user_id): self.processing_status.pop(user_id, None) + # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') @@ -11,40 +35,102 @@ COPILOT_HOST = os.getenv("COPILOT_HOST", "0.0.0.0") COPILOT_PORT = int(os.getenv("COPILOT_PORT", 8000)) COPILOT_PATH = "/copilot" +# A single user ID for API interactions, as there's no multi-user concept here +API_USER_ID = 1 + +class APIHelper: + def __init__(self, bot: InferenceBot): + self.bot = bot + + async def _start_logic(self) -> str: + return await self.bot.start() + + async def _clear_logic(self, user_id: int) -> str: + self.bot.clear_conversation_history(user_id) + return "Conversation history cleared. Let's start fresh!" + + def _status_logic(self) -> str: + return self.bot.get_bot_status() + + async def _switch_logic(self) -> str: + if hasattr(self.bot, 'switch_model'): + return await self.bot.switch_model() + else: + return "Model switching is not supported for this bot." + + async def _handle_message_logic(self, user_id: int, user_message: str) -> str: + try: + response = await self.bot.handle_message(user_id, user_message) + return response + except Exception as e: + logging.error(f"Error in _handle_message_logic for user {user_id}: {str(e)}") + return f"Error processing message: {str(e)}" + + class CopilotRequestHandler(http.server.BaseHTTPRequestHandler): + # This will be set by the server when it's created + api_helper_instance: APIHelper = None + + def _send_response(self, status_code: int, content: str): + self.send_response(status_code) + self.send_header('Content-type', 'text/plain; charset=utf-8') + self.end_headers() + self.wfile.write(content.encode('utf-8')) + def do_POST(self): if self.path == COPILOT_PATH: content_length = int(self.headers['Content-Length']) post_data_bytes = self.rfile.read(content_length) - post_data_str = post_data_bytes.decode('utf-8') + user_message = post_data_bytes.decode('utf-8').strip() - logging.info(f"Received data from {self.client_address[0]}: {post_data_str}") + logging.info(f"Received POST from {self.client_address[0]}: {user_message}") - # In a real scenario, you would pass post_data_str to your AI model - # and get a response. For now, we just echo it back. - response_text = f"Copilot received: {post_data_str}" - - self.send_response(200) - self.send_header('Content-type', 'text/plain; charset=utf-8') - self.end_headers() - self.wfile.write(response_text.encode('utf-8')) + response_text = "" + # Use a fixed user ID for the API interaction + user_id = API_USER_ID + + if self.api_helper_instance is None: + logging.error("APIHelper instance not set on request handler.") + self._send_response(500, "Internal Server Error: API Helper not initialized.") + return + + # Simulate command handling based on message content + if user_message.startswith('/'): + command_parts = user_message.split(' ', 1) + command = command_parts[0] + + if command == '/start': + response_text = asyncio.run(self.api_helper_instance._start_logic()) + elif command == '/clear': + response_text = asyncio.run(self.api_helper_instance._clear_logic(user_id)) + elif command == '/status': + response_text = self.api_helper_instance._status_logic() + elif command == '/switch': + response_text = asyncio.run(self.api_helper_instance._switch_logic()) + else: + # For unknown commands, treat as a regular message or an error + response_text = asyncio.run(self.api_helper_instance._handle_message_logic(user_id, user_message)) + + else: + # Treat as a regular message + response_text = asyncio.run(self.api_helper_instance._handle_message_logic(user_id, user_message)) + + self._send_response(200, response_text) else: - self.send_response(404) - self.end_headers() - self.wfile.write(b"Not Found") + self._send_response(404, "Not Found") def do_GET(self): if self.path == "/health": - self.send_response(200) - self.send_header('Content-type', 'text/plain; charset=utf-8') - self.end_headers() - self.wfile.write(b"API Helper is running") + self._send_response(200, "API Helper is running") else: - self.send_response(404) - self.end_headers() - self.wfile.write(b"Not Found") + self._send_response(404, "Not Found") + +def run_server(bot_instance: InferenceBot, server_class=http.server.HTTPServer, handler_class=CopilotRequestHandler, host=COPILOT_HOST, port=COPILOT_PORT): + # Create an instance of APIHelper + api_helper = APIHelper(bot_instance) + # Attach the APIHelper instance to the handler class + handler_class.api_helper_instance = api_helper -def run_server(server_class=http.server.HTTPServer, handler_class=CopilotRequestHandler, host=COPILOT_HOST, port=COPILOT_PORT): server_address = (host, port) httpd = server_class(server_address, handler_class) logging.info(f"Starting Copilot API helper on http://{host}:{port}{COPILOT_PATH}") @@ -56,4 +142,8 @@ def run_server(server_class=http.server.HTTPServer, handler_class=CopilotRequest httpd.server_close() if __name__ == '__main__': - run_server() + # In a real deployment, you would pass a properly configured InferenceBot instance here. + # For standalone execution, we instantiate the placeholder InferenceBot. + logging.warning("Running api_helper.py in standalone mode with a placeholder InferenceBot.") + logging.warning("Ensure a proper InferenceBot instance is passed when integrating into a larger system.") + run_server(bot_instance=InferenceBot()) From fb111ab0181e7a3a4bae1c0b41d6ccb0c5c205d3 Mon Sep 17 00:00:00 2001 From: cyclop-bot <178948048+cyclop-bot@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:43:14 -0500 Subject: [PATCH 4/4] feat: Configure call_external_copilot URL via environment variable --- tools/standalone_llm_tool.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/tools/standalone_llm_tool.py b/tools/standalone_llm_tool.py index 3c13291..8b9479b 100644 --- a/tools/standalone_llm_tool.py +++ b/tools/standalone_llm_tool.py @@ -9,6 +9,9 @@ import urllib.error class StandaloneLLMTool(BaseTool): def __init__(self): self.client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) + self.copilot_url = os.getenv("COPILOT_API_URL") + if not self.copilot_url: + logging.warning("COPILOT_API_URL environment variable not set. call_external_copilot will not function.") def clear(self): pass @@ -55,28 +58,27 @@ class StandaloneLLMTool(BaseTool): "prompt": { "type": "string", "description": "The plain text prompt to send to the external copilot." - }, - "url": { - "type": "string", - "description": "The URL of the external copilot's API endpoint (e.g., 'http://localhost:8000/copilot')." } }, - "required": ["prompt", "url"] + "required": ["prompt"] } }, "_tags": ["copilot", "external", "http"] } ] - def _call_external_copilot(self, prompt: str, url: str): - logging.info(f"Calling external copilot at URL: {url} with prompt: {prompt[:50]}...") - if not url.startswith('http://') and not url.startswith('https://'): - error_message = f"Invalid URL scheme for external copilot: {url}. URL must start with http:// or https://" + def _call_external_copilot(self, prompt: str): + if not self.copilot_url: + return "Error: COPILOT_API_URL environment variable is not set. Cannot call external copilot." + + logging.info(f"Calling external copilot at URL: {self.copilot_url} with prompt: {prompt[:50]}...") + if not self.copilot_url.startswith('http://') and not self.copilot_url.startswith('https://'): + error_message = f"Invalid URL scheme for external copilot: {self.copilot_url}. URL must start with http:// or https://" logging.error(error_message) return error_message try: req = urllib.request.Request( - url, + self.copilot_url, data=prompt.encode('utf-8'), headers={'Content-Type': 'text/plain; charset=utf-8', 'User-Agent': 'DualAICopilot/0.1'}, method='POST' @@ -87,7 +89,7 @@ class StandaloneLLMTool(BaseTool): logging.info(f"Received response from external copilot: {response_data[:100]}...") return response_data else: - error_message = f"External copilot at {url} returned an error: {response.status} {response.reason}" + error_message = f"External copilot at {self.copilot_url} returned an error: {response.status} {response.reason}" logging.error(error_message) return error_message # Return error as string except urllib.error.HTTPError as e: @@ -96,15 +98,15 @@ class StandaloneLLMTool(BaseTool): error_body = e.read().decode('utf-8', 'replace') # Added error decoding fallback except Exception: pass - error_message = f"HTTP Error {e.code} calling external copilot at {url}: {e.reason}. Response: {error_body}" + error_message = f"HTTP Error {e.code} calling external copilot at {self.copilot_url}: {e.reason}. Response: {error_body}" logging.error(error_message) return error_message except urllib.error.URLError as e: - error_message = f"URL Error calling external copilot at {url}: {e.reason}" + error_message = f"URL Error calling external copilot at {self.copilot_url}: {e.reason}" logging.error(error_message) return error_message except Exception as e: - error_message = f"An unexpected error occurred while calling external copilot at {url}: {str(e)}" + error_message = f"An unexpected error occurred while calling external copilot at {self.copilot_url}: {str(e)}" logging.error(error_message) return error_message @@ -114,7 +116,7 @@ class StandaloneLLMTool(BaseTool): max_tokens = kwargs.get("max_tokens", 16384) # Default from spec return self.call_external_llm(kwargs.get("prompt"), model, max_tokens) elif function_name == "call_external_copilot": - return self._call_external_copilot(kwargs.get("prompt"), kwargs.get("url")) + return self._call_external_copilot(kwargs.get("prompt")) else: error_message = f"Unknown function: {function_name}" logging.error(error_message)