aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--actions.py2
-rwxr-xr-xllm.py36
-rw-r--r--web_utils.py12
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()