Skip to content
Closed
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

An open control plane designed for enterprises to deploy, manage, and govern AI agents at scale.

## Note About External Repository Import Testing

To validate agent import and deployment from an external source repository, we used the following public repository as a compatible agent example:

- https://github.com/kooljo/poc-agent-sdk (branch: main, commit: 8de1694693c4dea1220dbb1397b00a227fd23e98)

This repository was used to verify integration and automatic deployment of internally hosted agents from source code stored on GitHub.

## Overview

WSO2 Agent Manager provides a comprehensive platform for enterprise AI agent management. It enables organizations to deploy AI agents (both internally hosted and externally deployed), monitor their behavior through full-stack observability, and enforce governance policies at scale.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,80 @@ spec:

# Schema definition for monitor workflows
# Note: These parameters are populated by the Go scheduler for each run
schema:
parameters:
monitor:
name: "string | description=\"Monitor unique name\""
displayName: "string | default=\"\" description=\"Human-readable monitor name\""
agent:
id: "string | description=\"Agent unique identifier\""
environment:
id: "string | description=\"Environment unique identifier\""
evaluation:
evaluators: "string | description=\"JSON array of evaluator configs\""
llmProviderConfigs: "string | default=\"[]\" description=\"JSON array of LLM provider credentials (envVar/value pairs)\""
samplingRate: "number | default=1.0 description=\"Sampling rate for traces (0.0-1.0)\""
traceStart: "string | description=\"Start time for trace evaluation (ISO 8601 format, calculated by scheduler)\""
traceEnd: "string | description=\"End time for trace evaluation (ISO 8601 format, calculated by scheduler)\""
publishing:
monitorId: "string | description=\"Monitor UUID for publishing scores\""
runId: "string | description=\"Run UUID for this evaluation execution\""
parameters:
openAPIV3Schema:
type: object
required:
- monitor
- agent
- environment
- evaluation
- publishing
properties:
monitor:
type: object
required:
- name
properties:
name:
type: string
description: Monitor unique name
displayName:
type: string
default: ""
description: Human-readable monitor name
agent:
type: object
required:
- id
properties:
id:
type: string
description: Agent unique identifier
environment:
type: object
required:
- id
properties:
id:
type: string
description: Environment unique identifier
evaluation:
type: object
required:
- evaluators
- traceStart
- traceEnd
properties:
evaluators:
type: string
description: JSON array of evaluator configs
llmProviderConfigs:
type: string
default: "[]"
description: JSON array of LLM provider credentials (envVar/value pairs)
samplingRate:
type: number
default: 1.0
description: Sampling rate for traces (0.0-1.0)
traceStart:
type: string
description: Start time for trace evaluation (ISO 8601 format, calculated by scheduler)
traceEnd:
type: string
description: End time for trace evaluation (ISO 8601 format, calculated by scheduler)
publishing:
type: object
required:
- monitorId
- runId
properties:
monitorId:
type: string
description: Monitor UUID for publishing scores
runId:
type: string
description: Run UUID for this evaluation execution

# Rendered workflow resource for WorkflowRun executions
# Each WorkflowRun creates a single Workflow (not CronWorkflow)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ spec:
items:
type: string
default:
- "http://localhost:3000"
- "http://localhost:13000"
allowMethods:
type: array
items:
Expand Down Expand Up @@ -153,10 +153,31 @@ spec:
- name: main
image: ${workload.container.image}
imagePullPolicy: ${environmentConfigs.imagePullPolicy}
command: |
${has(workload.container.command) ? workload.container.command : oc_omit()}
args: |
${has(workload.container.args) ? workload.container.args : oc_omit()}
command:
- /bin/sh
- -lc
args:
- |
cat >/tmp/amp_chat_hotfix.py <<'PY'
from pathlib import Path

p = Path('/workspace/main.py')
text = p.read_text()
old = """# Pydantic model for handling chat input\nclass Question(BaseModel):\n prompt: str\n"""
new = """# Pydantic model for handling chat input\nclass Question(BaseModel):\n prompt: str | None = None\n message: str | None = None\n session_id: str | None = None\n context: dict | None = None\n"""
if old in text:
text = text.replace(old, new, 1)

old2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": question.prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=question.prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
new2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n user_prompt = question.message or question.prompt\n if not user_prompt:\n raise HTTPException(status_code=422, detail=\"message or prompt is required\")\n\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": user_prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=user_prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
if old2 in text:
text = text.replace(old2, new2, 1)

p.write_text(text)
print("startup hotfix applied")
Comment on lines +162 to +177

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: unguarded read_text() crashes startup for any agent-api without /workspace/main.py.

p.read_text() raises FileNotFoundError if the target file doesn't exist. Since this ClusterComponentType is shared across all agent-api workloads — including those built with amp-docker or amp-ballerina-buildpack (see allowedWorkflows, lines 12–17) — any agent that doesn't happen to have /workspace/main.py will CrashLoopBackOff because exec /cnb/process/web never runs.

🛡️ Proposed fix — add an existence guard at the top of the patch script
-                      p = Path('/workspace/main.py')
-                      text = p.read_text()
+                      p = Path('/workspace/main.py')
+                      if not p.exists():
+                          print("startup hotfix: /workspace/main.py not found, skipping")
+                          exit(0)
+                      text = p.read_text()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@deployments/helm-charts/wso2-amp-platform-resources-extension/templates/component-types/agent-api.yaml`
around lines 162 - 177, The startup hotfix script calls p.read_text() on p =
Path('/workspace/main.py') without checking existence, which causes a
FileNotFoundError for agents that don't include that file; update the script
around the p = Path('/workspace/main.py') / p.read_text() usage to first check
p.exists() (or wrap read_text() in a try/except FileNotFoundError) and skip the
patch/early-return (and print a short message) when the file is absent so the
component doesn't crash on startup.

Comment on lines +168 to +177

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Major: print("startup hotfix applied") is unconditional — masks silent no-ops.

p.write_text(text) and print("startup hotfix applied") execute regardless of whether either if old in text: / if old2 in text: branch matched. If the source code changes and neither pattern matches, the script silently does nothing but still reports success, making it invisible in logs that the hotfix was never applied.

🛡️ Proposed fix — conditional reporting
+                      applied = False
                       if old in text:
                           text = text.replace(old, new, 1)
+                          applied = True

                       ...

                       if old2 in text:
                           text = text.replace(old2, new2, 1)
+                          applied = True

                       p.write_text(text)
-                      print("startup hotfix applied")
+                      if applied:
+                          print("startup hotfix applied")
+                      else:
+                          print("startup hotfix: no patterns matched — source may have changed")
📝 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
if old in text:
text = text.replace(old, new, 1)
old2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": question.prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=question.prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
new2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n user_prompt = question.message or question.prompt\n if not user_prompt:\n raise HTTPException(status_code=422, detail=\"message or prompt is required\")\n\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": user_prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=user_prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
if old2 in text:
text = text.replace(old2, new2, 1)
p.write_text(text)
print("startup hotfix applied")
applied = False
if old in text:
text = text.replace(old, new, 1)
applied = True
old2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": question.prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=question.prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
new2 = """# Route to chat with OpenAI\n@app.post(\"/chat/\")\ndef ask_openai(question: Question):\n try:\n user_prompt = question.message or question.prompt\n if not user_prompt:\n raise HTTPException(status_code=422, detail=\"message or prompt is required\")\n\n response = client.chat.completions.create(\n model=\"gpt-4o\",\n messages=[{\"role\": \"user\", \"content\": user_prompt}]\n )\n result = response.choices[0].message.content\n\n # Store chat history in database\n db = SessionLocal()\n db.add(QueryHistory(question=user_prompt, response=result))\n db.commit()\n\n return {\"response\": result}\n except HTTPException:\n raise\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"""
if old2 in text:
text = text.replace(old2, new2, 1)
applied = True
p.write_text(text)
if applied:
print("startup hotfix applied")
else:
print("startup hotfix: no patterns matched — source may have changed")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@deployments/helm-charts/wso2-amp-platform-resources-extension/templates/component-types/agent-api.yaml`
around lines 168 - 177, The script unconditionally calls p.write_text(text) and
print("startup hotfix applied") even when no replacement occurred; modify the
flow to detect whether any replacement happened (e.g., track a boolean changed
flag or compare original_text vs text) after attempting both if old in text and
if old2 in text replacements, and only call p.write_text(text) and
print("startup hotfix applied") when changed is true; otherwise skip
writing/printing (or emit a distinct message) so p.write_text, print and the
variables old, old2, new, new2, text reflect actual changes.

PY
/layers/google.python.runtime/python/bin/python3 /tmp/amp_chat_hotfix.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Major: hardcoded GCP Buildpack interpreter path breaks non-GCP agent-api deployments.

/layers/google.python.runtime/python/bin/python3 only exists inside containers built with the Google Cloud Python Buildpack. The allowedWorkflows list on this same ClusterComponentType includes amp-docker and amp-ballerina-buildpack, neither of which will have this interpreter at that path. The python3 invocation will fail before any existence check in the script can run, again preventing exec /cnb/process/web from executing.

Consider falling back to a PATH-resolved interpreter so the script is a no-op for non-matching containers:

🛡️ Proposed fix — use PATH-resolved python3 with fallback
-                      /layers/google.python.runtime/python/bin/python3 /tmp/amp_chat_hotfix.py
+                      PYTHON3=$(command -v python3 2>/dev/null || echo /layers/google.python.runtime/python/bin/python3)
+                      "$PYTHON3" /tmp/amp_chat_hotfix.py

Note that this alone is still insufficient if /workspace/main.py is absent (see the preceding comment).

📝 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
/layers/google.python.runtime/python/bin/python3 /tmp/amp_chat_hotfix.py
PYTHON3=$(command -v python3 2>/dev/null || echo /layers/google.python.runtime/python/bin/python3)
"$PYTHON3" /tmp/amp_chat_hotfix.py
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@deployments/helm-charts/wso2-amp-platform-resources-extension/templates/component-types/agent-api.yaml`
at line 179, The container command hardcodes the GCP Buildpack interpreter path
"/layers/google.python.runtime/python/bin/python3" which will fail for non-GCP
workflows listed in allowedWorkflows (e.g., amp-docker,
amp-ballerina-buildpack); replace that hardcoded path with a PATH-resolved
fallback so non-GCP containers simply skip/noop: change the command that runs
"/tmp/amp_chat_hotfix.py" to invoke python via a PATH resolution (e.g., prefer
`python3` if available, fall back to `python` or skip) or wrap invocation in a
shell that checks for a python interpreter before executing the script, ensuring
exec /cnb/process/web still runs when no suitable interpreter exists.

exec /cnb/process/web
resources:
requests:
cpu: ${environmentConfigs.resources.requests.cpu}
Expand Down