Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7016105521 | |||
| 2e4a79e925 | |||
| 14325cc16e | |||
| d9acd94155 | |||
| 85d5330fc8 | |||
| 9f27228565 | |||
| c27cdf870b | |||
| f9b1b0964e | |||
| fb5aa0070f | |||
| 35616d3e3e | |||
| 64e375e960 | |||
| 3087006561 | |||
| 3fa9f925ff | |||
| 0e27e83b57 | |||
| 33f34e9615 | |||
| 3257f4dc01 | |||
| e633f96338 | |||
| 2149e14573 | |||
| b8400242e4 | |||
| a234b48cda | |||
| 2e671bd27f | |||
| 2cd7286320 | |||
| fa1a5847cf | |||
| 10163277ca | |||
| 1a005b6584 | |||
| a4ae97ef81 | |||
| 378a657595 | |||
| 7f0aba9241 | |||
| e69c82dea9 | |||
| 70597dc453 | |||
| af90aeddf9 | |||
| 175979fb15 | |||
| 367fad475b | |||
| c086336a0f | |||
| 2d252e3221 | |||
| fdb81ee671 | |||
| 22b7e87aaf | |||
| 96d0a034cd | |||
| f07effc51e | |||
| 80629105ed | |||
| 25c75bc89a |
277
README.md
277
README.md
@@ -1,6 +1,6 @@
|
|||||||
# nanocode
|
# nanocode
|
||||||
|
|
||||||
Minimal Claude Code alternative using DSPy ReAct! Single Python file, zero dependencies, ~250 lines.
|
Minimal Claude Code alternative using DSPy RLM! Single Python file, ~305 lines.
|
||||||
|
|
||||||
Built using Claude Code, then used to build itself.
|
Built using Claude Code, then used to build itself.
|
||||||
|
|
||||||
@@ -8,57 +8,294 @@ Built using Claude Code, then used to build itself.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Full agentic loop with tool use
|
- Full agentic loop with tool use via [DSPy RLM](https://dspy.ai/)
|
||||||
- Tools: `read`, `write`, `edit`, `glob`, `grep`, `bash`
|
- Tools: `read`, `write`, `edit`, `glob`, `grep`, `bash`
|
||||||
- Conversation history
|
- Conversation history with context
|
||||||
- Colored terminal output
|
- Colored terminal output
|
||||||
|
- **Modaic Integration**: Push, version, and share as a [Modaic](https://modaic.dev) autoprogram
|
||||||
|
|
||||||
### OpenRouter
|
---
|
||||||
|
|
||||||
Use [OpenRouter](https://openrouter.ai) to access any model:
|
## Prerequisites
|
||||||
|
|
||||||
|
Before using nanocode (or any DSPy RLM-based program), you need to install the Deno code interpreter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install deno
|
||||||
|
```
|
||||||
|
|
||||||
|
This is required for the RLM's code execution capabilities.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Use as a Modaic AutoProgram
|
||||||
|
|
||||||
|
Load and run nanocode directly from the Modaic Hub without cloning:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from modaic import AutoProgram
|
||||||
|
|
||||||
|
# Load the precompiled nanocode agent from Modaic Hub
|
||||||
|
agent = AutoProgram.from_precompiled(
|
||||||
|
"farouk1/nanocode",
|
||||||
|
config={
|
||||||
|
"lm": "openrouter/openai/gpt-5.2-codex",
|
||||||
|
"max_iters": 50
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run a coding task
|
||||||
|
result = agent(task="What Python files are in this directory?")
|
||||||
|
print(result.answer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Run Locally (Interactive CLI)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export OPENROUTER_API_KEY="your-key"
|
export OPENROUTER_API_KEY="your-key"
|
||||||
python nanocode.py
|
python nanocode.py
|
||||||
```
|
```
|
||||||
|
|
||||||
To use a different model:
|
To use a specific model:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export OPENROUTER_API_KEY="your-key"
|
export OPENROUTER_API_KEY="your-key"
|
||||||
export MODEL="openai/gpt-5.2"
|
export MODEL="openai/gpt-4"
|
||||||
python nanocode.py
|
python nanocode.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Commands
|
---
|
||||||
|
|
||||||
- `/c` - Clear conversation
|
## Configuration
|
||||||
- `/q` or `exit` - Quit
|
|
||||||
|
When using as a Modaic AutoProgram, you can configure these options:
|
||||||
|
|
||||||
|
| Parameter | Type | Default | Description |
|
||||||
|
|-----------|------|---------|-------------|
|
||||||
|
| `lm` | str | `openrouter/openai/gpt-5.2-codex` | Primary language model |
|
||||||
|
| `sub_lm` | str | `openrouter/openai/gpt-5-mini` | Sub-LM for reasoning steps |
|
||||||
|
| `max_iters` | int | `50` | Maximum agent iterations |
|
||||||
|
| `api_base` | str | `https://openrouter.ai/api/v1` | API base URL |
|
||||||
|
| `max_tokens` | int | `50000` | Maximum tokens per request |
|
||||||
|
| `max_output_chars` | int | `100000` | Maximum output character limit |
|
||||||
|
| `verbose` | bool | `False` | Enable verbose logging |
|
||||||
|
| `track_usage` | bool | `True` | Track token usage |
|
||||||
|
|
||||||
|
Example with custom configuration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from modaic import AutoProgram
|
||||||
|
|
||||||
|
agent = AutoProgram.from_precompiled(
|
||||||
|
"farouk1/nanocode",
|
||||||
|
config={
|
||||||
|
"lm": "openrouter/anthropic/claude-sonnet-4",
|
||||||
|
"sub_lm": "openrouter/openai/gpt-4.1-mini",
|
||||||
|
"max_iters": 30,
|
||||||
|
"max_tokens": 8000,
|
||||||
|
"verbose": True,
|
||||||
|
"track_usage": False
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/c` | Clear conversation history |
|
||||||
|
| `/q` or `exit` | Quit the application |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
The agent has access to the following tools:
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `read` | Read file with line numbers, offset/limit |
|
| `read_file(path, offset, limit)` | Read file contents with line numbers |
|
||||||
| `write` | Write content to file |
|
| `write_file(path, content)` | Write content to a file |
|
||||||
| `edit` | Replace string in file (must be unique) |
|
| `edit_file(path, old, new, replace_all)` | Replace text in a file (old must be unique unless `replace_all=True`) |
|
||||||
| `glob` | Find files by pattern, sorted by mtime |
|
| `glob_files(pattern, path)` | Find files matching a glob pattern, sorted by modification time |
|
||||||
| `grep` | Search files for regex |
|
| `grep_files(pattern, path)` | Search files for a regex pattern |
|
||||||
| `bash` | Run shell command |
|
| `run_bash(cmd)` | Run a shell command and return output |
|
||||||
|
|
||||||
## Example
|
---
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
### Interactive CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
────────────────────────────────────────
|
────────────────────────────────────────
|
||||||
❯ what files are here?
|
❯ what files are here?
|
||||||
────────────────────────────────────────
|
────────────────────────────────────────
|
||||||
|
|
||||||
⏺ Glob(**/*.py)
|
⏺ Thinking...
|
||||||
⎿ nanocode.py
|
⏺ globfiles(pattern='**/*', path='.')
|
||||||
|
|
||||||
⏺ There's one Python file: nanocode.py
|
⏺ I found the following files:
|
||||||
|
- nanocode.py
|
||||||
|
- README.md
|
||||||
|
- modaic/SKILL.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Programmatic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from modaic import AutoProgram
|
||||||
|
|
||||||
|
agent = AutoProgram.from_precompiled("farouk1/nanocode")
|
||||||
|
|
||||||
|
# Read a file
|
||||||
|
result = agent(task="Read the first 10 lines of nanocode.py")
|
||||||
|
print(result.answer)
|
||||||
|
|
||||||
|
# Search for patterns
|
||||||
|
result = agent(task="Find all functions that contain 'file' in their name")
|
||||||
|
print(result.answer)
|
||||||
|
|
||||||
|
# Make edits
|
||||||
|
result = agent(task="Add a comment at the top of README.md")
|
||||||
|
print(result.answer)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
nanocode.py
|
||||||
|
├── File Operations
|
||||||
|
│ ├── read_file() - Read with line numbers
|
||||||
|
│ ├── write_file() - Write content
|
||||||
|
│ └── edit_file() - Find & replace
|
||||||
|
├── Search Operations
|
||||||
|
│ ├── glob_files() - Pattern matching
|
||||||
|
│ └── grep_files() - Regex search
|
||||||
|
├── Shell Operations
|
||||||
|
│ └── run_bash() - Execute commands
|
||||||
|
├── DSPy Components
|
||||||
|
│ ├── CodingAssistant (Signature)
|
||||||
|
│ ├── RLMCodingProgram (PrecompiledProgram)
|
||||||
|
│ │ ├── forward() - Run agent on task
|
||||||
|
│ │ ├── get_tools() - Get available tools
|
||||||
|
│ │ ├── set_tool() - Add/replace a tool
|
||||||
|
│ │ ├── remove_tool() - Remove a tool
|
||||||
|
│ │ ├── reload_lms() - Recreate LMs from config
|
||||||
|
│ │ └── load_state() - Load state with LM fix
|
||||||
|
│ └── RLMReasoningCallback
|
||||||
|
└── Modaic Integration
|
||||||
|
└── RLMCodingConfig (PrecompiledConfig)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Classes
|
||||||
|
|
||||||
|
#### `RLMCodingConfig`
|
||||||
|
Configuration class extending `PrecompiledConfig` for experiment-specific parameters.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RLMCodingConfig(PrecompiledConfig):
|
||||||
|
max_iters: int = 50
|
||||||
|
lm: str = "openrouter/openai/gpt-5.2-codex"
|
||||||
|
sub_lm: str = "openrouter/openai/gpt-5-mini"
|
||||||
|
api_base: str = "https://openrouter.ai/api/v1"
|
||||||
|
max_tokens: int = 50000
|
||||||
|
max_output_chars: int = 100000
|
||||||
|
verbose: bool = False
|
||||||
|
track_usage: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `RLMCodingProgram`
|
||||||
|
Main program class extending `PrecompiledProgram`. Wraps a DSPy RLM agent with coding tools.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RLMCodingProgram(PrecompiledProgram):
|
||||||
|
config: RLMCodingConfig
|
||||||
|
|
||||||
|
def forward(self, task: str) -> dspy.Prediction:
|
||||||
|
# Returns prediction with .answer
|
||||||
|
return self.agent(task=task)
|
||||||
|
|
||||||
|
def get_tools(self) -> dict:
|
||||||
|
# Returns dict of available tools
|
||||||
|
|
||||||
|
def set_tool(self, name: str, tool: callable):
|
||||||
|
# Add or replace a tool
|
||||||
|
|
||||||
|
def remove_tool(self, name: str):
|
||||||
|
# Remove a tool by name
|
||||||
|
|
||||||
|
def reload_lms(self):
|
||||||
|
# Recreate LM objects from current config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `CodingAssistant`
|
||||||
|
DSPy Signature defining the agent's input/output schema.
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CodingAssistant(dspy.Signature):
|
||||||
|
"""You are a concise coding assistant with access to sub agents."""
|
||||||
|
|
||||||
|
task: str = dspy.InputField(desc="The user's coding task or question")
|
||||||
|
answer: str = dspy.OutputField(desc="Your response to the user after completing the task")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Publishing Your Own Version
|
||||||
|
|
||||||
|
If you modify nanocode and want to publish your own version to Modaic Hub:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from nanocode import RLMCodingProgram, RLMCodingConfig
|
||||||
|
|
||||||
|
# Create and optionally optimize your program
|
||||||
|
program = RLMCodingProgram(RLMCodingConfig())
|
||||||
|
|
||||||
|
# Push to your Modaic Hub repo
|
||||||
|
program.push_to_hub(
|
||||||
|
"your-username/my-nanocode",
|
||||||
|
commit_message="My customized nanocode",
|
||||||
|
with_code=True # Include source code for AutoProgram loading
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [DSPy](https://dspy.ai/) - Framework for programming language models
|
||||||
|
- [Modaic](https://modaic.dev/) - Hub for sharing and versioning DSPy programs
|
||||||
|
- OpenRouter API key (for accessing language models)
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install dspy modaic
|
||||||
|
# or with uv
|
||||||
|
uv add dspy modaic
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| `OPENROUTER_API_KEY` | Yes | Your OpenRouter API key |
|
||||||
|
| `MODEL` | No | Override the default model selection |
|
||||||
|
| `MODAIC_TOKEN` | For Hub | Required for pushing/loading from Modaic Hub |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
12
config.json
12
config.json
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"model": null,
|
"model": null,
|
||||||
"max_iters": 20,
|
"max_iters": 50,
|
||||||
"lm": "openai/gpt-5.2-codex",
|
"lm": "openrouter/anthropic/claude-opus-4.5",
|
||||||
"sub_lm": "openrouter/openai/gpt-4.1",
|
"sub_lm": "openrouter/qwen/qwen-coder",
|
||||||
"api_base": "https://openrouter.ai/api/v1",
|
"api_base": "https://openrouter.ai/api/v1",
|
||||||
"max_tokens": 16000,
|
"max_tokens": 50000,
|
||||||
"max_output_chars": 100000,
|
"max_output_chars": 100000,
|
||||||
"verbose": false
|
"verbose": true,
|
||||||
|
"track_usage": true,
|
||||||
|
"track_trace": false
|
||||||
}
|
}
|
||||||
490
nanocode.py
490
nanocode.py
@@ -1,17 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import glob as globlib
|
|
||||||
import subprocess
|
|
||||||
from modaic import PrecompiledProgram, PrecompiledConfig
|
from modaic import PrecompiledProgram, PrecompiledConfig
|
||||||
import dspy
|
import dspy
|
||||||
|
import weave
|
||||||
|
import subprocess
|
||||||
from dspy.utils.callback import BaseCallback
|
from dspy.utils.callback import BaseCallback
|
||||||
|
|
||||||
# --- Modaic ---
|
|
||||||
|
|
||||||
MODAIC_REPO_PATH = "farouk1/nanocode"
|
MODAIC_REPO_PATH = "farouk1/nanocode"
|
||||||
|
|
||||||
# --- ANSI colors ---
|
|
||||||
|
|
||||||
RESET = "\033[0m"
|
RESET = "\033[0m"
|
||||||
BOLD = "\033[1m"
|
BOLD = "\033[1m"
|
||||||
DIM = "\033[2m"
|
DIM = "\033[2m"
|
||||||
@@ -22,24 +17,11 @@ YELLOW = "\033[33m"
|
|||||||
RED = "\033[31m"
|
RED = "\033[31m"
|
||||||
MAGENTA = "\033[35m"
|
MAGENTA = "\033[35m"
|
||||||
|
|
||||||
# --- Display utilities ---
|
|
||||||
|
|
||||||
|
|
||||||
def separator():
|
|
||||||
"""Return a horizontal separator line that fits the terminal width."""
|
|
||||||
return f"{DIM}{'─' * min(os.get_terminal_size().columns, 80)}{RESET}"
|
|
||||||
|
|
||||||
|
|
||||||
def render_markdown(text):
|
|
||||||
"""Convert basic markdown bold syntax to ANSI bold."""
|
|
||||||
return re.sub(r"\*\*(.+?)\*\*", f"{BOLD}\\1{RESET}", text)
|
|
||||||
|
|
||||||
|
|
||||||
# --- File operations ---
|
# --- File operations ---
|
||||||
|
|
||||||
|
|
||||||
def read_file(path: str, offset: int = 0, limit: int = None) -> str:
|
def read_file(path: str, offset: int = 0, limit: int = None) -> str:
|
||||||
"""Read file contents with line numbers.
|
"""[EXTERNAL FILESYSTEM] Read file contents from disk with line numbers.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path to the file to read
|
path: Path to the file to read
|
||||||
@@ -53,26 +35,45 @@ def read_file(path: str, offset: int = 0, limit: int = None) -> str:
|
|||||||
if limit is None:
|
if limit is None:
|
||||||
limit = len(lines)
|
limit = len(lines)
|
||||||
selected = lines[offset : offset + limit]
|
selected = lines[offset : offset + limit]
|
||||||
return "".join(f"{offset + idx + 1:4}| {line}" for idx, line in enumerate(selected))
|
content = "".join(
|
||||||
|
f"{offset + idx + 1:4}| {line}" for idx, line in enumerate(selected)
|
||||||
|
)
|
||||||
|
tokens = len(content) // 4 # ~4 chars per token estimate
|
||||||
|
print(f"{MAGENTA}⏺ Reading file({path}) (~{tokens:,} tokens){RESET}")
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
def write_file(path: str, content: str) -> str:
|
def write_file(path: str, content: str) -> str:
|
||||||
"""Write content to a file.
|
"""[EXTERNAL FILESYSTEM] Write content to a file on disk (creates or overwrites).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path to the file to write
|
path: Path to the file to write
|
||||||
content: Content to write to the file
|
content: Content to write to the file
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
'ok' on success
|
Status message with file stats
|
||||||
"""
|
"""
|
||||||
|
is_new = not os.path.exists(path)
|
||||||
|
action = "Creating" if is_new else "Overwriting"
|
||||||
|
|
||||||
|
# Auto-create parent directories
|
||||||
|
parent = os.path.dirname(path)
|
||||||
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
|
||||||
with open(path, "w") as f:
|
with open(path, "w") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
return "ok"
|
|
||||||
|
lines = content.count("\n") + (1 if content and not content.endswith("\n") else 0)
|
||||||
|
tokens = len(content) // 4
|
||||||
|
print(
|
||||||
|
f"{MAGENTA}⏺ {action} file({path}) ({lines} lines, ~{tokens:,} tokens){RESET}"
|
||||||
|
)
|
||||||
|
return f"ok: wrote {lines} lines ({tokens:,} tokens) to {path}"
|
||||||
|
|
||||||
|
|
||||||
def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str:
|
def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str:
|
||||||
"""Replace text in a file.
|
"""[EXTERNAL FILESYSTEM] Replace text in a file on disk.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: Path to the file to edit
|
path: Path to the file to edit
|
||||||
@@ -83,6 +84,8 @@ def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
'ok' on success, error message on failure
|
'ok' on success, error message on failure
|
||||||
"""
|
"""
|
||||||
|
print(f"{MAGENTA}⏺ Edit({path}){RESET}")
|
||||||
|
|
||||||
text = open(path).read()
|
text = open(path).read()
|
||||||
if old not in text:
|
if old not in text:
|
||||||
return "error: old_string not found"
|
return "error: old_string not found"
|
||||||
@@ -96,7 +99,9 @@ def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def glob_files(pattern: str, path: str = ".") -> str:
|
def glob_files(pattern: str, path: str = ".") -> str:
|
||||||
"""Find files matching a glob pattern, sorted by modification time.
|
"""[EXTERNAL FILESYSTEM] Do not use for simple file listing, run bash instead. Find files on disk matching a glob pattern.
|
||||||
|
|
||||||
|
Respects .gitignore files automatically via ripgrep. Sorted by modification time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pattern: Glob pattern to match (e.g., '**/*.py')
|
pattern: Glob pattern to match (e.g., '**/*.py')
|
||||||
@@ -105,43 +110,60 @@ def glob_files(pattern: str, path: str = ".") -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Newline-separated list of matching files
|
Newline-separated list of matching files
|
||||||
"""
|
"""
|
||||||
full_pattern = (path + "/" + pattern).replace("//", "/")
|
print(f"{MAGENTA}⏺ Glob({pattern}): {path}{RESET}")
|
||||||
files = globlib.glob(full_pattern, recursive=True)
|
|
||||||
files = sorted(
|
cmd = ["rg", "--files", "--no-require-git", "-g", pattern, path]
|
||||||
files,
|
try:
|
||||||
key=lambda f: os.path.getmtime(f) if os.path.isfile(f) else 0,
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
reverse=True,
|
files = result.stdout.strip().split("\n") if result.stdout.strip() else []
|
||||||
)
|
files = sorted(
|
||||||
return "\n".join(files) or "no files found"
|
files,
|
||||||
|
key=lambda f: os.path.getmtime(f) if os.path.isfile(f) else 0,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return "\n".join(files) or "no files found"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "error: ripgrep (rg) not installed - install with 'brew install ripgrep'"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "error: search timed out after 30s"
|
||||||
|
|
||||||
|
|
||||||
def grep_files(pattern: str, path: str = ".") -> str:
|
def grep_files(
|
||||||
"""Search files for a regex pattern.
|
pattern: str, path: str = ".", glob: str = None, max_results: int = 50
|
||||||
|
) -> str:
|
||||||
|
"""[EXTERNAL FILESYSTEM] Search files on disk for a regex pattern using ripgrep.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pattern: Regular expression pattern to search for
|
pattern: Regular expression pattern to search for
|
||||||
path: Base directory to search in
|
path: Base directory to search in
|
||||||
|
glob: Optional glob pattern to filter files (e.g., '*.py')
|
||||||
|
max_results: Maximum number of results to return
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Matching lines in format 'filepath:line_num:content'
|
Matching lines in format 'filepath:line_num:content'
|
||||||
"""
|
"""
|
||||||
regex = re.compile(pattern)
|
print(f"{MAGENTA}⏺ Grep: {pattern}{RESET}")
|
||||||
hits = []
|
|
||||||
for filepath in globlib.glob(path + "/**", recursive=True):
|
cmd = ["rg", "-n", "--no-heading", "--color=never", f"-m{max_results}"]
|
||||||
try:
|
if glob:
|
||||||
for line_num, line in enumerate(open(filepath), 1):
|
cmd.extend(["-g", glob])
|
||||||
if regex.search(line):
|
cmd.extend([pattern, path])
|
||||||
hits.append(f"{filepath}:{line_num}:{line.rstrip()}")
|
|
||||||
except Exception:
|
try:
|
||||||
pass
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
return "\n".join(hits[:50]) or "no matches found"
|
output = result.stdout.strip()
|
||||||
|
return output if output else "no matches found"
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "error: ripgrep (rg) not installed - install with 'brew install ripgrep'"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "error: search timed out after 30s"
|
||||||
|
|
||||||
|
|
||||||
# --- Shell operations ---
|
# --- Shell operations ---
|
||||||
|
|
||||||
|
|
||||||
def run_bash(cmd: str) -> str:
|
def run_bash(cmd: str) -> str:
|
||||||
"""Run a shell command and return output.
|
"""[EXTERNAL SYSTEM] Run a shell command on the host machine.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cmd: Shell command to execute
|
cmd: Shell command to execute
|
||||||
@@ -149,6 +171,8 @@ def run_bash(cmd: str) -> str:
|
|||||||
Returns:
|
Returns:
|
||||||
Command output (stdout and stderr combined)
|
Command output (stdout and stderr combined)
|
||||||
"""
|
"""
|
||||||
|
print(f"{MAGENTA}⏺ Bash: {cmd}{RESET}")
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
|
||||||
)
|
)
|
||||||
@@ -168,223 +192,249 @@ def run_bash(cmd: str) -> str:
|
|||||||
return "".join(output_lines).strip() or "(empty output)"
|
return "".join(output_lines).strip() or "(empty output)"
|
||||||
|
|
||||||
|
|
||||||
# --- Model selection ---
|
class RLMReasoningCallback(BaseCallback):
|
||||||
|
def on_module_end(self, call_id, outputs, exception):
|
||||||
AVAILABLE_MODELS = {
|
if outputs and hasattr(outputs, "reasoning") and hasattr(outputs, "code"):
|
||||||
"1": ("GPT-5.2 Codex", "openai/gpt-5.2-codex"),
|
has_backticks = "```" in outputs.code
|
||||||
"2": ("GPT-5.2", "openai/gpt-5.2"),
|
print(f"{DIM}⏺ [REASONING STEP]\n{outputs.reasoning}\n{RESET}")
|
||||||
"3": ("Claude Opus 4.5", "anthropic/claude-opus-4.5"),
|
if has_backticks:
|
||||||
"4": ("Claude Opus 4", "anthropic/claude-opus-4"),
|
print(f"{DIM}⏺ [CODE]\n{outputs.code}\n{RESET}")
|
||||||
"5": ("Qwen 3 Coder", "qwen/qwen3-coder"),
|
|
||||||
"6": ("Gemini 3 Flash Preview", "google/gemini-3-flash-preview"),
|
|
||||||
"7": ("Kimi K2 0905", "moonshotai/kimi-k2-0905"),
|
|
||||||
"8": ("Minimax M2.1", "minimax/minimax-m2.1"),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def select_model():
|
|
||||||
"""Interactive model selection or use environment variable."""
|
|
||||||
model_env = os.getenv("MODEL")
|
|
||||||
if model_env:
|
|
||||||
print(f"{GREEN}⏺ Using model from environment: {model_env}{RESET}")
|
|
||||||
return model_env
|
|
||||||
|
|
||||||
print(f"\n{BOLD}Select a model:{RESET}")
|
|
||||||
for key, (name, model_id) in AVAILABLE_MODELS.items():
|
|
||||||
print(f" {BLUE}{key}{RESET}. {name} ({DIM}{model_id}{RESET})")
|
|
||||||
print(f" {BLUE}c{RESET}. Custom model (enter manually)")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
choice = (
|
|
||||||
input(f"\n{BOLD}{BLUE}❯{RESET} Enter choice (1-8 or c): ")
|
|
||||||
.strip()
|
|
||||||
.lower()
|
|
||||||
)
|
|
||||||
|
|
||||||
if choice in AVAILABLE_MODELS:
|
|
||||||
name, model_id = AVAILABLE_MODELS[choice]
|
|
||||||
print(f"{GREEN}⏺ Selected: {name}{RESET}")
|
|
||||||
return model_id
|
|
||||||
elif choice == "c":
|
|
||||||
custom_model = input(
|
|
||||||
f"{BOLD}{BLUE}❯{RESET} Enter model ID (e.g., openai/gpt-4): "
|
|
||||||
).strip()
|
|
||||||
if custom_model:
|
|
||||||
print(f"{GREEN}⏺ Selected custom model: {custom_model}{RESET}")
|
|
||||||
return custom_model
|
|
||||||
else:
|
|
||||||
print(f"{RED}⏺ Invalid model ID{RESET}")
|
|
||||||
else:
|
else:
|
||||||
print(f"{RED}⏺ Invalid choice. Please enter 1-8 or c{RESET}")
|
print(f"{DIM}⏺ [CODE]\n```\n{outputs.code}\n```\n{RESET}")
|
||||||
except (KeyboardInterrupt, EOFError):
|
|
||||||
print(f"\n{RED}⏺ Model selection cancelled{RESET}")
|
|
||||||
exit(1)
|
# -- Program ---
|
||||||
|
|
||||||
|
|
||||||
class CodingAssistant(dspy.Signature):
|
class CodingAssistant(dspy.Signature):
|
||||||
"""You are a concise coding assistant. Help the user with their coding task by using the available tools to read, write, edit files, search the codebase, and run commands."""
|
"""You are a concise coding assistant.
|
||||||
|
|
||||||
|
CRITICAL - Two execution environments exist:
|
||||||
|
|
||||||
|
1. INTERNAL REPL (sandbox): Standard Python code you write executes in an isolated sandbox. Variables persist between iterations. Use for data processing, string manipulation, logic, loops, etc.
|
||||||
|
|
||||||
|
2. EXTERNAL TOOLS (real system): Functions like read_file(), write_file(), run_bash(), glob_files(), grep_files() execute OUTSIDE the sandbox on the real filesystem and host machine. These have real, persistent side effects.
|
||||||
|
|
||||||
|
When you need to:
|
||||||
|
- Process data, do math, manipulate strings, iterate → write Python code directly in the REPL
|
||||||
|
- Read/write actual files on disk → call read_file(), write_file(), edit_file()
|
||||||
|
- Run shell commands on the host → call run_bash()
|
||||||
|
- Search the codebase → call glob_files(), grep_files()
|
||||||
|
|
||||||
|
Do NOT confuse REPL variables with external files. Reading a file into a variable does not mean the variable updates if the file changes - you must call read_file() again."""
|
||||||
|
|
||||||
task: str = dspy.InputField(desc="The user's coding task or question")
|
task: str = dspy.InputField(desc="The user's coding task or question")
|
||||||
answer: str = dspy.OutputField(
|
answer: str = dspy.OutputField(
|
||||||
desc="Your response to the user after completing the task"
|
desc="Your response to the user after completing the task"
|
||||||
)
|
)
|
||||||
affected_files: list[str] = dspy.OutputField(
|
|
||||||
desc="List of files that were written or modified during the task"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
tools = {
|
|
||||||
"readfile": read_file,
|
|
||||||
"writefile": write_file,
|
|
||||||
"editfile": edit_file,
|
|
||||||
"globfiles": glob_files,
|
|
||||||
"grepfiles": grep_files,
|
|
||||||
"runbash": run_bash,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ToolLoggingCallback(BaseCallback):
|
|
||||||
"""Callback that logs tool calls as they happen."""
|
|
||||||
|
|
||||||
def on_tool_start(self, call_id, instance, inputs):
|
|
||||||
"""Log when a tool starts executing."""
|
|
||||||
tool_name = instance.name if hasattr(instance, "name") else str(instance)
|
|
||||||
# Format args nicely
|
|
||||||
args_str = ", ".join(f"{k}={repr(v)[:50]}" for k, v in inputs.items())
|
|
||||||
print(f" {MAGENTA}⏺ {tool_name}({args_str}){RESET}", flush=True)
|
|
||||||
|
|
||||||
def on_tool_end(self, call_id, outputs, exception):
|
|
||||||
"""Log when a tool finishes executing."""
|
|
||||||
if exception:
|
|
||||||
print(f" {RED}Error: {exception}{RESET}", flush=True)
|
|
||||||
|
|
||||||
def on_module_end(self, call_id, outputs, exception):
|
|
||||||
"""Log when the finish tool is called (ReAct completion)."""
|
|
||||||
# Check if this is a ReAct prediction with tool_calls
|
|
||||||
if outputs and "tool_calls" in outputs:
|
|
||||||
for call in outputs["tool_calls"]:
|
|
||||||
args_str = ", ".join(
|
|
||||||
f"{k}={repr(v)[:50]}" for k, v in call.args.items()
|
|
||||||
)
|
|
||||||
if call.name == "finish":
|
|
||||||
print(f" {GREEN}⏺ finish{RESET}", flush=True)
|
|
||||||
else:
|
|
||||||
print(f" {MAGENTA}⏺ {call.name}({args_str}){RESET}", flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
class RLMCodingConfig(PrecompiledConfig):
|
class RLMCodingConfig(PrecompiledConfig):
|
||||||
max_iters: int = 20
|
max_iters: int = 50
|
||||||
lm: str = "openrouter/anthropic/claude-3.5-sonnet" # Default fallback
|
lm: str = "openrouter/anthropic/claude-opus-4.5"
|
||||||
sub_lm: str = "openrouter/openai/gpt-4.1" # Default fallback
|
sub_lm: str = "openrouter/qwen/qwen-coder"
|
||||||
api_base: str = "https://openrouter.ai/api/v1"
|
api_base: str = "https://openrouter.ai/api/v1"
|
||||||
max_tokens: int = 16000
|
max_tokens: int = 50000
|
||||||
max_output_chars: int = 100000
|
max_output_chars: int = 100000
|
||||||
verbose: bool = False
|
verbose: bool = True
|
||||||
|
track_usage: bool = True
|
||||||
|
track_trace: bool = False
|
||||||
|
|
||||||
|
|
||||||
class RLMCodingProgram(PrecompiledProgram):
|
class RLMCodingProgram(PrecompiledProgram):
|
||||||
config: RLMCodingConfig
|
config: RLMCodingConfig
|
||||||
|
|
||||||
|
def ensure_config(self, config):
|
||||||
|
"""Override to fix Python 3.14 compatibility issue with __annotations__ access."""
|
||||||
|
ConfigClass = self.__class__.__annotations__.get("config", PrecompiledConfig)
|
||||||
|
if config is None:
|
||||||
|
config = ConfigClass()
|
||||||
|
elif isinstance(config, dict):
|
||||||
|
config = ConfigClass(**config)
|
||||||
|
elif type(config) is not ConfigClass:
|
||||||
|
raise ValueError(
|
||||||
|
f"config must be an instance of {self.__class__.__name__}.config, got {type(config)}"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
def __init__(self, config: RLMCodingConfig, **kwargs):
|
def __init__(self, config: RLMCodingConfig, **kwargs):
|
||||||
self.config = config
|
|
||||||
super().__init__(config, **kwargs)
|
super().__init__(config, **kwargs)
|
||||||
|
|
||||||
# tool logging for introspections on multi-turn conversations
|
if config.track_trace:
|
||||||
dspy.settings.configure(callbacks=[ToolLoggingCallback()])
|
project = kwargs.get("project", os.getenv("WANDB_PROJECT"))
|
||||||
lm = dspy.LM(
|
if project is None:
|
||||||
self.config.lm,
|
raise ValueError("project is required when track_trace is True")
|
||||||
|
|
||||||
|
wandb_key = kwargs.get("wandb_key", os.getenv("WANDB_API_KEY"))
|
||||||
|
if wandb_key is None:
|
||||||
|
raise ValueError("wandb_key is required when track_trace is True")
|
||||||
|
|
||||||
|
os.environ["WANDB_PROJECT"] = project
|
||||||
|
os.environ["WANDB_API_KEY"] = wandb_key
|
||||||
|
weave.init(project_name=project)
|
||||||
|
|
||||||
|
self.config = config
|
||||||
|
self.tools = {
|
||||||
|
"read_file": read_file,
|
||||||
|
"write_file": write_file,
|
||||||
|
"edit_file": edit_file,
|
||||||
|
"glob_files": glob_files,
|
||||||
|
"grep_files": grep_files,
|
||||||
|
"run_bash": run_bash,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lm = dspy.LM(
|
||||||
|
model=self.config.lm,
|
||||||
api_base=self.config.api_base,
|
api_base=self.config.api_base,
|
||||||
max_tokens=self.config.max_tokens,
|
max_tokens=self.config.max_tokens,
|
||||||
|
track_usage=self.config.track_usage,
|
||||||
)
|
)
|
||||||
sub_lm = dspy.LM(
|
self.sub_lm = dspy.LM(
|
||||||
self.config.sub_lm,
|
model=self.config.sub_lm,
|
||||||
api_base=self.config.api_base,
|
api_base=self.config.api_base,
|
||||||
max_tokens=self.config.max_tokens,
|
max_tokens=self.config.max_tokens,
|
||||||
|
track_usage=self.config.track_usage,
|
||||||
)
|
)
|
||||||
agent = dspy.RLM(
|
self.agent = dspy.RLM(
|
||||||
CodingAssistant,
|
CodingAssistant,
|
||||||
sub_lm=sub_lm,
|
sub_lm=self.sub_lm,
|
||||||
tools=tools,
|
tools=self.tools,
|
||||||
max_output_chars=self.config.max_output_chars,
|
max_output_chars=self.config.max_output_chars,
|
||||||
max_iterations=self.config.max_iters,
|
max_iterations=self.config.max_iters,
|
||||||
verbose=self.config.verbose,
|
verbose=False, # We add our own verbose logging
|
||||||
)
|
)
|
||||||
|
self.agent.set_lm(self.lm)
|
||||||
|
|
||||||
agent.set_lm(lm)
|
if self.config.verbose:
|
||||||
self.agent = agent
|
self.add_logging_callbacks()
|
||||||
|
|
||||||
|
def add_logging_callbacks(self):
|
||||||
|
"""Add logging callbacks to the agent."""
|
||||||
|
|
||||||
|
self.agent.generate_action.callbacks.append(RLMReasoningCallback())
|
||||||
|
self._patch_llm_tools()
|
||||||
|
|
||||||
|
def _patch_llm_tools(self):
|
||||||
|
"""Monkey-patch the RLM's _make_llm_tools to add structured verbose logging."""
|
||||||
|
|
||||||
|
orig_factory = (
|
||||||
|
self.agent._make_llm_tools
|
||||||
|
) # capture the original bound method directly
|
||||||
|
|
||||||
|
def verbose_factory(max_workers=8):
|
||||||
|
tools = orig_factory(
|
||||||
|
max_workers=max_workers
|
||||||
|
) # call the original bound method
|
||||||
|
|
||||||
|
orig_q = tools["llm_query"]
|
||||||
|
orig_b = tools["llm_query_batched"]
|
||||||
|
|
||||||
|
def wrapped_q(prompt): # wrap query
|
||||||
|
print(
|
||||||
|
f"{DIM}⏺ [LLM QUERY]:\n{prompt[:100]}...{RESET}\n"
|
||||||
|
if len(prompt) > 100
|
||||||
|
else f"{DIM}⏺ [LLM QUERY]:\n{prompt}{RESET}\n"
|
||||||
|
)
|
||||||
|
res = orig_q(prompt)
|
||||||
|
print(
|
||||||
|
f"{DIM}⏺ [LLM QUERY RESULT]:\n{str(res)[:200]}...{RESET}\n"
|
||||||
|
if len(str(res)) > 200
|
||||||
|
else f"{DIM}⏺ [LLM QUERY RESULT]:\n{res}{RESET}\n"
|
||||||
|
)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def wrapped_b(prompts): # wrap batched query
|
||||||
|
print(f"{DIM}⏺ [LLM QUERY BATCHED]:\n{len(prompts)} prompts{RESET}\n")
|
||||||
|
res = orig_b(prompts)
|
||||||
|
print(f"{DIM}⏺ [LLM QUERY BATCHED]:\n{len(res)} results{RESET}\n")
|
||||||
|
return res
|
||||||
|
|
||||||
|
tools["llm_query"] = wrapped_q
|
||||||
|
tools["llm_query_batched"] = wrapped_b
|
||||||
|
return tools
|
||||||
|
|
||||||
|
self.agent._make_llm_tools = verbose_factory
|
||||||
|
|
||||||
def forward(self, task: str) -> str:
|
def forward(self, task: str) -> str:
|
||||||
assert task, "Task cannot be empty"
|
"""Forward pass for the agent."""
|
||||||
|
if not task:
|
||||||
|
return dspy.Prediction(answer="No Task Given.")
|
||||||
|
|
||||||
return self.agent(task=task)
|
return self.agent(task=task)
|
||||||
|
|
||||||
def main():
|
def get_tools(self):
|
||||||
model = os.getenv("MODEL")
|
"""Get the tools for the agent."""
|
||||||
if model is None:
|
return self.tools
|
||||||
model = select_model()
|
|
||||||
|
|
||||||
# Add openrouter/ prefix if not already present
|
def set_tool(self, name: str, tool: callable):
|
||||||
if not model.startswith("openrouter/"):
|
"""Set a tool for the agent."""
|
||||||
model = f"openrouter/{model}"
|
self.tools[name] = tool
|
||||||
|
self.reload_repl()
|
||||||
|
|
||||||
config = RLMCodingConfig()
|
def remove_tool(self, name: str):
|
||||||
config.lm = model
|
"""Remove a tool from the agent."""
|
||||||
|
if name in self.tools:
|
||||||
|
del self.tools[name]
|
||||||
|
self.reload_repl()
|
||||||
|
|
||||||
agent = RLMCodingProgram(config)
|
def reload_repl(
|
||||||
print(
|
self,
|
||||||
f"{BOLD}nanocode-dspy{RESET} | {DIM}{agent.config.lm} | {os.getcwd()}{RESET}\n"
|
): # We need to create a new instance for tool mutations to be passed back into the REPL
|
||||||
)
|
"""Reload the REPL with the current tools."""
|
||||||
|
|
||||||
# Conversation history for context
|
new_instance = dspy.RLM(
|
||||||
history = []
|
CodingAssistant,
|
||||||
|
sub_lm=self.sub_lm,
|
||||||
|
tools=self.tools,
|
||||||
|
max_output_chars=self.config.max_output_chars,
|
||||||
|
max_iterations=self.config.max_iters,
|
||||||
|
verbose=False, # We add our own verbose logging
|
||||||
|
)
|
||||||
|
new_instance.set_lm(self.lm)
|
||||||
|
self.agent = new_instance
|
||||||
|
if self.config.verbose:
|
||||||
|
self.add_logging_callbacks()
|
||||||
|
|
||||||
while True:
|
def reload_lms(self):
|
||||||
try:
|
"""Recreate LM objects from current config. Call this after changing config.lm or config.sub_lm."""
|
||||||
print(separator())
|
|
||||||
user_input = input(f"{BOLD}{BLUE}❯{RESET} ").strip()
|
|
||||||
print(separator())
|
|
||||||
|
|
||||||
if not user_input:
|
self.lm = dspy.LM(
|
||||||
continue
|
model=self.config.lm,
|
||||||
if user_input in ("/q", "exit"):
|
api_base=self.config.api_base,
|
||||||
break
|
max_tokens=self.config.max_tokens,
|
||||||
if user_input == "/c":
|
track_usage=self.config.track_usage,
|
||||||
history = []
|
)
|
||||||
print(f"{GREEN}⏺ Cleared conversation{RESET}")
|
self.sub_lm = dspy.LM(
|
||||||
continue
|
model=self.config.sub_lm,
|
||||||
|
api_base=self.config.api_base,
|
||||||
|
max_tokens=self.config.max_tokens,
|
||||||
|
track_usage=self.config.track_usage,
|
||||||
|
)
|
||||||
|
self.reload_repl()
|
||||||
|
if os.getenv("MODAIC_ENV") == "dev":
|
||||||
|
print(f"{BLUE}LMs RELOADED: {self.lm.model}, {self.sub_lm.model}{RESET}")
|
||||||
|
|
||||||
# Build context from history
|
def load_state(self, state):
|
||||||
context = f"Working directory: {os.getcwd()}\n"
|
"""Override to recreate LMs from config after loading state.
|
||||||
if history:
|
|
||||||
context += "\nPrevious conversation:\n"
|
|
||||||
for h in history[-5:]: # Keep last 5 exchanges
|
|
||||||
context += f"User: {h['user']}\nAssistant: {h['assistant']}\n\n"
|
|
||||||
|
|
||||||
task = f"{context}\nCurrent task: {user_input}"
|
PrecompiledProgram.from_precompiled() calls load_state() AFTER __init__,
|
||||||
|
which overwrites our LMs with saved state. We fix this by recreating
|
||||||
print(f"\n{CYAN}⏺{RESET} Thinking...", flush=True)
|
the LMs from self.config after the parent load_state runs. Modaic will
|
||||||
|
fix this in a later patch for future devs.
|
||||||
# Run the ReAct agent
|
"""
|
||||||
result = agent(task=task)
|
super().load_state(state)
|
||||||
|
self.reload_lms() # Recreate LMs from config (not from saved state)
|
||||||
# Display the answer
|
|
||||||
print(f"\n{CYAN}⏺{RESET} {render_markdown(result.answer)}")
|
|
||||||
|
|
||||||
# Save to history
|
|
||||||
history.append({"user": user_input, "assistant": result.answer})
|
|
||||||
|
|
||||||
print()
|
|
||||||
|
|
||||||
except (KeyboardInterrupt, EOFError):
|
|
||||||
break
|
|
||||||
except Exception as err:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
print(f"{RED}⏺ Error: {err}{RESET}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
agent = RLMCodingProgram(RLMCodingConfig(lm="openai/gpt-5.2-codex"))
|
agent = RLMCodingProgram(RLMCodingConfig())
|
||||||
agent.push_to_hub(MODAIC_REPO_PATH, commit_message="Switch to RLM instead of ReAct", tag="v0.0.1")
|
|
||||||
#main()
|
# agent(task="what's 1 + 1?")
|
||||||
|
|
||||||
|
branches = ["dev"]
|
||||||
|
for branch in branches:
|
||||||
|
agent.push_to_hub(
|
||||||
|
MODAIC_REPO_PATH,
|
||||||
|
commit_message="Remove list_files tool",
|
||||||
|
branch=branch,
|
||||||
|
)
|
||||||
|
|||||||
22
program.json
22
program.json
@@ -4,7 +4,7 @@
|
|||||||
"train": [],
|
"train": [],
|
||||||
"demos": [],
|
"demos": [],
|
||||||
"signature": {
|
"signature": {
|
||||||
"instructions": "You are a concise coding assistant. Help the user with their coding task by using the available tools to read, write, edit files, search the codebase, and run commands.\n\nYou are tasked with producing the following outputs given the inputs `task`:\n- {answer}\n- {affected_files} # note: the value you produce must adhere to the JSON schema: {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n\nYou have access to a Python REPL environment. Write Python code and it will be executed. You will see the output, then write more code based on what you learned. This is an iterative process.\n\nAvailable:\n- Variables: `task` (your input data)\n- `llm_query(prompt)` - query a sub-LLM (~500K char capacity) for semantic analysis\n- `llm_query_batched(prompts)` - query multiple prompts concurrently (much faster for multiple queries)\n- `print()` - ALWAYS print to see results\n- `SUBMIT(answer, affected_files)` - submit final output when done\n- Standard libraries: re, json, collections, math, etc.\n\nIMPORTANT: This is ITERATIVE. Each code block you write will execute, you'll see the output, then you decide what to do next. Do NOT try to solve everything in one step.\n\n1. EXPLORE FIRST - Look at your data before processing it. Print samples, check types/lengths, understand the structure.\n2. ITERATE - Write small code snippets, observe outputs, then decide next steps. State persists between iterations.\n3. VERIFY BEFORE SUBMITTING - If results seem wrong (zeros, empty, unexpected), reconsider your approach.\n4. USE llm_query FOR SEMANTICS - String matching finds WHERE things are; llm_query understands WHAT things mean.\n5. MINIMIZE RETYPING (INPUTS & OUTPUTS) - When values are long, precise, or error-prone (IDs, numbers, code, quotes), re-access them via variables and parse/compute in code instead of retyping. Use small, targeted prints to sanity-check, but avoid manual copying when variables can carry the exact value.\n6. SUBMIT ONLY AFTER SEEING OUTPUTS - SUBMIT ends the current run immediately. If you need to inspect printed output, run it in one step, review the result, then call SUBMIT in a later step.\n\nYou have max 50 sub-LLM calls. When done, call SUBMIT() with your output.\nAdditional tools available (use these instead of standard library equivalents):\n- `readfile(path: str, offset: int, limit: int) -> str` - Read file contents with line numbers.\n- `writefile(path: str, content: str) -> str` - Write content to a file.\n- `editfile(path: str, old: str, new: str, replace_all: bool) -> str` - Replace text in a file.\n- `globfiles(pattern: str, path: str) -> str` - Find files matching a glob pattern, sorted by modification time.\n- `grepfiles(pattern: str, path: str) -> str` - Search files for a regex pattern.\n- `runbash(cmd: str) -> str` - Run a shell command and return output.",
|
"instructions": "You are a concise coding assistant.\n\nCRITICAL - Two execution environments exist:\n\n1. INTERNAL REPL (sandbox): Standard Python code you write executes in an isolated sandbox. Variables persist between iterations. Use for data processing, string manipulation, logic, loops, etc.\n\n2. EXTERNAL TOOLS (real system): Functions like read_file(), write_file(), run_bash(), glob_files(), grep_files() execute OUTSIDE the sandbox on the real filesystem and host machine. These have real, persistent side effects.\n\nWhen you need to:\n- Process data, do math, manipulate strings, iterate \u2192 write Python code directly in the REPL\n- Read/write actual files on disk \u2192 call read_file(), write_file(), edit_file()\n- Run shell commands on the host \u2192 call run_bash()\n- Search the codebase \u2192 call glob_files(), grep_files()\n\nDo NOT confuse REPL variables with external files. Reading a file into a variable does not mean the variable updates if the file changes - you must call read_file() again.\n\nYou are tasked with producing the following outputs given the inputs `task`:\n- {answer}\n\nYou have access to a Python REPL environment. Write Python code and it will be executed. You will see the output, then write more code based on what you learned. This is an iterative process.\n\nAvailable:\n- Variables: `task` (your input data)\n- `llm_query(prompt)` - query a sub-LLM (~500K char capacity) for semantic analysis\n- `llm_query_batched(prompts)` - query multiple prompts concurrently (much faster for multiple queries)\n- `print()` - ALWAYS print to see results\n- `SUBMIT(answer)` - submit final output when done\n- Standard libraries: re, json, collections, math, etc.\n\nIMPORTANT: This is ITERATIVE. Each code block you write will execute, you'll see the output, then you decide what to do next. Do NOT try to solve everything in one step.\n\n1. EXPLORE FIRST - Look at your data before processing it. Print samples, check types/lengths, understand the structure.\n2. ITERATE - Write small code snippets, observe outputs, then decide next steps. State persists between iterations.\n3. VERIFY BEFORE SUBMITTING - If results seem wrong (zeros, empty, unexpected), reconsider your approach.\n4. USE llm_query FOR SEMANTICS - String matching finds WHERE things are; llm_query understands WHAT things mean.\n5. MINIMIZE RETYPING (INPUTS & OUTPUTS) - When values are long, precise, or error-prone (IDs, numbers, code, quotes), re-access them via variables and parse/compute in code instead of retyping. Use small, targeted prints to sanity-check, but avoid manual copying when variables can carry the exact value.\n6. SUBMIT ONLY AFTER SEEING OUTPUTS - SUBMIT ends the current run immediately. If you need to inspect printed output, run it in one step, review the result, then call SUBMIT in a later step.\n\nYou have max 50 sub-LLM calls. When done, call SUBMIT() with your output.\nAdditional tools available (use these instead of standard library equivalents):\n- `read_file(path: str, offset: int, limit: int) -> str` - [EXTERNAL FILESYSTEM] Read file contents from disk with line numbers.\n- `write_file(path: str, content: str) -> str` - [EXTERNAL FILESYSTEM] Write content to a file on disk (creates or overwrites).\n- `edit_file(path: str, old: str, new: str, replace_all: bool) -> str` - [EXTERNAL FILESYSTEM] Replace text in a file on disk.\n- `glob_files(pattern: str, path: str) -> str` - [EXTERNAL FILESYSTEM] Do not use for simple file listing, run bash instead. Find files on disk matching a glob pattern.\n- `grep_files(pattern: str, path: str, glob: str, max_results: int) -> str` - [EXTERNAL FILESYSTEM] Search files on disk for a regex pattern using ripgrep.\n- `run_bash(cmd: str) -> str` - [EXTERNAL SYSTEM] Run a shell command on the host machine.",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"prefix": "Variables Info:",
|
"prefix": "Variables Info:",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lm": {
|
"lm": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openrouter/anthropic/claude-opus-4.5",
|
||||||
"model_type": "chat",
|
"model_type": "chat",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"num_retries": 3,
|
"num_retries": 3,
|
||||||
@@ -37,8 +37,9 @@
|
|||||||
"launch_kwargs": {},
|
"launch_kwargs": {},
|
||||||
"train_kwargs": {},
|
"train_kwargs": {},
|
||||||
"temperature": null,
|
"temperature": null,
|
||||||
"max_tokens": 16000,
|
"max_tokens": 50000,
|
||||||
"api_base": "https://openrouter.ai/api/v1"
|
"api_base": "https://openrouter.ai/api/v1",
|
||||||
|
"track_usage": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agent.extract": {
|
"agent.extract": {
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
"train": [],
|
"train": [],
|
||||||
"demos": [],
|
"demos": [],
|
||||||
"signature": {
|
"signature": {
|
||||||
"instructions": "The trajectory was generated with the following objective: \nYou are a concise coding assistant. Help the user with their coding task by using the available tools to read, write, edit files, search the codebase, and run commands.\n\n\nBased on the REPL trajectory, extract the final outputs now.\n\n Review your trajectory to see what information you gathered and what values you computed, then provide the final outputs.",
|
"instructions": "The trajectory was generated with the following objective: \nYou are a concise coding assistant.\n\nCRITICAL - Two execution environments exist:\n\n1. INTERNAL REPL (sandbox): Standard Python code you write executes in an isolated sandbox. Variables persist between iterations. Use for data processing, string manipulation, logic, loops, etc.\n\n2. EXTERNAL TOOLS (real system): Functions like read_file(), write_file(), run_bash(), glob_files(), grep_files() execute OUTSIDE the sandbox on the real filesystem and host machine. These have real, persistent side effects.\n\nWhen you need to:\n- Process data, do math, manipulate strings, iterate \u2192 write Python code directly in the REPL\n- Read/write actual files on disk \u2192 call read_file(), write_file(), edit_file()\n- Run shell commands on the host \u2192 call run_bash()\n- Search the codebase \u2192 call glob_files(), grep_files()\n\nDo NOT confuse REPL variables with external files. Reading a file into a variable does not mean the variable updates if the file changes - you must call read_file() again.\n\n\nBased on the REPL trajectory, extract the final outputs now.\n\n Review your trajectory to see what information you gathered and what values you computed, then provide the final outputs.",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"prefix": "Variables Info:",
|
"prefix": "Variables Info:",
|
||||||
@@ -59,15 +60,11 @@
|
|||||||
{
|
{
|
||||||
"prefix": "Answer:",
|
"prefix": "Answer:",
|
||||||
"description": "Your response to the user after completing the task"
|
"description": "Your response to the user after completing the task"
|
||||||
},
|
|
||||||
{
|
|
||||||
"prefix": "Affected Files:",
|
|
||||||
"description": "List of files that were written or modified during the task"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"lm": {
|
"lm": {
|
||||||
"model": "openai/gpt-5.2-codex",
|
"model": "openrouter/anthropic/claude-opus-4.5",
|
||||||
"model_type": "chat",
|
"model_type": "chat",
|
||||||
"cache": true,
|
"cache": true,
|
||||||
"num_retries": 3,
|
"num_retries": 3,
|
||||||
@@ -75,8 +72,9 @@
|
|||||||
"launch_kwargs": {},
|
"launch_kwargs": {},
|
||||||
"train_kwargs": {},
|
"train_kwargs": {},
|
||||||
"temperature": null,
|
"temperature": null,
|
||||||
"max_tokens": 16000,
|
"max_tokens": 50000,
|
||||||
"api_base": "https://openrouter.ai/api/v1"
|
"api_base": "https://openrouter.ai/api/v1",
|
||||||
|
"track_usage": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ version = "0.1.0"
|
|||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = ["dspy>=3.1.2", "modaic>=0.10.3"]
|
dependencies = ["dspy>=3.1.2", "fastmcp>=2.14.3", "mcp2py>=0.6.0", "modaic>=0.10.4", "wandb>=0.24.1", "weave>=0.52.25"]
|
||||||
|
|||||||
Reference in New Issue
Block a user