Skip to content
Open

Dev #12

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
37 changes: 33 additions & 4 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,39 @@
from fastapi import FastAPI
from app.routes import health, repos, webhook

logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
# ANSI color codes
_RESET = "\033[0m"
_GREY = "\033[90m"
_CYAN = "\033[96m"
_GREEN = "\033[92m"
_YELLOW = "\033[93m"
_RED = "\033[91m"
_BOLD = "\033[1m"

_LEVEL_COLORS = {
"DEBUG": _GREY,
"INFO": _GREEN,
"WARNING": _YELLOW,
"ERROR": _RED,
"CRITICAL": _RED + _BOLD,
}


class _ColorFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
level_color = _LEVEL_COLORS.get(record.levelname, "")
record.levelname = f"{level_color}{record.levelname}{_RESET}"
record.name = f"{_CYAN}{record.name}{_RESET}"
record.asctime = self.formatTime(record, self.datefmt)
record.asctime = f"{_GREY}{record.asctime}{_RESET}"
return (
f"{record.asctime} [{record.levelname}] {record.name}: {record.getMessage()}"
)


_handler = logging.StreamHandler()
_handler.setFormatter(_ColorFormatter())
logging.basicConfig(level=logging.INFO, handlers=[_handler])

app = FastAPI(
title="CodeSentinel",
Expand Down
48 changes: 48 additions & 0 deletions app/services/review_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ def _log_output_items(iteration: int, output: list) -> None:
f"tool={getattr(item, 'name', '?')} "
f"input={getattr(item, 'arguments', '?')}"
)
raw_output = getattr(item, "output", None)
if raw_output:
try:
parsed = json.loads(raw_output)
results = parsed.get("results", [])
logger.info(f"[iter {iteration}][mcp:result] {len(results)} result(s)")
for i, r in enumerate(results, 1):
logger.info(
f"[iter {iteration}][mcp:result:{i}] "
f"score={r.get('score', '?'):.3f} "
f"url={r.get('url', '?')}"
)
content = r.get("content", "").strip()
if content:
logger.info(f"[iter {iteration}][mcp:result:{i}:content] {content}")
except json.JSONDecodeError as e:
logger.warning(f"[iter {iteration}][mcp:result] failed to parse output: {e}")
elif item_type == "mcp_call_result":
logger.info(
f"[iter {iteration}][mcp:result] "
Expand Down Expand Up @@ -161,6 +178,28 @@ def _strip_removed_lines(diff: str) -> str:
return "\n".join(result)


_TOOL_COLORS = {
"run_linter": "\033[93m", # yellow
"tavily_search": "\033[96m", # cyan
"tavily_extract": "\033[96m", # cyan
"tavily_research":"\033[96m", # cyan
"tavily_skill": "\033[96m", # cyan
}
_RESET = "\033[0m"
_BOLD = "\033[1m"


def _log_tools_used(tools: list[str]) -> None:
if not tools:
logger.info(f"[review] tools used: {_BOLD}(none){_RESET}")
return
colored = []
for t in tools:
color = _TOOL_COLORS.get(t, "\033[90m")
colored.append(f"{color}{t}{_RESET}")
logger.info(f"[review] tools used: {_BOLD}" + " → ".join(colored) + _RESET)


def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]:
"""Run the agent loop: diff → tool calls (linter + Tavily MCP) → final findings."""
if not diff.strip():
Expand Down Expand Up @@ -205,6 +244,7 @@ def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]
# the model from calling it repeatedly when it receives an empty result.
active_tools = list(TOOLS)
linter_called = False
tools_used: list[str] = []

for iteration in range(1, 6):
response = _client.responses.create(
Expand All @@ -215,6 +255,7 @@ def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]
)

function_calls = [item for item in response.output if item.type == "function_call"]
mcp_calls = [item for item in response.output if item.type == "mcp_call"]

logger.info(
f"[review] iteration {iteration} — "
Expand All @@ -223,8 +264,13 @@ def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]
)
_log_output_items(iteration, response.output)

# Track MCP tool calls in execution order
for mc in mcp_calls:
tools_used.append(getattr(mc, "name", "mcp"))

if not function_calls:
# No local tools pending — model produced its final answer
_log_tools_used(tools_used)
return _parse_findings(response.output_text or "{}")

# Extend with the full assistant turn (includes any completed MCP call items).
Expand All @@ -233,6 +279,7 @@ def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]

# Dispatch each local function call and append its result
for fc in function_calls:
tools_used.append(fc.name)
args = json.loads(fc.arguments)
result = _dispatch_tool(fc.name, args, file_contents, iteration)
input_items.append({
Expand All @@ -246,4 +293,5 @@ def review_diff(diff: str, repo_full_name: str, pr_number: int) -> list[Finding]
active_tools = [t for t in active_tools if t.get("name") != "run_linter"]

logger.warning("[review] Agent loop hit max iterations without a final answer")
_log_tools_used(tools_used)
return []