Merge pull request #216 from bucolucas/feature/dual-ai-comms
feat: Introduce external copilot communication via API
This commit is contained in:
+149
@@ -0,0 +1,149 @@
|
||||
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')
|
||||
|
||||
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)
|
||||
user_message = post_data_bytes.decode('utf-8').strip()
|
||||
|
||||
logging.info(f"Received POST from {self.client_address[0]}: {user_message}")
|
||||
|
||||
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, "Not Found")
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self._send_response(200, "API Helper is running")
|
||||
else:
|
||||
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
|
||||
|
||||
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__':
|
||||
# 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())
|
||||
+104
-13
@@ -3,10 +3,15 @@ import os
|
||||
import json
|
||||
import logging
|
||||
from openai import OpenAI
|
||||
import urllib.request
|
||||
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
|
||||
@@ -15,7 +20,7 @@ class StandaloneLLMTool(BaseTool):
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"function": {
|
||||
"name": "call_external_llm",
|
||||
"description": "Call an external language model",
|
||||
"parameters": {
|
||||
@@ -29,34 +34,120 @@ 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."
|
||||
}
|
||||
},
|
||||
"required": ["prompt"]
|
||||
}
|
||||
},
|
||||
"_tags": ["copilot", "external", "http"]
|
||||
}
|
||||
]
|
||||
|
||||
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(
|
||||
self.copilot_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 {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:
|
||||
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 {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 {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 {self.copilot_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"))
|
||||
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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user