-
Notifications
You must be signed in to change notification settings - Fork 6
Add agent jobs flow #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| __pycache__ | ||
| .claude |
| 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()) | ||||||||||
|
|
||||||||||
| # 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extraneous This f-string has no interpolation placeholders. 🐛 Proposed fix conv_logger.info(
- f"[assistant:ThinkingBlock] (thinking)")
+ "[assistant:ThinkingBlock] (thinking)")📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.0)[error] 128-128: f-string without any placeholders Remove extraneous (F541) 🤖 Prompt for AI Agents |
||||||||||
| 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, | ||||||||||
| ) | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Config loading lacks error handling for missing or malformed file.
If
config.jsonis missing or contains invalid JSON, the application will crash at import time with an unclear error. Consider adding explicit error handling.🛡️ Proposed fix
🤖 Prompt for AI Agents