Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9b7853c
feat: port BuzzLLM from Python to Rust
RohanAwhad May 31, 2026
fc6ccad
docs: add C4 architecture diagrams for Rust port
RohanAwhad May 31, 2026
bc90a2d
test: add Rust test suite — 126 tests across providers, tools, CLI
RohanAwhad May 31, 2026
fce452c
docs: rewrite README for Rust v2 — providers, tools, coding agent, ex…
RohanAwhad May 31, 2026
3b3bb41
feat: v2.1 — LlmClient trait, coding agent tools, CI workflow, clippy…
RohanAwhad May 31, 2026
74aa09c
bazinga: fix Clippy unnecessary_unwrap on PR #3
RohanAwhad May 31, 2026
39f1651
bazinga: fix cargo fmt + install ripgrep in CI on PR #3
RohanAwhad May 31, 2026
60eea0e
bazinga: fix rg --no-ignore + explicit path on PR #3
RohanAwhad May 31, 2026
a1cdaa1
docs: add system prerequisites to README
RohanAwhad May 31, 2026
5de3527
bazinga: fix no-op output tests — add print_to_writer + assertions on…
RohanAwhad May 31, 2026
b92dfe8
bazinga: add daily log rotation — PR #3 code-review finding #4
RohanAwhad May 31, 2026
4866889
bazinga: restore url as positional arg — PR #3 code-review F1
RohanAwhad May 31, 2026
fce8b09
bazinga: cargo fmt on PR #3
RohanAwhad May 31, 2026
705a6c8
bazinga: multi-tool assembler tests + stale diagram fixes on PR #3
RohanAwhad May 31, 2026
92212aa
bazinga: cargo fmt + fix HashMap ordering in assembler tests on PR #3
RohanAwhad May 31, 2026
357eae1
bazinga: codesearch + websearch tests — PR #3 review items F10 + F6
RohanAwhad May 31, 2026
d547691
bazinga: cargo fmt on PR #3
RohanAwhad May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .dingllm/prds/v2/000_overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# BuzzLLM v2 — Python to Rust Migration

## Goal

Rewrite BuzzLLM from Python to Rust. Produce a single static binary (`buzzllm`) with identical CLI interface, identical provider support, and identical tool capabilities. The Rust code lives in `rust/` within this repo.

## Current Python codebase (~1600 SLOC)

```
src/buzzllm/
main.py 195 lines CLI + orchestration
llm.py 576 lines types, 4 providers, SSE parsing, invoke_llm loop
prompts/ ~255 lines 6 static prompt templates
tools/
utils.py 136 lines tool registry + schema gen
websearch.py 235 lines DDG + Brave + crawl4ai scraping
codesearch.py 256 lines rg/find subprocess wrappers
pythonexec.py 176 lines Docker container + socket protocol
```

## Architecture (Rust)

```
rust/
Cargo.toml
src/
main.rs CLI (clap) + async main
types.rs LlmOptions, RequestArgs, StreamResponse, ToolCall
providers/
mod.rs Provider trait + dispatch enum
openai_chat.rs request builder + SSE parser + message assembler
openai_responses.rs
anthropic.rs
vertexai_anthropic.rs
tools/
mod.rs Tool trait + registry
schema.rs OpenAI/Anthropic JSON schema generation
codesearch.rs bash_find, bash_ripgrep, bash_read
websearch.rs search_web (DDG + Brave), scrape_webpage (chromiumoxide)
pythonexec.rs Docker container lifecycle + socket protocol
prompts/
mod.rs prompt registry (HashMap or match)
*.txt prompt templates loaded via include_str!
output.rs print_to_stdout (ANSI, SSE, brief modes)
```

## Phases

| Phase | PRD | Delivers | Depends on |
|-------|-----|----------|------------|
| 1 | [001_core_types_and_cli.md](001_core_types_and_cli.md) | Cargo project, all types, clap CLI, stubs | nothing |
| 2 | [002_provider_system.md](002_provider_system.md) | Provider trait, all 4 request builders + SSE parsers | Phase 1 |
| 3 | [003_streaming_and_llm_loop.md](003_streaming_and_llm_loop.md) | `invoke_llm` loop, reqwest streaming, stdout output | Phase 2 |
| 4 | [004_tool_system.md](004_tool_system.md) | Tool trait, registry, schema gen | Phase 1 |
| 5 | [005_codesearch.md](005_codesearch.md) | codesearch tools (rg, find, read) | Phase 4 |
| 6 | [006_websearch.md](006_websearch.md) | websearch tools (DDG, Brave, chromiumoxide) | Phase 4 |
| 7 | [007_pythonexec.md](007_pythonexec.md) | pythonexec tool (Docker + socket) | Phase 4 |
| 8 | [008_prompts_and_logging.md](008_prompts_and_logging.md) | Prompt templates, tracing, error handling, final wiring | Phase 3 + 4 |
| 9 | [009_testing.md](009_testing.md) | Test infrastructure, fixtures, unit/integration/e2e tests | All phases |

Phases 5, 6, 7 are independent of each other (all depend on Phase 4 only).
Phase 9 (testing) is written alongside each phase — the PRD defines the full test matrix.

## Crate dependencies

| Crate | Purpose |
|-------|---------|
| `clap` (derive) | CLI arg parsing |
| `serde`, `serde_json` | JSON serialization for all types and API payloads |
| `reqwest` (stream feature) | HTTP client with SSE streaming |
| `tokio` (full) | async runtime |
| `futures` | stream combinators |
| `tracing`, `tracing-subscriber`, `tracing-appender` | logging to `/tmp/buzzllm.logs` |
| `anyhow` | error propagation |
| `bollard` | Docker Engine API (pythonexec) |
| `chromiumoxide` | headless Chrome for webpage scraping |
| `scraper` | HTML parsing (DuckDuckGo results) |
| `tokio-retry` | retry with exponential backoff (websearch) |

## Acceptance criteria

1. `cargo run -- --help` output matches Python `buzzllm -h`
2. All 4 providers stream responses identically (text, reasoning, tool calls)
3. `--system-prompt websearch|codesearch|pythonexec` activates correct tools
4. `--sse`, `--brief`, `--think` flags behave identically
5. Tool results feed back into conversation loop correctly
6. Logs write to `/tmp/buzzllm.logs`
7. Single binary, no Python runtime required

## Non-goals

- No new features beyond current Python parity
- No GUI or TUI
- No config file system (args-only, like current Python)
- No plugin system beyond the existing tool registration pattern
132 changes: 132 additions & 0 deletions .dingllm/prds/v2/001_core_types_and_cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Phase 1: Core Types and CLI

## Goal

Set up the Cargo project in `rust/`, define all core data types, and implement the CLI argument parser. The binary should compile and print parsed args.

## Source reference

- `src/buzzllm/llm.py:15-68` — `LLMOptions`, `RequestArgs`, `StreamResponse`, `ToolCall`
- `src/buzzllm/main.py:26-62` — `parse_args()`

## Deliverables

### 1. Cargo project scaffold

```
rust/
Cargo.toml
src/
main.rs
types.rs
```

`Cargo.toml` dependencies for this phase:
- `clap` (derive feature)
- `serde`, `serde_json`
- `tokio` (full)

### 2. Types (`types.rs`)

Port these Python dataclasses to Rust structs with `serde::Serialize` + `Deserialize`:

#### `LlmOptions`

```rust
pub struct LlmOptions {
pub model: String,
pub url: String,
pub api_key_name: Option<String>,
pub max_tokens: Option<u32>, // default 8192
pub temperature: f64, // default 0.8
pub think: bool, // default false
pub tools: Option<Vec<serde_json::Value>>,
pub max_infer_iters: u32, // default 10
}
```

#### `RequestArgs`

```rust
pub struct RequestArgs {
pub data: serde_json::Value, // JSON body
pub headers: HashMap<String, String>,
}
```

#### `StreamResponse`

```rust
pub enum StreamResponseType {
ResponseStart,
OutputText,
ReasoningContent,
ToolCall,
ToolResult,
BlockEnd,
ResponseEnd,
}

pub struct StreamResponse {
pub id: String,
pub delta: String,
pub response_type: StreamResponseType,
}
```

Must implement `to_json(&self) -> String` for SSE output mode.

#### `ToolCall`

```rust
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: String, // raw JSON string, parsed at execution time
pub executed: bool,
pub result: Option<serde_json::Value>,
}
```

### 3. CLI (`main.rs`)

Use `clap` derive to mirror the exact Python interface:

```
buzzllm <MODEL> <URL> <PROMPT> \
--provider <openai-chat|openai-responses|anthropic|vertexai-anthropic> \
--api-key-name <ENV_VAR> \
[--system-prompt <TEXT>] \
[--max-tokens <N>] \
[--temperature <F>] \
[--think] \
[-S | --sse] \
[-b | --brief]
```

Positional args: `model`, `url`, `prompt` (in that order).

Defaults to match Python:
- `--system-prompt`: `"scream at mee for not setting your system prompt"`
- `--max-tokens`: `8192`
- `--temperature`: `0.8`

Provider is required. `--api-key-name` is required.

### 4. Stub main

```rust
#[tokio::main]
async fn main() {
let args = Cli::parse();
// TODO: wire to chat()
println!("parsed: {:?}", args);
}
```

## Verification

- `cargo build` compiles with no errors
- `cargo run -- --help` prints usage matching Python's `buzzllm -h`
- `cargo run -- "gpt-4o-mini" "https://api.openai.com/v1/chat/completions" "hello" --provider openai-chat --api-key-name OPENAI_API_KEY` parses without error
- Unit test: construct each type, serialize to JSON, verify fields
98 changes: 98 additions & 0 deletions .dingllm/prds/v2/002_provider_system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Phase 2: Provider System

## Goal

Implement all 4 LLM providers as pure functions: request builders, SSE line parsers, and tool-response message assemblers. No networking yet — this phase is fully unit-testable with captured SSE samples.

## Source reference

- `src/buzzllm/llm.py:204-236` — OpenAI Chat request builder
- `src/buzzllm/llm.py:239-309` — OpenAI Chat SSE parser
- `src/buzzllm/llm.py:312-335` — OpenAI Chat tool message assembler
- `src/buzzllm/llm.py:343-369` — Anthropic request builder
- `src/buzzllm/llm.py:372-438` — Anthropic SSE parser
- `src/buzzllm/llm.py:441-468` — Anthropic tool message assembler
- `src/buzzllm/llm.py:476-502` — OpenAI Responses request builder
- `src/buzzllm/llm.py:505-538` — OpenAI Responses SSE parser
- `src/buzzllm/llm.py:554-576` — VertexAI Anthropic request builder

## Deliverables

### File structure

```
rust/src/providers/
mod.rs Provider enum + dispatch
openai_chat.rs
openai_responses.rs
anthropic.rs
vertexai_anthropic.rs
```

### Provider trait (or function triple)

Each provider must supply 3 functions. Use an enum with methods rather than a trait (simpler, no dyn dispatch needed):

```rust
pub enum Provider {
OpenaiChat,
OpenaiResponses,
Anthropic,
VertexaiAnthropic,
}

impl Provider {
pub fn make_request_args(&self, opts: &LlmOptions, prompt: &str, system_prompt: &str) -> RequestArgs;
pub fn parse_sse_line(&self, line: &str, message_started: bool) -> Vec<StreamResponse>;
pub fn assemble_tool_messages(&self, messages: &mut Vec<Value>, tool_calls: &HashMap<String, ToolCall>);
}
```

### Provider-specific details to preserve

#### OpenAI Chat
- Reasoning models list: `gpt-5.2, gpt-5.1, gpt-5, gpt-5-mini, o4-mini, o3, o3-pro, gpt-5-pro` + any model starting with `gpt-5`
- For reasoning models: system role becomes `developer`, add `response_format: {type: "text"}`, add `reasoning_effort` field
- `gpt-5.1` without `--think`: `reasoning_effort: "none"`, all others: `"high"`
- Non-reasoning models get `temperature` and `max_tokens`
- Auth header: `Authorization: Bearer <key>`
- SSE format: `data: {json}\n` lines, terminated by `data: [DONE]`
- Tool calls arrive incrementally: first chunk has `id` + `function.name`, subsequent chunks append to `function.arguments`
- Tracks `current_tool_call_id` across chunks

#### Anthropic
- System prompt goes in top-level `system` field (not in messages)
- Think mode: `max_tokens: 32000`, `thinking: {type: "enabled", budget_tokens: 24000}`
- Auth headers: `x-api-key` + `anthropic-version: 2023-06-01`
- SSE format: `event:` lines (ignored) + `data:` lines with `type` field
- Tool calls: `content_block_start` with `type: "tool_use"` gives id+name, `content_block_delta` with `partial_json` appends arguments
- Tool results: assistant message has `content: [{type: "tool_use", ...}]`, user message has `content: [{type: "tool_result", ...}]`

#### OpenAI Responses
- Uses `input` instead of `messages`, `instructions` instead of system message
- Always sends `reasoning: {effort: "high", summary: "detailed"}` and `store: false`
- Tools: `NotImplementedError` in Python — port this as returning an error
- SSE events: `response.created`, `response.output_text.delta`, `response.reasoning_summary_text.delta`, `response.completed`

#### VertexAI Anthropic
- Same SSE parser and message assembler as Anthropic
- Different request builder: adds `anthropic_version: "vertex-2023-10-16"`
- Auth: calls `gcloud auth print-access-token` via subprocess
- Uses `Authorization: Bearer <token>` (not `x-api-key`)

### Global state migration

Python uses module-level `TOOL_CALLS: dict` and `current_tool_call_id: str`. In Rust:
- Pass `&mut HashMap<String, ToolCall>` and `&mut String` (current_tool_call_id) into the SSE parser
- No global mutable state

## Verification

For each provider, create unit tests with real captured SSE lines:

1. **Request builder test**: call `make_request_args()`, assert JSON body structure, headers, and conditional fields (think mode, reasoning models, tools)
2. **SSE parser test**: feed captured lines from a real API response, assert correct sequence of `StreamResponse` types and deltas
3. **Tool call accumulation test**: feed multi-chunk tool call SSE lines, assert `ToolCall` is assembled correctly with complete name + arguments
4. **Message assembler test**: given a populated `tool_calls` map, assert output messages match expected provider format

Capture real SSE samples by running the Python version with `--sse` and saving output to `rust/tests/fixtures/`.
Loading
Loading