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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
.claude
200 changes: 200 additions & 0 deletions server/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
"""
Core agent execution logic shared between sync and async endpoints.
"""

import json
import logging
import traceback
from collections.abc import Callable
from dataclasses import dataclass, field
from pathlib import Path

from claude_agent_sdk import (
query,
ClaudeAgentOptions,
AssistantMessage,
ResultMessage,
TextBlock,
ThinkingBlock,
ToolUseBlock,
ToolResultBlock,
)

# Resolve the plugin directory (repo root) relative to this file.
PLUGIN_DIR = str(Path(__file__).resolve().parent.parent / "plugins" / "oape")

CONVERSATION_LOG = Path("/tmp/conversation.log")

conv_logger = logging.getLogger("conversation")
conv_logger.setLevel(logging.INFO)
_handler = logging.FileHandler(CONVERSATION_LOG)
_handler.setFormatter(logging.Formatter("%(message)s"))
conv_logger.addHandler(_handler)

with open(Path(__file__).resolve().parent / "config.json") as cf:
CONFIGS = json.loads(cf.read())
Comment on lines +34 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Config loading lacks error handling for missing or malformed file.

If config.json is missing or contains invalid JSON, the application will crash at import time with an unclear error. Consider adding explicit error handling.

🛡️ Proposed fix
-with open(Path(__file__).resolve().parent / "config.json") as cf:
-    CONFIGS = json.loads(cf.read())
+_config_path = Path(__file__).resolve().parent / "config.json"
+try:
+    with open(_config_path) as cf:
+        CONFIGS = json.load(cf)
+except FileNotFoundError:
+    raise RuntimeError(f"Missing required config file: {_config_path}")
+except json.JSONDecodeError as e:
+    raise RuntimeError(f"Invalid JSON in config file {_config_path}: {e}")
🤖 Prompt for AI Agents
In `@server/agent.py` around lines 34 - 35, The CONFIGS initialization currently
opens and parses config.json at import time without error handling; update the
code around CONFIGS to catch FileNotFoundError and json.JSONDecodeError when
opening/parsing the Path(__file__).resolve().parent / "config.json", and handle
them by logging or raising a clear, descriptive exception (or falling back to
sane defaults) so the app fails with a helpful message instead of an obscure
traceback; ensure you reference CONFIGS and the config file load block when
applying the change.


# Supported commands and their corresponding plugin skill names.
SUPPORTED_COMMANDS = {
"api-implement": "oape:api-implement",
}


@dataclass
class AgentResult:
"""Result returned after running the Claude agent."""

output: str
cost_usd: float
error: str | None = None
conversation: list[dict] = field(default_factory=list)

@property
def success(self) -> bool:
return self.error is None


async def run_agent(
command: str,
ep_url: str,
working_dir: str,
on_message: Callable[[dict], None] | None = None,
) -> AgentResult:
"""Run the Claude agent and return the result.

Args:
command: The command key (e.g. "api-implement").
ep_url: The enhancement proposal PR URL.
working_dir: Absolute path to the operator repo.
on_message: Optional callback invoked with each conversation message
dict as it arrives, enabling real-time streaming.

Returns:
An AgentResult with the output or error.
"""
skill_name = SUPPORTED_COMMANDS.get(command)
if skill_name is None:
return AgentResult(
output="",
cost_usd=0.0,
error=f"Unsupported command: {command}. "
f"Supported: {', '.join(SUPPORTED_COMMANDS)}",
)

options = ClaudeAgentOptions(
system_prompt=(
"You are an OpenShift operator code generation assistant. "
f"Execute the {skill_name} plugin with the provided EP URL. "
),
cwd=working_dir,
permission_mode="bypassPermissions",
allowed_tools=CONFIGS["claude_allowed_tools"],
plugins=[{"type": "local", "path": PLUGIN_DIR}],
)

output_parts: list[str] = []
conversation: list[dict] = []
cost_usd = 0.0

conv_logger.info(
f"\n{'=' * 60}\n[request] command={command} ep_url={ep_url} "
f"cwd={working_dir}\n{'=' * 60}"
)

def _emit(entry: dict) -> None:
"""Append to conversation and invoke on_message callback if set."""
conversation.append(entry)
if on_message is not None:
on_message(entry)

try:
async for message in query(
prompt=f"/{skill_name} {ep_url}",
options=options,
):
if isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock):
output_parts.append(block.text)
entry = {"type": "assistant", "block_type": "text",
"content": block.text}
_emit(entry)
conv_logger.info(f"[assistant] {block.text}")
elif isinstance(block, ThinkingBlock):
entry = {"type": "assistant", "block_type": "thinking",
"content": block.thinking}
_emit(entry)
conv_logger.info(
f"[assistant:ThinkingBlock] (thinking)")
Comment on lines +127 to +128
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove extraneous f prefix from string without placeholders.

This f-string has no interpolation placeholders.

🐛 Proposed fix
                     conv_logger.info(
-                        f"[assistant:ThinkingBlock] (thinking)")
+                        "[assistant:ThinkingBlock] (thinking)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
conv_logger.info(
f"[assistant:ThinkingBlock] (thinking)")
conv_logger.info(
"[assistant:ThinkingBlock] (thinking)")
🧰 Tools
🪛 Ruff (0.15.0)

[error] 128-128: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for AI Agents
In `@server/agent.py` around lines 127 - 128, The log call using conv_logger.info
currently uses an unnecessary f-string: change the call in the relevant code
block (the conv_logger.info invocation inside the assistant thinking logging) to
use a plain string literal instead of an f-string; if interpolation was
intended, add the appropriate placeholders and variables, otherwise remove the
leading "f" so it reads a normal string.

elif isinstance(block, ToolUseBlock):
entry = {"type": "assistant", "block_type": "tool_use",
"tool_name": block.name,
"tool_input": block.input}
_emit(entry)
conv_logger.info(
f"[assistant:ToolUseBlock] {block.name}")
elif isinstance(block, ToolResultBlock):
content = block.content
if not isinstance(content, str):
content = json.dumps(content, default=str)
entry = {"type": "assistant", "block_type": "tool_result",
"tool_use_id": block.tool_use_id,
"content": content,
"is_error": block.is_error or False}
_emit(entry)
conv_logger.info(
f"[assistant:ToolResultBlock] {block.tool_use_id}")
else:
detail = json.dumps(
getattr(block, "__dict__", str(block)),
default=str,
)
entry = {
"type": "assistant",
"block_type": type(block).__name__,
"content": detail,
}
_emit(entry)
conv_logger.info(
f"[assistant:{type(block).__name__}] {detail}"
)
elif isinstance(message, ResultMessage):
cost_usd = message.total_cost_usd
if message.result:
output_parts.append(message.result)
entry = {
"type": "result",
"content": message.result,
"cost_usd": cost_usd,
}
_emit(entry)
conv_logger.info(
f"[result] {message.result} cost=${cost_usd:.4f}"
)
else:
detail = json.dumps(
getattr(message, "__dict__", str(message)), default=str
)
entry = {
"type": type(message).__name__,
"content": detail,
}
_emit(entry)
conv_logger.info(f"[{type(message).__name__}] {detail}")

conv_logger.info(
f"[done] cost=${cost_usd:.4f} parts={len(output_parts)}\n"
)
return AgentResult(
output="\n".join(output_parts),
cost_usd=cost_usd,
conversation=conversation,
)
except Exception as exc:
conv_logger.info(f"[error] {traceback.format_exc()}")
return AgentResult(
output="",
cost_usd=cost_usd,
error=str(exc),
conversation=conversation,
)
Loading