From eed65c42216a348a1f851b21daefe826730c2a8c Mon Sep 17 00:00:00 2001 From: Farouk Adeleke Date: Mon, 19 Jan 2026 01:07:19 -0800 Subject: [PATCH] (no commit message) --- config.json | 2 +- nanocode.py | 233 ++++++++++++++++++++++++++++++++++++--- program.json | 4 +- utils/__init__.py | 50 --------- utils/display.py | 25 ----- utils/file_ops.py | 104 ----------------- utils/model_selection.py | 51 --------- utils/shell_ops.py | 34 ------ 8 files changed, 221 insertions(+), 282 deletions(-) delete mode 100644 utils/__init__.py delete mode 100644 utils/display.py delete mode 100644 utils/file_ops.py delete mode 100644 utils/model_selection.py delete mode 100644 utils/shell_ops.py diff --git a/config.json b/config.json index 73010a8..b22fe68 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,7 @@ { "model": null, "max_iters": 15, - "lm": "openrouter/anthropic/claude-3.5-sonnet", + "lm": "openai/gpt-5.2-codex", "api_base": "https://openrouter.ai/api/v1", "max_tokens": 8192 } \ No newline at end of file diff --git a/nanocode.py b/nanocode.py index 21284c4..6445985 100644 --- a/nanocode.py +++ b/nanocode.py @@ -2,16 +2,218 @@ """nanocode-dspy - minimal claude code alternative using DSPy ReAct""" import os +import re +import glob as globlib +import subprocess from modaic import PrecompiledProgram, PrecompiledConfig import dspy from dspy.utils.callback import BaseCallback -from utils import ( - RESET, BOLD, DIM, BLUE, CYAN, GREEN, RED, MAGENTA, - separator, render_markdown, - read_file, write_file, edit_file, glob_files, grep_files, run_bash, - AVAILABLE_MODELS, select_model, -) + +# --- ANSI colors --- + +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" +BLUE = "\033[34m" +CYAN = "\033[36m" +GREEN = "\033[32m" +YELLOW = "\033[33m" +RED = "\033[31m" +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 --- + +def read_file(path: str, offset: int = 0, limit: int = None) -> str: + """Read file contents with line numbers. + + Args: + path: Path to the file to read + offset: Line number to start from (0-indexed) + limit: Maximum number of lines to read + + Returns: + File contents with line numbers + """ + lines = open(path).readlines() + if limit is None: + limit = len(lines) + selected = lines[offset : offset + limit] + return "".join(f"{offset + idx + 1:4}| {line}" for idx, line in enumerate(selected)) + + +def write_file(path: str, content: str) -> str: + """Write content to a file. + + Args: + path: Path to the file to write + content: Content to write to the file + + Returns: + 'ok' on success + """ + with open(path, "w") as f: + f.write(content) + return "ok" + + +def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str: + """Replace text in a file. + + Args: + path: Path to the file to edit + old: Text to find and replace + new: Replacement text + replace_all: If True, replace all occurrences; otherwise old must be unique + + Returns: + 'ok' on success, error message on failure + """ + text = open(path).read() + if old not in text: + return "error: old_string not found" + count = text.count(old) + if not replace_all and count > 1: + return f"error: old_string appears {count} times, must be unique (use replace_all=True)" + replacement = text.replace(old, new) if replace_all else text.replace(old, new, 1) + with open(path, "w") as f: + f.write(replacement) + return "ok" + + +def glob_files(pattern: str, path: str = ".") -> str: + """Find files matching a glob pattern, sorted by modification time. + + Args: + pattern: Glob pattern to match (e.g., '**/*.py') + path: Base directory to search in + + Returns: + Newline-separated list of matching files + """ + full_pattern = (path + "/" + pattern).replace("//", "/") + files = globlib.glob(full_pattern, recursive=True) + files = sorted( + 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" + + +def grep_files(pattern: str, path: str = ".") -> str: + """Search files for a regex pattern. + + Args: + pattern: Regular expression pattern to search for + path: Base directory to search in + + Returns: + Matching lines in format 'filepath:line_num:content' + """ + regex = re.compile(pattern) + hits = [] + for filepath in globlib.glob(path + "/**", recursive=True): + try: + for line_num, line in enumerate(open(filepath), 1): + if regex.search(line): + hits.append(f"{filepath}:{line_num}:{line.rstrip()}") + except Exception: + pass + return "\n".join(hits[:50]) or "no matches found" + + +# --- Shell operations --- + +def run_bash(cmd: str) -> str: + """Run a shell command and return output. + + Args: + cmd: Shell command to execute + + Returns: + Command output (stdout and stderr combined) + """ + proc = subprocess.Popen( + cmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True + ) + output_lines = [] + try: + while True: + line = proc.stdout.readline() + if not line and proc.poll() is not None: + break + if line: + print(f" {DIM}│ {line.rstrip()}{RESET}", flush=True) + output_lines.append(line) + proc.wait(timeout=30) + except subprocess.TimeoutExpired: + proc.kill() + output_lines.append("\n(timed out after 30s)") + return "".join(output_lines).strip() or "(empty output)" + + +# --- Model selection --- + +AVAILABLE_MODELS = { + "1": ("Claude 3.5 Sonnet", "anthropic/claude-3.5-sonnet"), + "2": ("Claude 3.5 Haiku", "anthropic/claude-3.5-haiku"), + "3": ("GPT-4o", "openai/gpt-4o"), + "4": ("GPT-4o mini", "openai/gpt-4o-mini"), + "5": ("Gemini Pro 1.5", "google/gemini-pro-1.5"), + "6": ("Llama 3.1 405B", "meta-llama/llama-3.1-405b-instruct"), + "7": ("DeepSeek V3", "deepseek/deepseek-chat"), + "8": ("Qwen 2.5 72B", "qwen/qwen-2.5-72b-instruct"), +} + + +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: + print(f"{RED}⏺ Invalid choice. Please enter 1-8 or c{RESET}") + except (KeyboardInterrupt, EOFError): + print(f"\n{RED}⏺ Model selection cancelled{RESET}") + exit(1) # --- DSPy Signature --- @@ -30,19 +232,19 @@ tools = [read_file, write_file, edit_file, glob_files, grep_files, 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 @@ -63,19 +265,19 @@ class AgentConfig(PrecompiledConfig): class AgentProgram(PrecompiledProgram): config: AgentConfig - + def __init__(self, config: AgentConfig, **kwargs): self.config = config super().__init__(config, **kwargs) # Configure logging callback globally dspy.settings.configure(callbacks=[ToolLoggingCallback()]) - + agent = dspy.ReAct(CodingAssistant, tools=tools, max_iters=self.config.max_iters) lm = dspy.LM(self.config.lm, api_base=self.config.api_base, max_tokens=self.config.max_tokens) agent.set_lm(lm) self.agent = agent - + def forward(self, task: str) -> str: assert task, "Task cannot be empty" return self.agent(task=task) @@ -92,7 +294,7 @@ def main(): # Add openrouter/ prefix if not already present if not model.startswith("openrouter/"): model = f"openrouter/{model}" - + config = AgentConfig() config.lm = model @@ -148,6 +350,7 @@ def main(): if __name__ == "__main__": - agent = AgentProgram(AgentConfig()) - agent.push_to_hub("farouk1/nanocode", with_code=True) + agent = AgentProgram(AgentConfig(lm="openai/gpt-5.2-codex")) + agent.push_to_hub("farouk1/nanocode") #main() + diff --git a/program.json b/program.json index 97a8601..dabddb8 100644 --- a/program.json +++ b/program.json @@ -29,7 +29,7 @@ ] }, "lm": { - "model": "openrouter/anthropic/claude-3.5-sonnet", + "model": "openai/gpt-5.2-codex", "model_type": "chat", "cache": true, "num_retries": 3, @@ -71,7 +71,7 @@ ] }, "lm": { - "model": "openrouter/anthropic/claude-3.5-sonnet", + "model": "openai/gpt-5.2-codex", "model_type": "chat", "cache": true, "num_retries": 3, diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index 1648c17..0000000 --- a/utils/__init__.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Utility modules for nanocode.""" - -from .display import ( - RESET, - BOLD, - DIM, - BLUE, - CYAN, - GREEN, - YELLOW, - RED, - MAGENTA, - separator, - render_markdown, -) -from .file_ops import ( - read_file, - write_file, - edit_file, - glob_files, - grep_files, -) -from .shell_ops import run_bash -from .model_selection import AVAILABLE_MODELS, select_model - -__all__ = [ - # Display - "RESET", - "BOLD", - "DIM", - "BLUE", - "CYAN", - "GREEN", - "YELLOW", - "RED", - "MAGENTA", - "separator", - "render_markdown", - # File operations - "read_file", - "write_file", - "edit_file", - "glob_files", - "grep_files", - # Shell operations - "run_bash", - # Model selection - "AVAILABLE_MODELS", - "select_model", -] diff --git a/utils/display.py b/utils/display.py deleted file mode 100644 index 16cbf3f..0000000 --- a/utils/display.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Display and UI utilities for nanocode.""" - -import os -import re - -# ANSI colors -RESET = "\033[0m" -BOLD = "\033[1m" -DIM = "\033[2m" -BLUE = "\033[34m" -CYAN = "\033[36m" -GREEN = "\033[32m" -YELLOW = "\033[33m" -RED = "\033[31m" -MAGENTA = "\033[35m" - - -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) diff --git a/utils/file_ops.py b/utils/file_ops.py deleted file mode 100644 index 739882a..0000000 --- a/utils/file_ops.py +++ /dev/null @@ -1,104 +0,0 @@ -"""File operation utilities for nanocode.""" - -import glob as globlib -import os -import re - - -def read_file(path: str, offset: int = 0, limit: int = None) -> str: - """Read file contents with line numbers. - - Args: - path: Path to the file to read - offset: Line number to start from (0-indexed) - limit: Maximum number of lines to read - - Returns: - File contents with line numbers - """ - lines = open(path).readlines() - if limit is None: - limit = len(lines) - selected = lines[offset : offset + limit] - return "".join(f"{offset + idx + 1:4}| {line}" for idx, line in enumerate(selected)) - - -def write_file(path: str, content: str) -> str: - """Write content to a file. - - Args: - path: Path to the file to write - content: Content to write to the file - - Returns: - 'ok' on success - """ - with open(path, "w") as f: - f.write(content) - return "ok" - - -def edit_file(path: str, old: str, new: str, replace_all: bool = False) -> str: - """Replace text in a file. - - Args: - path: Path to the file to edit - old: Text to find and replace - new: Replacement text - replace_all: If True, replace all occurrences; otherwise old must be unique - - Returns: - 'ok' on success, error message on failure - """ - text = open(path).read() - if old not in text: - return "error: old_string not found" - count = text.count(old) - if not replace_all and count > 1: - return f"error: old_string appears {count} times, must be unique (use replace_all=True)" - replacement = text.replace(old, new) if replace_all else text.replace(old, new, 1) - with open(path, "w") as f: - f.write(replacement) - return "ok" - - -def glob_files(pattern: str, path: str = ".") -> str: - """Find files matching a glob pattern, sorted by modification time. - - Args: - pattern: Glob pattern to match (e.g., '**/*.py') - path: Base directory to search in - - Returns: - Newline-separated list of matching files - """ - full_pattern = (path + "/" + pattern).replace("//", "/") - files = globlib.glob(full_pattern, recursive=True) - files = sorted( - 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" - - -def grep_files(pattern: str, path: str = ".") -> str: - """Search files for a regex pattern. - - Args: - pattern: Regular expression pattern to search for - path: Base directory to search in - - Returns: - Matching lines in format 'filepath:line_num:content' - """ - regex = re.compile(pattern) - hits = [] - for filepath in globlib.glob(path + "/**", recursive=True): - try: - for line_num, line in enumerate(open(filepath), 1): - if regex.search(line): - hits.append(f"{filepath}:{line_num}:{line.rstrip()}") - except Exception: - pass - return "\n".join(hits[:50]) or "no matches found" diff --git a/utils/model_selection.py b/utils/model_selection.py deleted file mode 100644 index a137ad8..0000000 --- a/utils/model_selection.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Model selection utilities for nanocode.""" - -import os -from .display import RESET, BOLD, DIM, BLUE, GREEN, RED - -# Available OpenRouter models -AVAILABLE_MODELS = { - "1": ("Claude 3.5 Sonnet", "anthropic/claude-3.5-sonnet"), - "2": ("Claude 3.5 Haiku", "anthropic/claude-3.5-haiku"), - "3": ("GPT-4o", "openai/gpt-4o"), - "4": ("GPT-4o mini", "openai/gpt-4o-mini"), - "5": ("Gemini Pro 1.5", "google/gemini-pro-1.5"), - "6": ("Llama 3.1 405B", "meta-llama/llama-3.1-405b-instruct"), - "7": ("DeepSeek V3", "deepseek/deepseek-chat"), - "8": ("Qwen 2.5 72B", "qwen/qwen-2.5-72b-instruct"), -} - - -def select_model(): - """Interactive model selection or use environment variable.""" - # Check environment variable first - 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: - print(f"{RED}⏺ Invalid choice. Please enter 1-8 or c{RESET}") - except (KeyboardInterrupt, EOFError): - print(f"\n{RED}⏺ Model selection cancelled{RESET}") - exit(1) diff --git a/utils/shell_ops.py b/utils/shell_ops.py deleted file mode 100644 index ff4ef32..0000000 --- a/utils/shell_ops.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Shell operation utilities for nanocode.""" - -import subprocess -from .display import DIM, RESET - - -def run_bash(cmd: str) -> str: - """Run a shell command and return output. - - Args: - cmd: Shell command to execute - - Returns: - Command output (stdout and stderr combined) - """ - proc = subprocess.Popen( - cmd, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True - ) - output_lines = [] - try: - while True: - line = proc.stdout.readline() - if not line and proc.poll() is not None: - break - if line: - print(f" {DIM}│ {line.rstrip()}{RESET}", flush=True) - output_lines.append(line) - proc.wait(timeout=30) - except subprocess.TimeoutExpired: - proc.kill() - output_lines.append("\n(timed out after 30s)") - return "".join(output_lines).strip() or "(empty output)"