From 723cbece96ac139a91c9766dd8af846c13681dba Mon Sep 17 00:00:00 2001 From: Joel Kronqvist Date: Tue, 13 Oct 2026 20:44:39 +0300 Subject: feat: socks5 proxy --- README.md | 7 +++++++ actions.py | 2 +- llm.py | 36 ++++++++++++++++++++++++++++-------- web_utils.py | 12 ++++++++---- 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0293439..0b39f0a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ This project implements a simple autonomous coding agent that can interact with - `webgrep`: Search for keywords within the markdownified content of a URL. - **Reasoning Loop**: Uses a recursive loop to process tasks, analyze results, and determine the next step until a final response is reached. - **Context Management**: Estimates the token count of the conversation and automatically squashes (summarizes) older history when it approaches the model's context size limit, keeping the most recent messages intact. +- **SOCKS5 Proxy Support**: API queries (and `webgrep` fetches) can be routed through a SOCKS5 proxy. - **Logging**: Logs interactions to `thoughts.log` for debugging and analysis. ## Requirements @@ -23,6 +24,7 @@ This project implements a simple autonomous coding agent that can interact with - `openai` Python library - `tldr` CLI installed on the system - An API key for the Aalto LLM Gateway (or a compatible OpenAI-style API) +- `socksio` Python package (only needed when using a SOCKS5 proxy) ## Setup @@ -56,6 +58,11 @@ This project implements a simple autonomous coding agent that can interact with - `--min-history N`: Minimum amount of most recent messages to keep intact when summarising the history to keep the context short enough (default: 3). - `--model-context-size N`: Context size supported by the model (defaults to `LLM_CONTEXT_SIZE`, or 1048576). - `--trust-level LEVEL`: The trust level of the agent (defaults to 2). Determines which actions require manual approval. +- `--socks5-proxy URL`: Route API queries (and `webgrep` fetches) through a SOCKS5 proxy (defaults to `LLM_SOCKS5_PROXY` environment variable). + ```bash + python3 llm.py --socks5-proxy socks5://user:pass@proxy.example.com:1080 + ``` + Use `socks5h://` as the scheme to also resolve DNS hostnames through the proxy. This requires the `socksio` package to be installed in the virtualenv (`pip install socksio`). ## How it Works diff --git a/actions.py b/actions.py index 7d2b61c..295528e 100644 --- a/actions.py +++ b/actions.py @@ -130,7 +130,7 @@ def _mypy(args: JSON, config: AgentConfig) -> JSON: def _webgrep(args: JSON, config: AgentConfig) -> JSON: '{"action": "webgrep", "url": URL, "keywords": KEYWORD_LIST} - find case-insensitive matches in markdownified content of URL for keywords in KEYWORD_LIST plus some context on both sides' - match fetch_and_markdownify(args["url"]): + match fetch_and_markdownify(args["url"], proxy=config.socks5_proxy): case FetchError(e): return {"status": "error", "reason": e} case FetchSuccess(content): diff --git a/llm.py b/llm.py index ca61b48..9abfe12 100755 --- a/llm.py +++ b/llm.py @@ -1,7 +1,8 @@ #!/bin/python3 import os from pathlib import Path -from openai import OpenAI +import httpx2 +from openai import OpenAI, DefaultHttpx2Client from openai.types.responses import EasyInputMessageParam as Message from typing import Optional, List, Dict, TypeVar, TypeAlias, Callable, Tuple, Literal import argparse @@ -39,13 +40,14 @@ class AgentConfig: trustlevels: Dict[str, int] trustlevel: int query_method: QueryMethod + socks5_proxy: Optional[str] = None search_padding: int = 200 def estimate_tokens(state: DiscussionState) -> int: """Simple heuristic to estimate tokens in the conversation state.""" text = "".join(str(m["content"]) for m in state) - return len(text) // 3.5 + return int(len(text) / 3.5) def mk_direct_query( @@ -187,13 +189,24 @@ def main(): default=2, help="The trust level of the agent (default: 2)", ) + parser.add_argument( + "--socks5-proxy", + type=str, + default=os.environ.get("LLM_SOCKS5_PROXY"), + help=( + "Route API queries (and webgrep fetches) through a SOCKS5 proxy, e.g. " + "socks5://host:1080. Use socks5h:// to resolve DNS through the proxy; " + "user:pass@ credentials in the URL are supported. " + "(default: LLM_SOCKS5_PROXY environment variable)" + ), + ) args = parser.parse_args() - client = OpenAI(base_url=args.endpoint, api_key=args.key) + http_client: Optional[httpx2.Client] = ( + DefaultHttpx2Client(proxy=args.socks5_proxy) if args.socks5_proxy else None + ) - # We need a version of direct_query that doesn't trigger squashing to use inside the squasher - # to avoid infinite recursion. - raw_direct_query = mk_direct_query(client, args.model, squasher=None) + client = OpenAI(base_url=args.endpoint, api_key=args.key, http_client=http_client) def squash_history(state: DiscussionState) -> DiscussionState: if len(state) <= 1 + args.min_history: @@ -203,7 +216,7 @@ def main(): state[-1], ) else: - (summary, _) = raw_direct_query( + (summary, _) = mk_direct_query(client, args.model, squasher=None)( "Please summarize the preceding conversation history concisely, preserving all key technical decisions, progress made, and current goals.", state[1 : -args.min_history], role="system", @@ -233,6 +246,7 @@ def main(): squasher=squash_history, max_token_count=args.model_context_size, ), + socks5_proxy=args.socks5_proxy, ) state = ( @@ -252,8 +266,14 @@ Once you are finished with your task, give a response with none of the above JSO ) while True: - query = input("> ").strip() + try: + query = input("> ").strip() + except KeyboardInterrupt: + break + # Don't include this in the try above st. if the user + # interrupts the agent, it's possible to find out what the + # program was up to at the moment. (response, state) = query_agent(query, config, state) print(response) diff --git a/web_utils.py b/web_utils.py index 89c6632..1962489 100644 --- a/web_utils.py +++ b/web_utils.py @@ -1,7 +1,7 @@ import httpx2 import subprocess import re -from typing import Dict, Union +from typing import Dict, Optional, Union from dataclasses import dataclass @@ -44,14 +44,18 @@ def html_to_markdown(html: str) -> str: raise RuntimeError(f"Unexpected error during markdown conversion: {e}") -def fetch_and_markdownify(url: str) -> FetchResult: - """Fetches a web page and transforms it to markdown, with memoization.""" +def fetch_and_markdownify(url: str, proxy: Optional[str] = None) -> FetchResult: + """Fetches a web page and transforms it to markdown, with memoization. + + If `proxy` is given (e.g. 'socks5://host:1080'), the request is routed + through that SOCKS5 proxy. + """ if url in _cache: return _cache[url] try: with httpx2.Client( - headers={"User-Agent": "Mozilla/5.0"}, timeout=10.0 + headers={"User-Agent": "Mozilla/5.0"}, timeout=10.0, proxy=proxy ) as client: response = client.get(url) response.raise_for_status() -- cgit v1.3.1