Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file added agents/__init__.py
Empty file.
34 changes: 34 additions & 0 deletions agents/debug_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from llm.provider import ask_llm


def debug_error(file_path, model):
try:
with open(file_path, "r") as f:
error = f.read().strip()
except FileNotFoundError:
return f"โŒ File not found: {file_path}"

if not error:
return "โŒ Error file is empty"

prompt = f"""
You are a senior software engineer.

Debug this error:

{error}

Explain clearly:

1. Root cause
2. Why it happens
3. Step-by-step fix
4. Example fix
"""

result = ask_llm(prompt, model)

if not result:
return "โŒ No response from LLM"

return result
72 changes: 72 additions & 0 deletions agents/search_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from core.parser import get_code_files
from llm.provider import ask_llm


def search_code(repo_path, query, model):
files = get_code_files(repo_path)

matches = []

for file in files:
try:
with open(file, "r", encoding="utf-8") as f:
lines = f.readlines()

for i, line in enumerate(lines):
if query.lower() in line.lower():
snippet = "".join(lines[max(0, i-2):i+3])

matches.append(f"""
FILE: {file}
LINE: {i+1}
CODE:
{snippet}
""")

if len(matches) >= 5:
break

except:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Bare except hides all read errors when scanning files.

Catching every exception here will hide real read issues (e.g., permission, corruption, encoding) and make failures hard to diagnose. Instead, catch specific exceptions like OSError/IOError and UnicodeDecodeError, and log or record skipped files so these errors remain visible.

Suggested implementation:

        except (OSError, UnicodeDecodeError) as exc:
            logging.warning("Skipping file %s due to read error: %s", file, exc)
            continue
  1. Ensure logging is imported at the top of agents/search_agent.py:
    • import logging
  2. Optionally configure logging (if not already configured elsewhere in the application) to ensure warnings are visible (e.g., basicConfig or the projectโ€™s logging setup).

continue

if not matches:
return "โŒ No matches found"

context = "\n\n".join(matches)

prompt = f"""
You are a senior software engineer analyzing search results from a codebase.

User query: "{query}"

Code snippets:
{context}

IMPORTANT:
- Do NOT describe prompts or text
- Focus on actual code logic
- Ignore placeholder strings

Give a professional answer:

1. Relevant functionality found
- What real functionality matches the query

2. Where it exists
- File names and modules

3. How it works
- Explain logic of code (not text)

4. Summary
- What developer should understand from this

Be precise, technical, and useful.
"""

result = ask_llm(prompt, model)

if not result or "โŒ" in result:
return "โŒ No meaningful response from LLM for search"

return result
Empty file.
Empty file added cli/__init__.py
Empty file.
11 changes: 0 additions & 11 deletions cli/devos.py

This file was deleted.

51 changes: 51 additions & 0 deletions cli/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import sys
from core.engine import build_context, build_prompt
from llm.provider import ask_llm
from agents.debug_agent import debug_error
from agents.search_agent import search_code


def explain(path, model):
context = build_context(path)
prompt = build_prompt(context, "Explain this codebase")
return ask_llm(prompt, model)


def main():
if len(sys.argv) < 3:
print("""
Usage:
explain <path> [--model openrouter|google|auto]
search <path> "<query>" [--model openrouter|google|auto]
debug <file> [--model openrouter|google|auto]
""")
return

command = sys.argv[1]
model = "auto"

if "--model" in sys.argv:
idx = sys.argv.index("--model")
if idx + 1 < len(sys.argv):
model = sys.argv[idx + 1]

print("๐Ÿ” Processing...\n")

if command == "explain":
result = explain(sys.argv[2], model)

elif command == "search":
result = search_code(sys.argv[2], sys.argv[3], model)
Comment on lines +37 to +38

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Argument parsing for search is brittle when --model is used or the query is missing.

This relies on the query always being at sys.argv[3], so a call like search path --model google "foo" will pass "--model" as the query. And because thereโ€™s no check that sys.argv[3] exists, a missing query will raise IndexError. Consider using argparse or at least deriving the query index from the position of --model and validating that the required arguments are present before indexing.


elif command == "debug":
result = debug_error(sys.argv[2], model)

else:
result = "โŒ Unknown command"

print("๐Ÿ’ก Result:\n")
print(result)


if __name__ == "__main__":
main()
Empty file added core/__init__.py
Empty file.
64 changes: 64 additions & 0 deletions core/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from core.parser import get_code_files, read_files

PRIORITY = ["main", "app", "engine", "cli", "api"]


def rank_files(files):
scored = []

for f in files:
score = 0
name = f.lower()

for p in PRIORITY:
if p in name:
score += 5

if "core" in name or "llm" in name:
score += 3

scored.append((score, f))

scored.sort(reverse=True)
return [f for _, f in scored]


def build_context(repo_path):
files = get_code_files(repo_path)
ranked = rank_files(files)

top_files = ranked[:5]
return read_files(top_files)


def build_prompt(context, question):
return f"""
You are a senior software engineer analyzing a real codebase.

STRICT RULES:
- Use ONLY the given code context
- Do NOT assume missing features
- Do NOT mention technologies not present in context

CONTEXT:
{context}

TASK:
{question}

Respond in this format:

1. Purpose
- What this system actually does (based only on code)

2. Architecture
- Real structure (modules, flow)

3. Key Components
- Explain actual files/modules found

4. Execution Flow
- Step-by-step how system works

Keep answer precise, technical, and grounded in code.
"""
38 changes: 38 additions & 0 deletions core/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

IGNORE_DIRS = {
".venv", "__pycache__", ".git", "node_modules",
"dist", "build",
"apps",
"frontend",
"infrastructure"
}

EXTENSIONS = (".py", ".js", ".ts", ".tsx")


def get_code_files(repo_path):
code_files = []

for root, dirs, files in os.walk(repo_path):
dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]

for file in files:
if file.endswith(EXTENSIONS):
code_files.append(os.path.join(root, file))

return code_files


def read_files(files, max_chars=800):
contents = []

for file in files:
try:
with open(file, "r", encoding="utf-8") as f:
text = f.read()[:max_chars]
contents.append(f"\n# FILE: {file}\n{text}")
except:
continue

return "\n\n".join(contents)
Empty file added llm/__init__.py
Empty file.
95 changes: 95 additions & 0 deletions llm/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import requests
from dotenv import load_dotenv

load_dotenv()

OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")


def ask_openrouter(prompt):
if not OPENROUTER_API_KEY:
return None

try:
res = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "openai/gpt-3.5-turbo",
"messages": [{"role": "user", "content": prompt}]
},
timeout=20
)

data = res.json()

if "choices" in data:
return data["choices"][0]["message"]["content"]

return None

except:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Catch more specific exceptions and handle HTTP/JSON errors explicitly.

A bare except will hide all errors, including programmer mistakes like TypeError/KeyError, and turns HTTP/JSON failures into None, making them indistinguishable from a legitimate โ€œno matchesโ€ case. Catch requests.RequestException and ValueError (for JSON) explicitly, and check res.status_code before res.json() so you can distinguish network/HTTP errors from valid but empty responses.

Suggested implementation:

        except (requests.RequestException, ValueError) as exc:
            # Network/HTTP or JSON parsing error: skip this iteration but keep
            # programmer errors (TypeError, KeyError, etc.) visible.
            continue
  1. Ensure import requests is present at the top of llm/provider.py so requests.RequestException is available.
  2. In the same try block above this except, before calling res.json(), add an explicit status-code check, e.g.:
    • if res.status_code != 200: raise requests.RequestException(f"Unexpected status {res.status_code}")
      This will make HTTP failures flow into the new except (requests.RequestException, ValueError) branch instead of being silently treated as a successful โ€œno matchesโ€ case.
  3. If you want to propagate HTTP/JSON failures to callers instead of silently skipping them, you can re-raise after optional logging inside the except block instead of continue.

return None


def ask_google(prompt):
if not GOOGLE_API_KEY:
return None

try:
res = requests.post(
f"https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent?key={GOOGLE_API_KEY}",
headers={"Content-Type": "application/json"},
json={
"contents": [{"parts": [{"text": prompt}]}]
},
timeout=30
)

data = res.json()

if "candidates" in data:
return data["candidates"][0]["content"]["parts"][0]["text"]

return None

except:
return None


def ask_llm(prompt, model="auto"):

def clean(res):
if not res:
return None
res = str(res).strip()
return res if len(res) > 10 else None # IMPORTANT

# -------- OPENROUTER --------
if model == "openrouter":
res = clean(ask_openrouter(prompt))
return res if res else "โŒ OpenRouter returned empty response"

# -------- GOOGLE --------
if model == "google":
res = clean(ask_google(prompt))
return res if res else "โŒ Google returned empty response"

# -------- AUTO MODE --------
if model == "auto":
res = clean(ask_openrouter(prompt))
if res:
return res

res = clean(ask_google(prompt))
if res:
return res

return "โŒ All LLM providers failed or returned empty"

return "โŒ Invalid model"
Loading