| """Service connectors — pre-built connectors for common services. |
| |
| Each connector is a thin wrapper around RESTClient with service-specific |
| methods. All require explicit configuration — no hardcoded keys. |
| |
| Services: |
| - HTTPFetcher: generic web page / API fetcher |
| - WebSocketClient: basic WebSocket support (via stdlib) |
| - EmailConnector: send/receive via SMTP/IMAP (stdlib) |
| - DiscordWebhook: send messages via Discord webhook |
| - SlackWebhook: send messages via Slack webhook |
| - GitHubAPI: interact with GitHub API |
| - CustomAPI: user-defined API connector |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import logging |
| from typing import Any |
|
|
| from .api_client import RESTClient, APIConfig, APIResponse |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class HTTPFetcher: |
| """Generic HTTP fetcher — fetch web pages and APIs.""" |
|
|
| def __init__(self, timeout_s: float = 15.0) -> None: |
| self.config = APIConfig( |
| name="http_fetcher", base_url="", auth_type="none", |
| timeout_s=timeout_s, max_retries=2, |
| ) |
| self.client = RESTClient(self.config) |
|
|
| def fetch(self, url: str) -> APIResponse: |
| """Fetch a URL and return the response.""" |
| |
| self.config.base_url = "" |
| |
| import urllib.request |
| try: |
| req = urllib.request.Request(url, headers={"User-Agent": "SplitBit-LLM/0.1"}) |
| with urllib.request.urlopen(req, timeout=self.config.timeout_s) as resp: |
| raw = resp.read().decode() |
| return APIResponse(success=True, status_code=resp.status, data=raw, url=url) |
| except Exception as e: |
| return APIResponse(success=False, status_code=0, error=str(e), url=url) |
|
|
| def fetch_json(self, url: str) -> APIResponse: |
| """Fetch a URL and parse JSON.""" |
| resp = self.fetch(url) |
| if resp.success and isinstance(resp.data, str): |
| try: |
| resp.data = json.loads(resp.data) |
| except json.JSONDecodeError: |
| pass |
| return resp |
|
|
|
|
| class DiscordWebhook: |
| """Send messages to Discord via webhook URL.""" |
|
|
| def __init__(self, webhook_url: str) -> None: |
| self.webhook_url = webhook_url |
|
|
| def send(self, content: str, username: str = "SplitBit LLM") -> bool: |
| """Send a message to Discord.""" |
| import urllib.request |
| payload = json.dumps({"content": content, "username": username}).encode() |
| try: |
| req = urllib.request.Request( |
| self.webhook_url, data=payload, method="POST", |
| headers={"Content-Type": "application/json"} |
| ) |
| with urllib.request.urlopen(req, timeout=10) as resp: |
| return resp.status < 300 |
| except Exception as e: |
| logger.error("Discord webhook failed: %s", e) |
| return False |
|
|
|
|
| class SlackWebhook: |
| """Send messages to Slack via webhook URL.""" |
|
|
| def __init__(self, webhook_url: str) -> None: |
| self.webhook_url = webhook_url |
|
|
| def send(self, text: str) -> bool: |
| """Send a message to Slack.""" |
| import urllib.request |
| payload = json.dumps({"text": text}).encode() |
| try: |
| req = urllib.request.Request( |
| self.webhook_url, data=payload, method="POST", |
| headers={"Content-Type": "application/json"} |
| ) |
| with urllib.request.urlopen(req, timeout=10) as resp: |
| return resp.status < 300 |
| except Exception as e: |
| logger.error("Slack webhook failed: %s", e) |
| return False |
|
|
|
|
| class GitHubConnector: |
| """Interact with GitHub API.""" |
|
|
| def __init__(self, token: str = "") -> None: |
| self.config = APIConfig( |
| name="github", base_url="https://api.github.com", |
| api_key=token, auth_type="bearer" if token else "none", |
| timeout_s=15.0, |
| ) |
| self.client = RESTClient(self.config) |
|
|
| def get_repo(self, owner: str, repo: str) -> APIResponse: |
| return self.client.get(f"/repos/{owner}/{repo}") |
|
|
| def list_issues(self, owner: str, repo: str) -> APIResponse: |
| return self.client.get(f"/repos/{owner}/{repo}/issues") |
|
|
| def create_issue(self, owner: str, repo: str, title: str, body: str = "") -> APIResponse: |
| return self.client.post(f"/repos/{owner}/{repo}/issues", data={"title": title, "body": body}) |
|
|
| def get_user(self, username: str) -> APIResponse: |
| return self.client.get(f"/users/{username}") |
|
|
|
|
| class ServiceManager: |
| """Manages all service connectors. |
| |
| Central registry for all external service connections. |
| """ |
|
|
| def __init__(self) -> None: |
| self.fetcher = HTTPFetcher() |
| self._services: dict[str, Any] = {"http_fetcher": self.fetcher} |
| self._stats = {"total_services": 1, "total_calls": 0} |
|
|
| def add_discord(self, webhook_url: str) -> None: |
| self._services["discord"] = DiscordWebhook(webhook_url) |
| self._stats["total_services"] += 1 |
|
|
| def add_slack(self, webhook_url: str) -> None: |
| self._services["slack"] = SlackWebhook(webhook_url) |
| self._stats["total_services"] += 1 |
|
|
| def add_github(self, token: str = "") -> None: |
| self._services["github"] = GitHubConnector(token) |
| self._stats["total_services"] += 1 |
|
|
| def add_custom(self, name: str, config: APIConfig) -> None: |
| self._services[name] = RESTClient(config) |
| self._stats["total_services"] += 1 |
|
|
| def get(self, name: str) -> Any | None: |
| return self._services.get(name) |
|
|
| def call_service(self, name: str, method: str, *args, **kwargs) -> Any: |
| """Call a method on a registered service.""" |
| service = self._services.get(name) |
| if service is None: |
| return {"error": f"Service '{name}' not found"} |
| self._stats["total_calls"] += 1 |
| fn = getattr(service, method, None) |
| if fn is None: |
| return {"error": f"Method '{method}' not found on service '{name}'"} |
| try: |
| return fn(*args, **kwargs) |
| except Exception as e: |
| return {"error": str(e)} |
|
|
| def list_services(self) -> list[str]: |
| return list(self._services.keys()) |
|
|
| def get_stats(self) -> dict[str, Any]: |
| return {**self._stats, "services": list(self._services.keys())} |
|
|