import asyncio import aiohttp import json import argparse import sys from typing import Dict, List, Any, Optional import ais class OpenAPIFormatter: def __init__(self, openapi_spec: Dict[str, Any]): self.spec = openapi_spec self.title = openapi_spec.get("info", {}).get("title", "API") self.description = openapi_spec.get("info", {}).get("description", "") self.paths = openapi_spec.get("paths", {}) self.components = openapi_spec.get("components", {}) self.schemas = self.components.get("schemas", {}) def extract_endpoints(self) -> List[Dict[str, str]]: endpoints = [] for path, methods in self.paths.items(): for method, details in methods.items(): if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']: endpoints.append({ "endpoint": path, "method": method.upper(), "summary": details.get("summary", ""), "description": details.get("description", "") }) return endpoints def get_request_body_schema(self, path: str, method: str) -> Optional[Dict]: try: method_info = self.paths[path][method.lower()] request_body = method_info.get("requestBody", {}) content = request_body.get("content", {}) json_content = content.get("application/json", {}) schema = json_content.get("schema", {}) return self.resolve_schema_ref(schema) if schema else None except KeyError: return None def get_parameters(self, path: str, method: str) -> List[Dict]: try: method_info = self.paths[path][method.lower()] params = method_info.get("parameters", []) resolved = [] for p in params: if "$ref" in p: ref_path = p["$ref"] if ref_path.startswith("#/components/parameters/"): param_name = ref_path.split("/")[-1] p = self.components.get("parameters", {}).get(param_name, {}) schema = p.get("schema", {}) schema = self.resolve_schema_ref(schema) resolved.append({ "name": p.get("name"), "in": p.get("in"), "description": p.get("description", ""), "required": p.get("required", False), "schema": schema }) return resolved except KeyError: return [] def resolve_schema_ref(self, schema: Dict) -> Dict: if "$ref" in schema: ref_path = schema["$ref"] if ref_path.startswith("#/components/schemas/"): schema_name = ref_path.split("/")[-1] return self.schemas.get(schema_name, {}) return schema def generate_example_body(self, schema: Dict) -> Dict: if not schema: return {} schema = self.resolve_schema_ref(schema) properties = schema.get("properties", {}) example = {} for prop_name, prop_schema in properties.items(): prop_schema = self.resolve_schema_ref(prop_schema) prop_type = prop_schema.get("type", "string") if prop_type == "array": items_schema = self.resolve_schema_ref(prop_schema.get("items", {})) if items_schema.get("type") == "object": example[prop_name] = [self.generate_example_body(items_schema)] else: example[prop_name] = [self.get_example_value(items_schema)] elif prop_type == "object": example[prop_name] = self.generate_example_body(prop_schema) else: example[prop_name] = self.get_example_value(prop_schema) return example def get_example_value(self, schema: Dict) -> Any: if not schema: return "example_value" prop_type = schema.get("type", "string") description = schema.get("description", "").lower() title = schema.get("title", "").lower() if prop_type == "string": if "name" in description or "name" in title: return "Example Name" elif "type" in description or "type" in title: return "example_type" elif "query" in description or "search" in description: return "search_term" else: return "example_value" elif prop_type == "integer": return 1 elif prop_type == "number": return 1.0 elif prop_type == "boolean": return True elif prop_type == "array": return [] else: return "example_value" def generate_instructions(self) -> Dict[str, Any]: endpoints = self.extract_endpoints() example_single = self.generate_single_call_example(endpoints) example_multiple = self.generate_multiple_call_example(endpoints) instructions = { "llm_api_instruction": { "title": f"API Call Instructions for {self.title}", "overview": f"When you need to make API calls to {self.title}, respond with a JSON array containing API call objects. Each object represents one API call to execute.", "api_description": self.description, "required_response_format": { "description": "Your response must be a valid JSON array of objects in this exact format:", "format": [ { "endpoint": "string - endpoint path with path parameters substituted", "method": "string - HTTP method (GET, POST, PUT, DELETE, etc.)", "query": "object - query parameters (omit if not needed)", "body": "object - request body (omit if not needed)" } ], "example_single_call": example_single, "example_multiple_calls": example_multiple }, "available_endpoints": endpoints, "critical_rules": [ "ALWAYS return a JSON array, even for single calls: [{...}]", "Use EXACT endpoint paths from the available_endpoints list, substituting path parameters {param} with actual values", "For query parameters ('in': 'query' in endpoint_details), include them in the 'query' field", "For path parameters ('in': 'path'), substitute directly in the endpoint path", "Include ALL required parameters and fields", "Match data types from schemas in endpoint_details", "Replace example values with actual data from user's request", "Omit 'query' and 'body' fields if not needed", "Ensure your JSON is valid and parseable" ], "endpoint_details": self.generate_endpoint_details() } } return instructions def generate_single_call_example(self, endpoints: List[Dict]) -> List[Dict]: get_endpoints = [ep for ep in endpoints if ep["method"] == "GET"] if get_endpoints: endpoint = get_endpoints[0] else: post_endpoints = [ep for ep in endpoints if ep["method"] == "POST"] if not post_endpoints: return [{"endpoint": "/example", "method": "GET"}] endpoint = post_endpoints[0] return self._generate_example(endpoint) def generate_multiple_call_example(self, endpoints: List[Dict]) -> List[Dict]: examples = [] for endpoint in endpoints[:2]: examples.append(self._generate_example(endpoint)[0]) return examples if examples else [{"endpoint": "/example", "method": "GET"}] def _generate_example(self, endpoint: Dict) -> List[Dict]: path = endpoint["endpoint"] method = endpoint["method"] params = self.get_parameters(path, method) path_params = {p["name"]: self.get_example_value(p["schema"]) for p in params if p["in"] == "path"} query_params = {p["name"]: self.get_example_value(p["schema"]) for p in params if p["in"] == "query"} endpoint_str = path if path_params: try: endpoint_str = path.format(**{k: str(v) for k, v in path_params.items()}) except: pass example = { "endpoint": endpoint_str, "method": method } if query_params: example["query"] = query_params schema = self.get_request_body_schema(path, method) if schema: example["body"] = self.generate_example_body(schema) return [example] def generate_endpoint_details(self) -> Dict[str, Any]: details = {} for path, methods in self.paths.items(): for method, info in methods.items(): if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']: key = f"{method.upper()} {path}" schema = self.get_request_body_schema(path, method) details[key] = { "summary": info.get("summary", ""), "description": info.get("description", ""), "parameters": self.get_parameters(path, method), "request_body_schema": schema if schema else None } return details async def fetch_openapi_spec(url: str) -> Dict[str, Any]: async with aiohttp.ClientSession() as session: async with session.get(url, timeout=30) as resp: if resp.status != 200: text = await resp.text() raise Exception(f"Failed to fetch OpenAPI spec from {url}: {resp.status} {text}") return await resp.json() async def get_system_message(url: str) -> Dict[str, Any]: openapi_spec = await fetch_openapi_spec(url) formatter = OpenAPIFormatter(openapi_spec) return formatter.generate_instructions() async def prompt(message, system, model="gpt-4o"): system = json.dumps(system['llm_api_instruction']) client = ais.AIS(model="gemma", system_message=system) content = client.chat(message) return content async def make_call(session, BASE_URL, call): url_ = f"{BASE_URL.rstrip('/')}/{call['endpoint'].lstrip('/')}" method = call['method'].upper() query = call.get('query') body = call.get('body') params = query params = {k: v for k, v in (params or {}).items() if v is not None} json_payload = body if method not in ["GET", "HEAD", "DELETE"] else None print(call) async with session.request(method, url_, params=params, json=json_payload) as resp: try: response = await resp.json() except Exception: response = await resp.text() full_url = str(resp.url) print(f"HTTP Query: {method} {full_url} with body: {json_payload}, got response: {response} ({resp.status})", flush=True, file=sys.stderr) return response async def run_service(url, p): from urllib.parse import urljoin, urlparse parsed_url = urlparse(url) BASE_URL = f"{parsed_url.scheme}://{parsed_url.netloc}" system_message_dict = await get_system_message(url) system_message = system_message_dict calls = await prompt(p, system_message) results = [] if isinstance(calls, list): async with aiohttp.ClientSession() as session: tasks = [asyncio.create_task(make_call(session, BASE_URL, call)) for call in calls if isinstance(call, dict)] results += await asyncio.gather(*tasks) return results DEFAULT_URLS = [ "https://ada.molodetz.nl/api/memory/openapi.json", ] async def run_services(p, urls=None): if not urls: urls = DEFAULT_URLS tasks = [asyncio.create_task(run_service(url, p)) for url in urls] return await asyncio.gather(*tasks) if __name__ == "__main__": # Example: # python3 openapi.py --url=http://localhost:8000/openapi.json --prompt "Make a note for X and make a note for Y" parser = argparse.ArgumentParser(description="API Caller") parser.add_argument('--url', type=str, default="https://tools.molodetz.online/openapi.json", help="Base URL for API calls") parser.add_argument('--prompt', type=str, help="Prompt for LLM") args = parser.parse_args() print(asyncio.run(run_service(args.url, args.prompt)))