Spaces:
Build error
Build error
GitHub Actions commited on
Commit ·
6146c81
1
Parent(s): 1f505c0
Sync from GitHub on master (Thu Nov 20 11:13:32 UTC 2025)
Browse files- app.py +0 -5
- hf-space/agentctl/llm.py +52 -31
- hf-space/backend/services/agent_service.py +19 -4
- hf-space/hf-space/agentctl/llm.py +49 -9
- hf-space/hf-space/app.py +18 -7
- hf-space/hf-space/backend/routers/agent.py +10 -3
- hf-space/hf-space/backend/services/agent_service.py +33 -46
- hf-space/hf-space/hf-space/agentctl/llm.py +12 -0
- hf-space/hf-space/hf-space/backend/main.py +2 -2
- hf-space/hf-space/hf-space/backend/routers/agent.py +44 -0
- hf-space/hf-space/hf-space/backend/services/agent_service.py +97 -11
- hf-space/hf-space/hf-space/hf-space/app.py +120 -43
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +27 -8
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/k8s_client.py +2 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py +10 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +119 -241
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py +11 -9
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/apply.py +67 -16
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/logs.py +15 -6
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/snapshot.py +45 -10
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/yaml_gen.py +24 -8
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/__init__.py +0 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py +30 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/__init__.py +0 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/apply.py +37 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/logs.py +11 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/snapshot.py +19 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/yaml_gen.py +23 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/__init__.py +0 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/agent_service.py +29 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/k8s_service.py +28 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +1 -8
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +211 -101
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/.github/workflows/sync-to-hf.yml +39 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/README.md +216 -7
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +49 -183
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/.gitignore +97 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/README.md +16 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/__init__.py +1 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/agent.py +297 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/config.py +11 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/k8s_client.py +209 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/schemas.py +44 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py +345 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt +6 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/k8s/deployment-example.yaml +22 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/k8s/job-example.yaml +21 -0
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt +7 -6
- hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt +2 -0
- hf-space/hf-space/requirements.txt +3 -0
app.py
CHANGED
|
@@ -81,11 +81,6 @@ def run_agent(instruction: str, namespace: str, kind: str):
|
|
| 81 |
except Exception as e:
|
| 82 |
return f"# ERROR: {e}", {}, "ERROR"
|
| 83 |
|
| 84 |
-
|
| 85 |
-
# -------------------------------------------------------------------
|
| 86 |
-
# Snapshot Formatter (Emoji-Free)
|
| 87 |
-
# -------------------------------------------------------------------
|
| 88 |
-
|
| 89 |
def format_snapshot(data):
|
| 90 |
lines = []
|
| 91 |
|
|
|
|
| 81 |
except Exception as e:
|
| 82 |
return f"# ERROR: {e}", {}, "ERROR"
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def format_snapshot(data):
|
| 85 |
lines = []
|
| 86 |
|
hf-space/agentctl/llm.py
CHANGED
|
@@ -5,48 +5,69 @@ from huggingface_hub import InferenceClient
|
|
| 5 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 6 |
HF_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct"
|
| 7 |
|
| 8 |
-
client = InferenceClient(
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
def _hf_call(prompt: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
try:
|
| 12 |
-
messages = [
|
| 13 |
-
{"role": "user", "content": prompt}
|
| 14 |
-
]
|
| 15 |
-
|
| 16 |
response = client.chat_completion(
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
top_p=0.9,
|
| 22 |
stream=False
|
| 23 |
)
|
| 24 |
-
|
| 25 |
-
if response
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# --- CLEANUP LOGIC END ---
|
| 42 |
-
|
| 43 |
-
print("[HF WARNING] Received empty response structure.")
|
| 44 |
-
return ""
|
| 45 |
|
| 46 |
except Exception as e:
|
| 47 |
print(f"[HF LLM ERROR] {e}")
|
| 48 |
return ""
|
| 49 |
|
|
|
|
| 50 |
async def ask_llm(prompt: str) -> str:
|
| 51 |
loop = asyncio.get_event_loop()
|
| 52 |
-
return await loop.run_in_executor(None, lambda: _hf_call(prompt))
|
|
|
|
| 5 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 6 |
HF_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct"
|
| 7 |
|
| 8 |
+
client = InferenceClient(
|
| 9 |
+
token=HF_TOKEN,
|
| 10 |
+
timeout=120
|
| 11 |
+
)
|
| 12 |
|
| 13 |
def _hf_call(prompt: str) -> str:
|
| 14 |
+
"""
|
| 15 |
+
Safe HF call:
|
| 16 |
+
- Uses chat_completion
|
| 17 |
+
- Cleans code fencing
|
| 18 |
+
- Hard-fails quietly so AgentService can fallback
|
| 19 |
+
"""
|
| 20 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
response = client.chat_completion(
|
| 22 |
+
model=HF_MODEL,
|
| 23 |
+
messages=[
|
| 24 |
+
{
|
| 25 |
+
"role": "system",
|
| 26 |
+
"content": (
|
| 27 |
+
"You are a STRICT Kubernetes generator. "
|
| 28 |
+
"You MUST always generate minimal, safe YAML.\n"
|
| 29 |
+
"YOU MUST OBEY:\n"
|
| 30 |
+
"- Create ONLY simple CPU-only Jobs.\n"
|
| 31 |
+
"- NEVER use GPU.\n"
|
| 32 |
+
"- NEVER use PyTorch, TensorFlow, ML training, train.py.\n"
|
| 33 |
+
"- Allowed images ONLY: busybox OR python:3.10-slim.\n"
|
| 34 |
+
"- Commands must be simple: echo, sleep, ls.\n"
|
| 35 |
+
"- Memory <= 256Mi, CPU <= 250m.\n"
|
| 36 |
+
"- No external registries.\n"
|
| 37 |
+
"- Must fit in Minikube.\n"
|
| 38 |
+
"- YAML must ALWAYS be short, valid, deterministic.\n"
|
| 39 |
+
)
|
| 40 |
+
},
|
| 41 |
+
{"role": "user", "content": prompt}
|
| 42 |
+
],
|
| 43 |
+
max_tokens=400,
|
| 44 |
+
temperature=0.2,
|
| 45 |
top_p=0.9,
|
| 46 |
stream=False
|
| 47 |
)
|
| 48 |
+
|
| 49 |
+
if not response or not response.choices:
|
| 50 |
+
print("[HF WARNING] Empty response structure")
|
| 51 |
+
return ""
|
| 52 |
+
|
| 53 |
+
content = response.choices[0].message.content.strip()
|
| 54 |
+
|
| 55 |
+
# -------- Clean ```yaml fencing ----------
|
| 56 |
+
lines = content.splitlines()
|
| 57 |
+
|
| 58 |
+
if lines and lines[0].strip().startswith("```"):
|
| 59 |
+
lines = lines[1:]
|
| 60 |
+
|
| 61 |
+
if lines and lines[-1].strip().startswith("```"):
|
| 62 |
+
lines = lines[:-1]
|
| 63 |
+
|
| 64 |
+
return "\n".join(lines).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
except Exception as e:
|
| 67 |
print(f"[HF LLM ERROR] {e}")
|
| 68 |
return ""
|
| 69 |
|
| 70 |
+
|
| 71 |
async def ask_llm(prompt: str) -> str:
|
| 72 |
loop = asyncio.get_event_loop()
|
| 73 |
+
return await loop.run_in_executor(None, lambda: _hf_call(prompt))
|
hf-space/backend/services/agent_service.py
CHANGED
|
@@ -42,10 +42,25 @@ class AgentService:
|
|
| 42 |
# 1) LLM PLAN STEP
|
| 43 |
# -------------------------------
|
| 44 |
plan_prompt = f"""
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
try:
|
| 51 |
plan = await ask_llm(plan_prompt)
|
|
|
|
| 42 |
# 1) LLM PLAN STEP
|
| 43 |
# -------------------------------
|
| 44 |
plan_prompt = f"""
|
| 45 |
+
Convert this plan into valid Kubernetes YAML.
|
| 46 |
+
HARD RULES:
|
| 47 |
+
- kind: Job
|
| 48 |
+
- image: busybox or python:3.10-slim only
|
| 49 |
+
- restartPolicy: Never
|
| 50 |
+
- No GPUs, no ML, no training.
|
| 51 |
+
- Commands MUST be simple (echo/sleep/ls)
|
| 52 |
+
- No markdown
|
| 53 |
+
- No commentary
|
| 54 |
+
|
| 55 |
+
Plan:
|
| 56 |
+
{prompt}
|
| 57 |
+
Output:
|
| 58 |
+
- workload type
|
| 59 |
+
- image
|
| 60 |
+
- command
|
| 61 |
+
- args
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
|
| 65 |
try:
|
| 66 |
plan = await ask_llm(plan_prompt)
|
hf-space/hf-space/agentctl/llm.py
CHANGED
|
@@ -1,12 +1,52 @@
|
|
| 1 |
-
# agentctl/llm.py
|
| 2 |
import os
|
| 3 |
-
|
|
|
|
| 4 |
|
| 5 |
-
|
|
|
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
+
import asyncio
|
| 3 |
+
from huggingface_hub import InferenceClient
|
| 4 |
|
| 5 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 6 |
+
HF_MODEL = "Qwen/Qwen2.5-Coder-32B-Instruct"
|
| 7 |
|
| 8 |
+
client = InferenceClient(token=HF_TOKEN, timeout=120)
|
| 9 |
+
|
| 10 |
+
def _hf_call(prompt: str) -> str:
|
| 11 |
+
try:
|
| 12 |
+
messages = [
|
| 13 |
+
{"role": "user", "content": prompt}
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
response = client.chat_completion(
|
| 17 |
+
messages,
|
| 18 |
+
model=HF_MODEL,
|
| 19 |
+
max_tokens=500,
|
| 20 |
+
temperature=0.2,
|
| 21 |
+
top_p=0.9,
|
| 22 |
+
stream=False
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
if response and response.choices:
|
| 26 |
+
content = response.choices[0].message.content
|
| 27 |
+
|
| 28 |
+
# --- CLEANUP LOGIC START ---
|
| 29 |
+
# The model often wraps code in ```yaml ... ```. We must strip this.
|
| 30 |
+
lines = content.strip().splitlines()
|
| 31 |
+
|
| 32 |
+
# Remove the top line if it starts with ``` (e.g., ```yaml)
|
| 33 |
+
if lines and lines[0].strip().startswith("```"):
|
| 34 |
+
lines = lines[1:]
|
| 35 |
+
|
| 36 |
+
# Remove the bottom line if it is just ```
|
| 37 |
+
if lines and lines[-1].strip().startswith("```"):
|
| 38 |
+
lines = lines[:-1]
|
| 39 |
+
|
| 40 |
+
return "\n".join(lines).strip()
|
| 41 |
+
# --- CLEANUP LOGIC END ---
|
| 42 |
+
|
| 43 |
+
print("[HF WARNING] Received empty response structure.")
|
| 44 |
+
return ""
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"[HF LLM ERROR] {e}")
|
| 48 |
+
return ""
|
| 49 |
+
|
| 50 |
+
async def ask_llm(prompt: str) -> str:
|
| 51 |
+
loop = asyncio.get_event_loop()
|
| 52 |
+
return await loop.run_in_executor(None, lambda: _hf_call(prompt))
|
hf-space/hf-space/app.py
CHANGED
|
@@ -68,12 +68,19 @@ def run_agent(instruction: str, namespace: str, kind: str):
|
|
| 68 |
r.raise_for_status()
|
| 69 |
|
| 70 |
data = r.json()
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
except Exception as e:
|
| 76 |
-
return f"# ERROR: {e}", {}
|
|
|
|
| 77 |
|
| 78 |
# -------------------------------------------------------------------
|
| 79 |
# Snapshot Formatter (Emoji-Free)
|
|
@@ -270,14 +277,18 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 270 |
run_btn = gr.Button("Run Agent")
|
| 271 |
|
| 272 |
agent_yaml = gr.Code(label="Generated YAML", language="yaml")
|
| 273 |
-
agent_result = gr.JSON(label="
|
|
|
|
|
|
|
| 274 |
|
| 275 |
run_btn.click(
|
| 276 |
fn=run_agent,
|
| 277 |
inputs=[instruction, namespace2, kind2],
|
| 278 |
-
outputs=[agent_yaml, agent_result]
|
| 279 |
)
|
| 280 |
|
|
|
|
|
|
|
| 281 |
return ui
|
| 282 |
|
| 283 |
|
|
|
|
| 68 |
r.raise_for_status()
|
| 69 |
|
| 70 |
data = r.json()
|
| 71 |
+
|
| 72 |
+
yaml_text = data.get("yaml", "# No YAML")
|
| 73 |
+
result = data.get("result", {})
|
| 74 |
+
mode = data.get("mode", "unknown")
|
| 75 |
+
model = data.get("model", "none")
|
| 76 |
+
|
| 77 |
+
info = f"Mode: {mode}\nModel: {model}"
|
| 78 |
+
|
| 79 |
+
return yaml_text, result, info
|
| 80 |
+
|
| 81 |
except Exception as e:
|
| 82 |
+
return f"# ERROR: {e}", {}, "ERROR"
|
| 83 |
+
|
| 84 |
|
| 85 |
# -------------------------------------------------------------------
|
| 86 |
# Snapshot Formatter (Emoji-Free)
|
|
|
|
| 277 |
run_btn = gr.Button("Run Agent")
|
| 278 |
|
| 279 |
agent_yaml = gr.Code(label="Generated YAML", language="yaml")
|
| 280 |
+
agent_result = gr.JSON(label="Apply Result")
|
| 281 |
+
agent_info = gr.Textbox(label="Agent Info (LLM / Fallback)")
|
| 282 |
+
|
| 283 |
|
| 284 |
run_btn.click(
|
| 285 |
fn=run_agent,
|
| 286 |
inputs=[instruction, namespace2, kind2],
|
| 287 |
+
outputs=[agent_yaml, agent_result, agent_info]
|
| 288 |
)
|
| 289 |
|
| 290 |
+
|
| 291 |
+
|
| 292 |
return ui
|
| 293 |
|
| 294 |
|
hf-space/hf-space/backend/routers/agent.py
CHANGED
|
@@ -13,7 +13,7 @@ async def run_agent(payload: dict):
|
|
| 13 |
Agentic flow:
|
| 14 |
1. LLM → Plan → YAML (fallback: K8sAgent)
|
| 15 |
2. Apply YAML to Kubernetes
|
| 16 |
-
3. Return
|
| 17 |
"""
|
| 18 |
|
| 19 |
instruction = payload.get("instruction")
|
|
@@ -23,13 +23,17 @@ async def run_agent(payload: dict):
|
|
| 23 |
if not instruction:
|
| 24 |
raise HTTPException(status_code=400, detail="Missing instruction")
|
| 25 |
|
| 26 |
-
# 1. Generate YAML (LLM + fallback)
|
| 27 |
-
|
| 28 |
instruction,
|
| 29 |
namespace,
|
| 30 |
kind
|
| 31 |
)
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# 2. Apply YAML to K8s
|
| 34 |
client = K8sClient()
|
| 35 |
try:
|
|
@@ -37,8 +41,11 @@ async def run_agent(payload: dict):
|
|
| 37 |
except Exception as e:
|
| 38 |
raise HTTPException(status_code=500, detail=f"K8s apply failed: {e}")
|
| 39 |
|
|
|
|
| 40 |
return {
|
| 41 |
"instruction": instruction,
|
| 42 |
"yaml": yaml_text,
|
|
|
|
|
|
|
| 43 |
"result": apply_result
|
| 44 |
}
|
|
|
|
| 13 |
Agentic flow:
|
| 14 |
1. LLM → Plan → YAML (fallback: K8sAgent)
|
| 15 |
2. Apply YAML to Kubernetes
|
| 16 |
+
3. Return yaml, mode, model, result
|
| 17 |
"""
|
| 18 |
|
| 19 |
instruction = payload.get("instruction")
|
|
|
|
| 23 |
if not instruction:
|
| 24 |
raise HTTPException(status_code=400, detail="Missing instruction")
|
| 25 |
|
| 26 |
+
# 1. Generate YAML (LLM + fallback) → returns dict
|
| 27 |
+
agent_output = await agent_service.generate_yaml(
|
| 28 |
instruction,
|
| 29 |
namespace,
|
| 30 |
kind
|
| 31 |
)
|
| 32 |
|
| 33 |
+
yaml_text = agent_output["yaml"]
|
| 34 |
+
mode = agent_output["mode"]
|
| 35 |
+
model = agent_output["model"]
|
| 36 |
+
|
| 37 |
# 2. Apply YAML to K8s
|
| 38 |
client = K8sClient()
|
| 39 |
try:
|
|
|
|
| 41 |
except Exception as e:
|
| 42 |
raise HTTPException(status_code=500, detail=f"K8s apply failed: {e}")
|
| 43 |
|
| 44 |
+
# 3. Return everything to UI
|
| 45 |
return {
|
| 46 |
"instruction": instruction,
|
| 47 |
"yaml": yaml_text,
|
| 48 |
+
"mode": mode,
|
| 49 |
+
"model": model,
|
| 50 |
"result": apply_result
|
| 51 |
}
|
hf-space/hf-space/backend/services/agent_service.py
CHANGED
|
@@ -3,7 +3,7 @@ import yaml
|
|
| 3 |
|
| 4 |
from agentctl.agent import K8sAgent
|
| 5 |
from agentctl.config import settings
|
| 6 |
-
from agentctl.llm import ask_llm
|
| 7 |
|
| 8 |
|
| 9 |
def randomize_name(yaml_text: str) -> str:
|
|
@@ -28,88 +28,75 @@ def randomize_name(yaml_text: str) -> str:
|
|
| 28 |
|
| 29 |
except Exception:
|
| 30 |
return yaml_text
|
|
|
|
| 31 |
|
| 32 |
class AgentService:
|
| 33 |
-
"""
|
| 34 |
-
Upgraded AgentService:
|
| 35 |
-
- Primary: LLM generates plan → YAML
|
| 36 |
-
- Fallback: Use existing K8sAgent.nl_to_resource_yaml
|
| 37 |
-
"""
|
| 38 |
def __init__(self):
|
| 39 |
-
# Your existing rule-based agent
|
| 40 |
self.agent = K8sAgent()
|
| 41 |
|
| 42 |
async def generate_yaml(self, prompt: str, namespace: str | None, kind: str | None):
|
| 43 |
-
# default namespace
|
| 44 |
ns = namespace or settings.k8s_namespace
|
| 45 |
kind_sel = None if (kind is None or kind == "Auto") else kind
|
| 46 |
|
| 47 |
# -------------------------------
|
| 48 |
-
# 1) LLM
|
| 49 |
# -------------------------------
|
| 50 |
plan_prompt = f"""
|
| 51 |
You are an expert Kubernetes engineer.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
step-by-step Kubernetes workload plan (NO YAML yet).
|
| 55 |
-
|
| 56 |
-
Instruction:
|
| 57 |
-
{prompt}
|
| 58 |
-
|
| 59 |
-
Include:
|
| 60 |
-
- workload type (Job / Deployment / CronJob)
|
| 61 |
-
- container image
|
| 62 |
-
- commands/args
|
| 63 |
-
- resources
|
| 64 |
-
- environment variables
|
| 65 |
-
- namespace: {ns}
|
| 66 |
-
- override kind (if the user forced one): {kind_sel or "Auto"}
|
| 67 |
"""
|
| 68 |
|
| 69 |
try:
|
| 70 |
plan = await ask_llm(plan_prompt)
|
|
|
|
| 71 |
except Exception as e:
|
| 72 |
-
print(f"[LLM ERROR: plan]
|
| 73 |
-
|
| 74 |
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 75 |
-
prompt,
|
| 76 |
-
namespace=ns,
|
| 77 |
-
kind=kind_sel,
|
| 78 |
)
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
# -------------------------------
|
| 82 |
-
# 2) LLM YAML
|
| 83 |
# -------------------------------
|
| 84 |
yaml_prompt = f"""
|
| 85 |
-
Convert
|
| 86 |
-
Output
|
| 87 |
|
| 88 |
-
Plan:
|
| 89 |
{plan}
|
| 90 |
"""
|
| 91 |
|
| 92 |
try:
|
| 93 |
yaml_text = await ask_llm(yaml_prompt)
|
| 94 |
|
| 95 |
-
|
| 96 |
-
if not yaml_text or len(yaml_text.strip()) == 0:
|
| 97 |
raise ValueError("Empty YAML from LLM")
|
| 98 |
|
| 99 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
except Exception as e:
|
| 102 |
-
print(f"[LLM ERROR: yaml]
|
| 103 |
|
| 104 |
-
# -------------------------------
|
| 105 |
-
# 3) Fallback to your legacy NLP → YAML
|
| 106 |
-
# -------------------------------
|
| 107 |
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 108 |
-
prompt,
|
| 109 |
-
namespace=ns,
|
| 110 |
-
kind=kind_sel,
|
| 111 |
)
|
| 112 |
-
return randomize_name(yaml_text)
|
| 113 |
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
agent_service = AgentService()
|
|
|
|
| 3 |
|
| 4 |
from agentctl.agent import K8sAgent
|
| 5 |
from agentctl.config import settings
|
| 6 |
+
from agentctl.llm import ask_llm, HF_MODEL # <--- import model name
|
| 7 |
|
| 8 |
|
| 9 |
def randomize_name(yaml_text: str) -> str:
|
|
|
|
| 28 |
|
| 29 |
except Exception:
|
| 30 |
return yaml_text
|
| 31 |
+
|
| 32 |
|
| 33 |
class AgentService:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
def __init__(self):
|
|
|
|
| 35 |
self.agent = K8sAgent()
|
| 36 |
|
| 37 |
async def generate_yaml(self, prompt: str, namespace: str | None, kind: str | None):
|
|
|
|
| 38 |
ns = namespace or settings.k8s_namespace
|
| 39 |
kind_sel = None if (kind is None or kind == "Auto") else kind
|
| 40 |
|
| 41 |
# -------------------------------
|
| 42 |
+
# 1) LLM PLAN STEP
|
| 43 |
# -------------------------------
|
| 44 |
plan_prompt = f"""
|
| 45 |
You are an expert Kubernetes engineer.
|
| 46 |
+
Convert this natural-language instruction into a step-by-step plan.
|
| 47 |
+
Instruction: {prompt}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
"""
|
| 49 |
|
| 50 |
try:
|
| 51 |
plan = await ask_llm(plan_prompt)
|
| 52 |
+
|
| 53 |
except Exception as e:
|
| 54 |
+
print(f"[LLM ERROR: plan] Fallback. Error: {e}")
|
| 55 |
+
|
| 56 |
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 57 |
+
prompt, namespace=ns, kind=kind_sel
|
|
|
|
|
|
|
| 58 |
)
|
| 59 |
+
|
| 60 |
+
return {
|
| 61 |
+
"yaml": randomize_name(yaml_text),
|
| 62 |
+
"mode": "fallback",
|
| 63 |
+
"model": None
|
| 64 |
+
}
|
| 65 |
|
| 66 |
# -------------------------------
|
| 67 |
+
# 2) LLM YAML STEP
|
| 68 |
# -------------------------------
|
| 69 |
yaml_prompt = f"""
|
| 70 |
+
Convert this Kubernetes plan into valid Kubernetes YAML.
|
| 71 |
+
Output ONLY YAML:
|
| 72 |
|
|
|
|
| 73 |
{plan}
|
| 74 |
"""
|
| 75 |
|
| 76 |
try:
|
| 77 |
yaml_text = await ask_llm(yaml_prompt)
|
| 78 |
|
| 79 |
+
if not yaml_text or yaml_text.strip() == "":
|
|
|
|
| 80 |
raise ValueError("Empty YAML from LLM")
|
| 81 |
|
| 82 |
+
return {
|
| 83 |
+
"yaml": randomize_name(yaml_text),
|
| 84 |
+
"mode": "llm",
|
| 85 |
+
"model": HF_MODEL
|
| 86 |
+
}
|
| 87 |
|
| 88 |
except Exception as e:
|
| 89 |
+
print(f"[LLM ERROR: yaml] Fallback. Error: {e}")
|
| 90 |
|
|
|
|
|
|
|
|
|
|
| 91 |
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 92 |
+
prompt, namespace=ns, kind=kind_sel
|
|
|
|
|
|
|
| 93 |
)
|
|
|
|
| 94 |
|
| 95 |
+
return {
|
| 96 |
+
"yaml": randomize_name(yaml_text),
|
| 97 |
+
"mode": "fallback",
|
| 98 |
+
"model": None
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
agent_service = AgentService()
|
hf-space/hf-space/hf-space/agentctl/llm.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# agentctl/llm.py
|
| 2 |
+
import os
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
|
| 5 |
+
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 6 |
+
|
| 7 |
+
async def ask_llm(prompt: str, model="gpt-4.1"):
|
| 8 |
+
res = client.chat.completions.create(
|
| 9 |
+
model=model,
|
| 10 |
+
messages=[{"role": "user", "content": prompt}]
|
| 11 |
+
)
|
| 12 |
+
return res.choices[0].message.content
|
hf-space/hf-space/hf-space/backend/main.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
|
| 4 |
-
from backend.routers import yaml_gen, apply, snapshot, logs
|
| 5 |
|
| 6 |
app = FastAPI(
|
| 7 |
title="AgentCTL Backend API",
|
|
@@ -30,7 +30,7 @@ app.include_router(yaml_gen.router, prefix="/api")
|
|
| 30 |
app.include_router(apply.router, prefix="/api")
|
| 31 |
app.include_router(snapshot.router, prefix="/api")
|
| 32 |
app.include_router(logs.router, prefix="/api")
|
| 33 |
-
|
| 34 |
|
| 35 |
@app.get("/debug-env")
|
| 36 |
def debug_env():
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
|
| 4 |
+
from backend.routers import yaml_gen, apply, snapshot, logs, agent
|
| 5 |
|
| 6 |
app = FastAPI(
|
| 7 |
title="AgentCTL Backend API",
|
|
|
|
| 30 |
app.include_router(apply.router, prefix="/api")
|
| 31 |
app.include_router(snapshot.router, prefix="/api")
|
| 32 |
app.include_router(logs.router, prefix="/api")
|
| 33 |
+
app.include_router(agent.router, prefix="/api")
|
| 34 |
|
| 35 |
@app.get("/debug-env")
|
| 36 |
def debug_env():
|
hf-space/hf-space/hf-space/backend/routers/agent.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# routers/agent.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, HTTPException
|
| 4 |
+
from backend.services.agent_service import agent_service
|
| 5 |
+
from agentctl.k8s_client import K8sClient
|
| 6 |
+
|
| 7 |
+
router = APIRouter()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@router.post("/agent")
|
| 11 |
+
async def run_agent(payload: dict):
|
| 12 |
+
"""
|
| 13 |
+
Agentic flow:
|
| 14 |
+
1. LLM → Plan → YAML (fallback: K8sAgent)
|
| 15 |
+
2. Apply YAML to Kubernetes
|
| 16 |
+
3. Return plan, yaml, result
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
instruction = payload.get("instruction")
|
| 20 |
+
namespace = payload.get("namespace")
|
| 21 |
+
kind = payload.get("kind")
|
| 22 |
+
|
| 23 |
+
if not instruction:
|
| 24 |
+
raise HTTPException(status_code=400, detail="Missing instruction")
|
| 25 |
+
|
| 26 |
+
# 1. Generate YAML (LLM + fallback)
|
| 27 |
+
yaml_text = await agent_service.generate_yaml(
|
| 28 |
+
instruction,
|
| 29 |
+
namespace,
|
| 30 |
+
kind
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# 2. Apply YAML to K8s
|
| 34 |
+
client = K8sClient()
|
| 35 |
+
try:
|
| 36 |
+
apply_result = client.apply_manifest(yaml_text)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
raise HTTPException(status_code=500, detail=f"K8s apply failed: {e}")
|
| 39 |
+
|
| 40 |
+
return {
|
| 41 |
+
"instruction": instruction,
|
| 42 |
+
"yaml": yaml_text,
|
| 43 |
+
"result": apply_result
|
| 44 |
+
}
|
hf-space/hf-space/hf-space/backend/services/agent_service.py
CHANGED
|
@@ -1,29 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from agentctl.agent import K8sAgent
|
| 2 |
from agentctl.config import settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class AgentService:
|
| 6 |
"""
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
"""
|
| 9 |
-
|
| 10 |
def __init__(self):
|
|
|
|
| 11 |
self.agent = K8sAgent()
|
| 12 |
|
| 13 |
-
def generate_yaml(self, prompt: str, namespace: str | None, kind: str | None):
|
| 14 |
-
#
|
| 15 |
ns = namespace or settings.k8s_namespace
|
| 16 |
kind_sel = None if (kind is None or kind == "Auto") else kind
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
)
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
# Shared singleton
|
| 29 |
agent_service = AgentService()
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
import yaml
|
| 3 |
+
|
| 4 |
from agentctl.agent import K8sAgent
|
| 5 |
from agentctl.config import settings
|
| 6 |
+
from agentctl.llm import ask_llm
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def randomize_name(yaml_text: str) -> str:
|
| 10 |
+
try:
|
| 11 |
+
obj = yaml.safe_load(yaml_text)
|
| 12 |
+
if not isinstance(obj, dict):
|
| 13 |
+
return yaml_text
|
| 14 |
+
|
| 15 |
+
kind = obj.get("kind")
|
| 16 |
+
if kind not in ("Job", "Deployment", "CronJob"):
|
| 17 |
+
return yaml_text
|
| 18 |
+
|
| 19 |
+
metadata = obj.setdefault("metadata", {})
|
| 20 |
+
name = metadata.get("name")
|
| 21 |
+
if not name:
|
| 22 |
+
return yaml_text
|
| 23 |
|
| 24 |
+
suffix = str(uuid.uuid4())[:5]
|
| 25 |
+
metadata["name"] = f"{name}-{suffix}"
|
| 26 |
+
|
| 27 |
+
return yaml.dump(obj, sort_keys=False)
|
| 28 |
+
|
| 29 |
+
except Exception:
|
| 30 |
+
return yaml_text
|
| 31 |
|
| 32 |
class AgentService:
|
| 33 |
"""
|
| 34 |
+
Upgraded AgentService:
|
| 35 |
+
- Primary: LLM generates plan → YAML
|
| 36 |
+
- Fallback: Use existing K8sAgent.nl_to_resource_yaml
|
| 37 |
"""
|
|
|
|
| 38 |
def __init__(self):
|
| 39 |
+
# Your existing rule-based agent
|
| 40 |
self.agent = K8sAgent()
|
| 41 |
|
| 42 |
+
async def generate_yaml(self, prompt: str, namespace: str | None, kind: str | None):
|
| 43 |
+
# default namespace
|
| 44 |
ns = namespace or settings.k8s_namespace
|
| 45 |
kind_sel = None if (kind is None or kind == "Auto") else kind
|
| 46 |
|
| 47 |
+
# -------------------------------
|
| 48 |
+
# 1) LLM Planning Step
|
| 49 |
+
# -------------------------------
|
| 50 |
+
plan_prompt = f"""
|
| 51 |
+
You are an expert Kubernetes engineer.
|
|
|
|
| 52 |
|
| 53 |
+
Convert this natural-language instruction into a detailed,
|
| 54 |
+
step-by-step Kubernetes workload plan (NO YAML yet).
|
| 55 |
+
|
| 56 |
+
Instruction:
|
| 57 |
+
{prompt}
|
| 58 |
+
|
| 59 |
+
Include:
|
| 60 |
+
- workload type (Job / Deployment / CronJob)
|
| 61 |
+
- container image
|
| 62 |
+
- commands/args
|
| 63 |
+
- resources
|
| 64 |
+
- environment variables
|
| 65 |
+
- namespace: {ns}
|
| 66 |
+
- override kind (if the user forced one): {kind_sel or "Auto"}
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
try:
|
| 70 |
+
plan = await ask_llm(plan_prompt)
|
| 71 |
+
except Exception as e:
|
| 72 |
+
print(f"[LLM ERROR: plan] Falling back. Error: {e}")
|
| 73 |
+
# fallback immediately
|
| 74 |
+
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 75 |
+
prompt,
|
| 76 |
+
namespace=ns,
|
| 77 |
+
kind=kind_sel,
|
| 78 |
+
)
|
| 79 |
+
return yaml_text
|
| 80 |
+
|
| 81 |
+
# -------------------------------
|
| 82 |
+
# 2) LLM YAML Generation Step
|
| 83 |
+
# -------------------------------
|
| 84 |
+
yaml_prompt = f"""
|
| 85 |
+
Convert the following Kubernetes plan into VALID Kubernetes YAML.
|
| 86 |
+
Output only the YAML (no explanation, no markdown).
|
| 87 |
+
|
| 88 |
+
Plan:
|
| 89 |
+
{plan}
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
try:
|
| 93 |
+
yaml_text = await ask_llm(yaml_prompt)
|
| 94 |
+
|
| 95 |
+
# sanity: avoid hallucinated empty responses
|
| 96 |
+
if not yaml_text or len(yaml_text.strip()) == 0:
|
| 97 |
+
raise ValueError("Empty YAML from LLM")
|
| 98 |
+
|
| 99 |
+
return randomize_name(yaml_text)
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
print(f"[LLM ERROR: yaml] Falling back. Error: {e}")
|
| 103 |
|
| 104 |
+
# -------------------------------
|
| 105 |
+
# 3) Fallback to your legacy NLP → YAML
|
| 106 |
+
# -------------------------------
|
| 107 |
+
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 108 |
+
prompt,
|
| 109 |
+
namespace=ns,
|
| 110 |
+
kind=kind_sel,
|
| 111 |
+
)
|
| 112 |
+
return randomize_name(yaml_text)
|
| 113 |
|
| 114 |
# Shared singleton
|
| 115 |
agent_service = AgentService()
|
hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -17,7 +17,7 @@ BACKEND_URL = os.getenv(
|
|
| 17 |
"http://127.0.0.1:9000" # default for local development
|
| 18 |
)
|
| 19 |
|
| 20 |
-
print(f"
|
| 21 |
|
| 22 |
# Helper to join backend URL
|
| 23 |
def api(path: str) -> str:
|
|
@@ -44,41 +44,75 @@ def generate_yaml(prompt: str, namespace: str, kind: str) -> str:
|
|
| 44 |
|
| 45 |
|
| 46 |
def apply_yaml(yaml_text: str) -> tuple[str, str]:
|
| 47 |
-
"""
|
| 48 |
-
Call FastAPI: POST /api/apply
|
| 49 |
-
Backend returns:
|
| 50 |
-
{ "success": bool, "message": str, "raw": str }
|
| 51 |
-
"""
|
| 52 |
try:
|
| 53 |
r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
|
| 54 |
r.raise_for_status()
|
| 55 |
data = r.json()
|
| 56 |
-
|
| 57 |
-
# CORRECT FIX: backend sends "message" not "status"
|
| 58 |
message = data.get("message", "")
|
| 59 |
raw = data.get("raw", "")
|
| 60 |
-
|
| 61 |
return message, raw
|
| 62 |
-
|
| 63 |
except Exception as e:
|
| 64 |
-
return f"
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
"""
|
| 69 |
-
Call FastAPI: GET /api/snapshot
|
| 70 |
-
Backend returns JSON (not 'snapshot' field)
|
| 71 |
-
We must pretty-format the JSON output.
|
| 72 |
-
"""
|
| 73 |
try:
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
r.raise_for_status()
|
| 76 |
-
data = r.json()
|
| 77 |
-
import json
|
| 78 |
-
return json.dumps(data, indent=2)
|
| 79 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
except Exception as e:
|
| 81 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def get_logs_once(pod_name: str, tail: int) -> str:
|
|
@@ -89,7 +123,7 @@ def get_logs_once(pod_name: str, tail: int) -> str:
|
|
| 89 |
r.raise_for_status()
|
| 90 |
return r.json().get("logs", "No logs received")
|
| 91 |
except Exception as e:
|
| 92 |
-
return f"
|
| 93 |
|
| 94 |
|
| 95 |
def follow_logs(pod_name: str, tail: int):
|
|
@@ -108,13 +142,12 @@ def build_ui():
|
|
| 108 |
with gr.Blocks(title="AgentCTL Frontend (FastAPI Backend)") as ui:
|
| 109 |
|
| 110 |
gr.Markdown(f"""
|
| 111 |
-
#
|
| 112 |
-
FastAPI Backend URL in use:
|
| 113 |
|
| 114 |
-
|
|
|
|
| 115 |
|
| 116 |
Set `AGENTCTL_BACKEND_URL` to override.
|
| 117 |
-
|
| 118 |
---
|
| 119 |
""")
|
| 120 |
|
|
@@ -133,22 +166,18 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 133 |
"- schedule a cleanup script every 5 minutes\n"
|
| 134 |
)
|
| 135 |
)
|
| 136 |
-
|
| 137 |
with gr.Column():
|
| 138 |
-
namespace = gr.Textbox(
|
| 139 |
-
label="Namespace",
|
| 140 |
-
value="default"
|
| 141 |
-
)
|
| 142 |
kind = gr.Dropdown(
|
| 143 |
label="Kind",
|
| 144 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 145 |
value="Auto"
|
| 146 |
)
|
| 147 |
|
| 148 |
-
generate_btn = gr.Button("Generate YAML"
|
| 149 |
yaml_box = gr.Code(label="Generated YAML", language="yaml")
|
| 150 |
|
| 151 |
-
apply_btn = gr.Button("Apply YAML to Cluster"
|
| 152 |
apply_status = gr.Textbox(label="Status")
|
| 153 |
apply_raw = gr.Textbox(label="Raw API Response")
|
| 154 |
|
|
@@ -157,7 +186,6 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 157 |
inputs=[prompt, namespace, kind],
|
| 158 |
outputs=yaml_box
|
| 159 |
)
|
| 160 |
-
|
| 161 |
apply_btn.click(
|
| 162 |
fn=apply_yaml,
|
| 163 |
inputs=[yaml_box],
|
|
@@ -168,10 +196,22 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 168 |
# TAB 2 — Cluster Dashboard
|
| 169 |
# ----------------------------------------------------------
|
| 170 |
with gr.Tab("Cluster Dashboard"):
|
| 171 |
-
refresh_button = gr.Button("Refresh Snapshot"
|
| 172 |
snapshot_box = gr.Textbox(label="Cluster Snapshot", lines=25)
|
| 173 |
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
|
| 176 |
# ----------------------------------------------------------
|
| 177 |
# TAB 3 — Pod Logs
|
|
@@ -179,14 +219,11 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 179 |
with gr.Tab("Pod Logs"):
|
| 180 |
pod_name = gr.Textbox(label="Pod Name")
|
| 181 |
tail_lines = gr.Slider(
|
| 182 |
-
label="Tail Lines",
|
| 183 |
-
minimum=10,
|
| 184 |
-
maximum=500,
|
| 185 |
-
value=100
|
| 186 |
)
|
| 187 |
|
| 188 |
get_logs_btn = gr.Button("Get Logs")
|
| 189 |
-
follow_logs_btn = gr.Button("Follow Logs (Live)"
|
| 190 |
logs_html = gr.HTML(label="Logs Terminal")
|
| 191 |
|
| 192 |
get_logs_btn.click(
|
|
@@ -201,6 +238,46 @@ Set `AGENTCTL_BACKEND_URL` to override.
|
|
| 201 |
outputs=logs_html
|
| 202 |
)
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
return ui
|
| 205 |
|
| 206 |
|
|
|
|
| 17 |
"http://127.0.0.1:9000" # default for local development
|
| 18 |
)
|
| 19 |
|
| 20 |
+
print(f"AgentCTL Backend URL: {BACKEND_URL}")
|
| 21 |
|
| 22 |
# Helper to join backend URL
|
| 23 |
def api(path: str) -> str:
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def apply_yaml(yaml_text: str) -> tuple[str, str]:
|
| 47 |
+
"""Call FastAPI: POST /api/apply"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
|
| 50 |
r.raise_for_status()
|
| 51 |
data = r.json()
|
| 52 |
+
# Backend sends message + raw
|
|
|
|
| 53 |
message = data.get("message", "")
|
| 54 |
raw = data.get("raw", "")
|
|
|
|
| 55 |
return message, raw
|
|
|
|
| 56 |
except Exception as e:
|
| 57 |
+
return f"ERROR: {e}", ""
|
| 58 |
|
| 59 |
+
def run_agent(instruction: str, namespace: str, kind: str):
|
| 60 |
+
"""Call FastAPI: POST /api/agent"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
try:
|
| 62 |
+
payload = {
|
| 63 |
+
"instruction": instruction,
|
| 64 |
+
"namespace": namespace,
|
| 65 |
+
"kind": kind
|
| 66 |
+
}
|
| 67 |
+
r = requests.post(api("/api/agent"), json=payload)
|
| 68 |
r.raise_for_status()
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
data = r.json()
|
| 71 |
+
return (
|
| 72 |
+
data.get("yaml", "# No YAML"),
|
| 73 |
+
data.get("result", {})
|
| 74 |
+
)
|
| 75 |
except Exception as e:
|
| 76 |
+
return f"# ERROR: {e}", {}
|
| 77 |
+
|
| 78 |
+
# -------------------------------------------------------------------
|
| 79 |
+
# Snapshot Formatter (Emoji-Free)
|
| 80 |
+
# -------------------------------------------------------------------
|
| 81 |
+
|
| 82 |
+
def format_snapshot(data):
|
| 83 |
+
lines = []
|
| 84 |
+
|
| 85 |
+
lines.append(f"Namespace: {data.get('namespace','default')}\n")
|
| 86 |
+
lines.append("==== JOBS ====\n")
|
| 87 |
+
|
| 88 |
+
for job in data.get("jobs", []):
|
| 89 |
+
if job["succeeded"] > 0:
|
| 90 |
+
status = "Completed"
|
| 91 |
+
elif job["failed"] > 0:
|
| 92 |
+
status = "Failed"
|
| 93 |
+
elif job["active"] > 0:
|
| 94 |
+
status = "Active"
|
| 95 |
+
else:
|
| 96 |
+
status = "Pending"
|
| 97 |
+
|
| 98 |
+
lines.append(
|
| 99 |
+
f"{job['name']}: {status} | Active={job['active']} | Success={job['succeeded']} | Failed={job['failed']}"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
lines.append("\n==== PODS ====\n")
|
| 103 |
+
|
| 104 |
+
for pod in data.get("pods", []):
|
| 105 |
+
phase = pod["phase"]
|
| 106 |
+
lines.append(f"{pod['name']}: {phase}")
|
| 107 |
+
|
| 108 |
+
lines.append("\n==== CRONJOBS ====\n")
|
| 109 |
+
|
| 110 |
+
for cj in data.get("cronjobs", []):
|
| 111 |
+
lines.append(
|
| 112 |
+
f"{cj['name']}: Active={cj['active']} | LastSchedule={cj['last_schedule']}"
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
return "\n".join(lines)
|
| 116 |
|
| 117 |
|
| 118 |
def get_logs_once(pod_name: str, tail: int) -> str:
|
|
|
|
| 123 |
r.raise_for_status()
|
| 124 |
return r.json().get("logs", "No logs received")
|
| 125 |
except Exception as e:
|
| 126 |
+
return f"ERROR: {e}"
|
| 127 |
|
| 128 |
|
| 129 |
def follow_logs(pod_name: str, tail: int):
|
|
|
|
| 142 |
with gr.Blocks(title="AgentCTL Frontend (FastAPI Backend)") as ui:
|
| 143 |
|
| 144 |
gr.Markdown(f"""
|
| 145 |
+
# AgentCTL (Frontend)
|
|
|
|
| 146 |
|
| 147 |
+
Backend URL in use:
|
| 148 |
+
**{BACKEND_URL}**
|
| 149 |
|
| 150 |
Set `AGENTCTL_BACKEND_URL` to override.
|
|
|
|
| 151 |
---
|
| 152 |
""")
|
| 153 |
|
|
|
|
| 166 |
"- schedule a cleanup script every 5 minutes\n"
|
| 167 |
)
|
| 168 |
)
|
|
|
|
| 169 |
with gr.Column():
|
| 170 |
+
namespace = gr.Textbox(label="Namespace", value="default")
|
|
|
|
|
|
|
|
|
|
| 171 |
kind = gr.Dropdown(
|
| 172 |
label="Kind",
|
| 173 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 174 |
value="Auto"
|
| 175 |
)
|
| 176 |
|
| 177 |
+
generate_btn = gr.Button("Generate YAML")
|
| 178 |
yaml_box = gr.Code(label="Generated YAML", language="yaml")
|
| 179 |
|
| 180 |
+
apply_btn = gr.Button("Apply YAML to Cluster")
|
| 181 |
apply_status = gr.Textbox(label="Status")
|
| 182 |
apply_raw = gr.Textbox(label="Raw API Response")
|
| 183 |
|
|
|
|
| 186 |
inputs=[prompt, namespace, kind],
|
| 187 |
outputs=yaml_box
|
| 188 |
)
|
|
|
|
| 189 |
apply_btn.click(
|
| 190 |
fn=apply_yaml,
|
| 191 |
inputs=[yaml_box],
|
|
|
|
| 196 |
# TAB 2 — Cluster Dashboard
|
| 197 |
# ----------------------------------------------------------
|
| 198 |
with gr.Tab("Cluster Dashboard"):
|
| 199 |
+
refresh_button = gr.Button("Refresh Snapshot")
|
| 200 |
snapshot_box = gr.Textbox(label="Cluster Snapshot", lines=25)
|
| 201 |
|
| 202 |
+
def get_formatted_snapshot():
|
| 203 |
+
try:
|
| 204 |
+
r = requests.get(api("/api/snapshot"))
|
| 205 |
+
r.raise_for_status()
|
| 206 |
+
data = r.json()
|
| 207 |
+
return format_snapshot(data)
|
| 208 |
+
except Exception as e:
|
| 209 |
+
return f"ERROR fetching snapshot: {e}"
|
| 210 |
+
|
| 211 |
+
refresh_button.click(
|
| 212 |
+
fn=get_formatted_snapshot,
|
| 213 |
+
outputs=snapshot_box
|
| 214 |
+
)
|
| 215 |
|
| 216 |
# ----------------------------------------------------------
|
| 217 |
# TAB 3 — Pod Logs
|
|
|
|
| 219 |
with gr.Tab("Pod Logs"):
|
| 220 |
pod_name = gr.Textbox(label="Pod Name")
|
| 221 |
tail_lines = gr.Slider(
|
| 222 |
+
label="Tail Lines", minimum=10, maximum=500, value=100
|
|
|
|
|
|
|
|
|
|
| 223 |
)
|
| 224 |
|
| 225 |
get_logs_btn = gr.Button("Get Logs")
|
| 226 |
+
follow_logs_btn = gr.Button("Follow Logs (Live)")
|
| 227 |
logs_html = gr.HTML(label="Logs Terminal")
|
| 228 |
|
| 229 |
get_logs_btn.click(
|
|
|
|
| 238 |
outputs=logs_html
|
| 239 |
)
|
| 240 |
|
| 241 |
+
# ----------------------------------------------------------
|
| 242 |
+
# TAB 4 — Agent Mode (LLM → YAML → Apply)
|
| 243 |
+
# ----------------------------------------------------------
|
| 244 |
+
with gr.Tab("Agent Mode"):
|
| 245 |
+
gr.Markdown("""
|
| 246 |
+
## Agentic LLM Mode
|
| 247 |
+
Enter natural language instructions and let the backend agent:
|
| 248 |
+
1. Plan the Kubernetes operation
|
| 249 |
+
2. Generate YAML (LLM → fallback → validated)
|
| 250 |
+
3. Apply it to the cluster
|
| 251 |
+
""")
|
| 252 |
+
|
| 253 |
+
instruction = gr.Textbox(
|
| 254 |
+
label="Instruction",
|
| 255 |
+
lines=4,
|
| 256 |
+
placeholder="e.g. deploy a pytorch training job with 1 GPU"
|
| 257 |
+
)
|
| 258 |
+
|
| 259 |
+
namespace2 = gr.Textbox(
|
| 260 |
+
label="Namespace",
|
| 261 |
+
value="default"
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
kind2 = gr.Dropdown(
|
| 265 |
+
label="Kind Override (optional)",
|
| 266 |
+
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 267 |
+
value="Auto"
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
run_btn = gr.Button("Run Agent")
|
| 271 |
+
|
| 272 |
+
agent_yaml = gr.Code(label="Generated YAML", language="yaml")
|
| 273 |
+
agent_result = gr.JSON(label="API Apply Result")
|
| 274 |
+
|
| 275 |
+
run_btn.click(
|
| 276 |
+
fn=run_agent,
|
| 277 |
+
inputs=[instruction, namespace2, kind2],
|
| 278 |
+
outputs=[agent_yaml, agent_result]
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
return ui
|
| 282 |
|
| 283 |
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -17,6 +17,8 @@ BACKEND_URL = os.getenv(
|
|
| 17 |
"http://127.0.0.1:9000" # default for local development
|
| 18 |
)
|
| 19 |
|
|
|
|
|
|
|
| 20 |
# Helper to join backend URL
|
| 21 |
def api(path: str) -> str:
|
| 22 |
return f"{BACKEND_URL}{path}"
|
|
@@ -42,22 +44,39 @@ def generate_yaml(prompt: str, namespace: str, kind: str) -> str:
|
|
| 42 |
|
| 43 |
|
| 44 |
def apply_yaml(yaml_text: str) -> tuple[str, str]:
|
| 45 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
try:
|
| 47 |
r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
|
| 48 |
r.raise_for_status()
|
| 49 |
data = r.json()
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
except Exception as e:
|
| 52 |
return f"❌ ERROR: {e}", ""
|
| 53 |
|
| 54 |
|
| 55 |
def get_snapshot() -> str:
|
| 56 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
try:
|
| 58 |
r = requests.get(api("/api/snapshot"))
|
| 59 |
r.raise_for_status()
|
| 60 |
-
|
|
|
|
|
|
|
|
|
|
| 61 |
except Exception as e:
|
| 62 |
return f"ERROR fetching snapshot: {e}"
|
| 63 |
|
|
@@ -74,7 +93,7 @@ def get_logs_once(pod_name: str, tail: int) -> str:
|
|
| 74 |
|
| 75 |
|
| 76 |
def follow_logs(pod_name: str, tail: int):
|
| 77 |
-
"""Streaming
|
| 78 |
while True:
|
| 79 |
html = get_logs_once(pod_name, tail)
|
| 80 |
yield html
|
|
@@ -94,7 +113,7 @@ FastAPI Backend URL in use:
|
|
| 94 |
|
| 95 |
### **🔗 {BACKEND_URL}**
|
| 96 |
|
| 97 |
-
|
| 98 |
|
| 99 |
---
|
| 100 |
""")
|
|
@@ -146,7 +165,7 @@ _(Set AGENTCTL_BACKEND_URL environment variable to override)_
|
|
| 146 |
)
|
| 147 |
|
| 148 |
# ----------------------------------------------------------
|
| 149 |
-
# TAB 2 — Dashboard
|
| 150 |
# ----------------------------------------------------------
|
| 151 |
with gr.Tab("Cluster Dashboard"):
|
| 152 |
refresh_button = gr.Button("Refresh Snapshot", variant="primary")
|
|
@@ -155,7 +174,7 @@ _(Set AGENTCTL_BACKEND_URL environment variable to override)_
|
|
| 155 |
refresh_button.click(fn=get_snapshot, outputs=snapshot_box)
|
| 156 |
|
| 157 |
# ----------------------------------------------------------
|
| 158 |
-
# TAB 3 —
|
| 159 |
# ----------------------------------------------------------
|
| 160 |
with gr.Tab("Pod Logs"):
|
| 161 |
pod_name = gr.Textbox(label="Pod Name")
|
|
|
|
| 17 |
"http://127.0.0.1:9000" # default for local development
|
| 18 |
)
|
| 19 |
|
| 20 |
+
print(f"🔗 AgentCTL Backend URL: {BACKEND_URL}")
|
| 21 |
+
|
| 22 |
# Helper to join backend URL
|
| 23 |
def api(path: str) -> str:
|
| 24 |
return f"{BACKEND_URL}{path}"
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
def apply_yaml(yaml_text: str) -> tuple[str, str]:
|
| 47 |
+
"""
|
| 48 |
+
Call FastAPI: POST /api/apply
|
| 49 |
+
Backend returns:
|
| 50 |
+
{ "success": bool, "message": str, "raw": str }
|
| 51 |
+
"""
|
| 52 |
try:
|
| 53 |
r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
|
| 54 |
r.raise_for_status()
|
| 55 |
data = r.json()
|
| 56 |
+
|
| 57 |
+
# CORRECT FIX: backend sends "message" not "status"
|
| 58 |
+
message = data.get("message", "")
|
| 59 |
+
raw = data.get("raw", "")
|
| 60 |
+
|
| 61 |
+
return message, raw
|
| 62 |
+
|
| 63 |
except Exception as e:
|
| 64 |
return f"❌ ERROR: {e}", ""
|
| 65 |
|
| 66 |
|
| 67 |
def get_snapshot() -> str:
|
| 68 |
+
"""
|
| 69 |
+
Call FastAPI: GET /api/snapshot
|
| 70 |
+
Backend returns JSON (not 'snapshot' field)
|
| 71 |
+
We must pretty-format the JSON output.
|
| 72 |
+
"""
|
| 73 |
try:
|
| 74 |
r = requests.get(api("/api/snapshot"))
|
| 75 |
r.raise_for_status()
|
| 76 |
+
data = r.json()
|
| 77 |
+
import json
|
| 78 |
+
return json.dumps(data, indent=2)
|
| 79 |
+
|
| 80 |
except Exception as e:
|
| 81 |
return f"ERROR fetching snapshot: {e}"
|
| 82 |
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
def follow_logs(pod_name: str, tail: int):
|
| 96 |
+
"""Streaming logs (auto-refresh every 2s)"""
|
| 97 |
while True:
|
| 98 |
html = get_logs_once(pod_name, tail)
|
| 99 |
yield html
|
|
|
|
| 113 |
|
| 114 |
### **🔗 {BACKEND_URL}**
|
| 115 |
|
| 116 |
+
Set `AGENTCTL_BACKEND_URL` to override.
|
| 117 |
|
| 118 |
---
|
| 119 |
""")
|
|
|
|
| 165 |
)
|
| 166 |
|
| 167 |
# ----------------------------------------------------------
|
| 168 |
+
# TAB 2 — Cluster Dashboard
|
| 169 |
# ----------------------------------------------------------
|
| 170 |
with gr.Tab("Cluster Dashboard"):
|
| 171 |
refresh_button = gr.Button("Refresh Snapshot", variant="primary")
|
|
|
|
| 174 |
refresh_button.click(fn=get_snapshot, outputs=snapshot_box)
|
| 175 |
|
| 176 |
# ----------------------------------------------------------
|
| 177 |
+
# TAB 3 — Pod Logs
|
| 178 |
# ----------------------------------------------------------
|
| 179 |
with gr.Tab("Pod Logs"):
|
| 180 |
pod_name = gr.Textbox(label="Pod Name")
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/k8s_client.py
CHANGED
|
@@ -18,6 +18,8 @@ from .schemas import (
|
|
| 18 |
CronJobStatus,
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
class K8sClient:
|
| 23 |
def __init__(self, base_url: str | None = None, namespace: str | None = None):
|
|
|
|
| 18 |
CronJobStatus,
|
| 19 |
)
|
| 20 |
|
| 21 |
+
from dotenv import load_dotenv
|
| 22 |
+
load_dotenv()
|
| 23 |
|
| 24 |
class K8sClient:
|
| 25 |
def __init__(self, base_url: str | None = None, namespace: str | None = None):
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py
CHANGED
|
@@ -30,3 +30,13 @@ app.include_router(yaml_gen.router, prefix="/api")
|
|
| 30 |
app.include_router(apply.router, prefix="/api")
|
| 31 |
app.include_router(snapshot.router, prefix="/api")
|
| 32 |
app.include_router(logs.router, prefix="/api")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
app.include_router(apply.router, prefix="/api")
|
| 31 |
app.include_router(snapshot.router, prefix="/api")
|
| 32 |
app.include_router(logs.router, prefix="/api")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.get("/debug-env")
|
| 36 |
+
def debug_env():
|
| 37 |
+
import os
|
| 38 |
+
return {
|
| 39 |
+
"K8S_API_BASE_URL": os.getenv("K8S_API_BASE_URL"),
|
| 40 |
+
"K8S_NAMESPACE": os.getenv("K8S_NAMESPACE"),
|
| 41 |
+
"K8S_VERIFY_SSL": os.getenv("K8S_VERIFY_SSL")
|
| 42 |
+
}
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -1,314 +1,192 @@
|
|
| 1 |
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
import re
|
| 5 |
import time
|
| 6 |
-
import
|
| 7 |
-
import yaml
|
| 8 |
-
from typing import Tuple
|
| 9 |
-
|
| 10 |
import gradio as gr
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
# ---------------------------------------------------------------------
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
def _get_client() -> K8sClient:
|
| 24 |
-
return K8sClient()
|
| 25 |
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
# ---------------------------------------------------------------------
|
| 30 |
-
|
| 31 |
-
def _inject_unique_name(yaml_text: str) -> str:
|
| 32 |
-
"""
|
| 33 |
-
Add unique suffix to metadata.name for Job, Deployment, CronJob.
|
| 34 |
-
Prevents 409 AlreadyExists errors.
|
| 35 |
-
"""
|
| 36 |
try:
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
if kind == "CronJob":
|
| 52 |
-
jt = data["spec"]["jobTemplate"]["spec"]["template"]
|
| 53 |
-
if "metadata" in jt:
|
| 54 |
-
if "labels" in jt["metadata"]:
|
| 55 |
-
jt["metadata"]["labels"]["app"] = new_name
|
| 56 |
-
else:
|
| 57 |
-
jt["metadata"]["labels"] = {"app": new_name}
|
| 58 |
-
|
| 59 |
-
return yaml.safe_dump(data)
|
| 60 |
-
|
| 61 |
-
except Exception:
|
| 62 |
-
return yaml_text
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
# ---------------------------------------------------------------------
|
| 66 |
-
# YAML Generation + Apply
|
| 67 |
-
# ---------------------------------------------------------------------
|
| 68 |
-
|
| 69 |
-
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
| 70 |
-
if not prompt.strip():
|
| 71 |
-
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
| 72 |
-
|
| 73 |
-
ns = namespace.strip() or settings.k8s_namespace
|
| 74 |
-
kind_sel = None if kind == "Auto" else kind
|
| 75 |
-
|
| 76 |
-
_, yaml_text = agent.nl_to_resource_yaml(prompt, namespace=ns, kind=kind_sel)
|
| 77 |
-
return yaml_text
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
| 81 |
-
if not yaml_text.strip():
|
| 82 |
-
return "No YAML to apply.", ""
|
| 83 |
-
|
| 84 |
-
yaml_text = _inject_unique_name(yaml_text)
|
| 85 |
-
|
| 86 |
-
client = _get_client()
|
| 87 |
-
result = client.apply_manifest(yaml_text)
|
| 88 |
-
|
| 89 |
try:
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
def get_cluster_snapshot() -> str:
|
| 103 |
-
client = _get_client()
|
| 104 |
-
snap = client.snapshot()
|
| 105 |
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
out.append("Jobs:")
|
| 109 |
-
if not snap.jobs:
|
| 110 |
-
out.append(" (no jobs)")
|
| 111 |
-
else:
|
| 112 |
-
for j in snap.jobs:
|
| 113 |
-
out.append(
|
| 114 |
-
f" - {j.name}: succeeded={j.succeeded}, failed={j.failed}, active={j.active}"
|
| 115 |
-
)
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
if not snap.pods:
|
| 120 |
-
out.append(" (no pods)")
|
| 121 |
-
else:
|
| 122 |
-
for p in snap.pods:
|
| 123 |
-
out.append(f" - {p.name}: phase={p.phase}")
|
| 124 |
-
|
| 125 |
-
out.append("")
|
| 126 |
-
out.append("Deployments:")
|
| 127 |
-
if not snap.deployments:
|
| 128 |
-
out.append(" (no deployments)")
|
| 129 |
-
else:
|
| 130 |
-
for d in snap.deployments:
|
| 131 |
-
out.append(f" - {d.name}: replicas={d.replicas}, ready={d.ready}")
|
| 132 |
-
|
| 133 |
-
out.append("")
|
| 134 |
-
out.append("CronJobs:")
|
| 135 |
-
if not snap.cronjobs:
|
| 136 |
-
out.append(" (no cronjobs)")
|
| 137 |
-
else:
|
| 138 |
-
for c in snap.cronjobs:
|
| 139 |
-
last = c.last_schedule or "never"
|
| 140 |
-
out.append(f" - {c.name}: active={c.active}, lastScheduleTime={last}")
|
| 141 |
-
|
| 142 |
-
return "\n".join(out)
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
# ---------------------------------------------------------------------
|
| 146 |
-
# Logs System
|
| 147 |
-
# ---------------------------------------------------------------------
|
| 148 |
-
|
| 149 |
-
def _colorize_logs(text: str) -> str:
|
| 150 |
-
if not text:
|
| 151 |
-
return "<span style='color:#888;'>No logs</span>"
|
| 152 |
-
|
| 153 |
-
html_lines = []
|
| 154 |
-
for line in text.splitlines():
|
| 155 |
-
if re.search(r"(error|failed|exception)", line, re.IGNORECASE):
|
| 156 |
-
color = "#ff4b4b"
|
| 157 |
-
elif re.search(r"warn", line, re.IGNORECASE):
|
| 158 |
-
color = "#f7c843"
|
| 159 |
-
elif re.search(r"(info|started|running|completed)", line, re.IGNORECASE):
|
| 160 |
-
color = "#5ad55a"
|
| 161 |
-
else:
|
| 162 |
-
color = "#d0d0d0"
|
| 163 |
-
|
| 164 |
-
safe = line.replace("<", "<").replace(">", ">")
|
| 165 |
-
html_lines.append(f"<span style='color:{color};'>{safe}</span>")
|
| 166 |
-
|
| 167 |
-
return "<br>".join(html_lines)
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def get_pod_logs_once(pod_name: str, tail_lines: int) -> str:
|
| 171 |
-
pod_name = pod_name.strip()
|
| 172 |
-
if not pod_name:
|
| 173 |
-
return "<span style='color:#ff4b4b;'>Enter a pod name.</span>"
|
| 174 |
-
|
| 175 |
-
client = _get_client()
|
| 176 |
-
raw = client.get_pod_logs(pod_name, tail_lines)
|
| 177 |
-
return _colorize_logs(raw)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def follow_pod_logs(pod_name: str, tail_lines: int):
|
| 181 |
-
pod_name = pod_name.strip()
|
| 182 |
-
if not pod_name:
|
| 183 |
-
yield "<span style='color:#ff4b4b;'>Enter a pod name.</span>"
|
| 184 |
-
return
|
| 185 |
-
|
| 186 |
-
client = _get_client()
|
| 187 |
while True:
|
| 188 |
-
|
| 189 |
-
yield
|
| 190 |
time.sleep(2)
|
| 191 |
|
| 192 |
|
| 193 |
-
# -------------------------------------------------------------------
|
| 194 |
-
#
|
| 195 |
-
# -------------------------------------------------------------------
|
| 196 |
-
|
| 197 |
-
TERMINAL_CSS = """
|
| 198 |
-
#logs_terminal {
|
| 199 |
-
background-color: #111;
|
| 200 |
-
color: #ddd;
|
| 201 |
-
font-family: monospace;
|
| 202 |
-
padding: 14px;
|
| 203 |
-
border-radius: 8px;
|
| 204 |
-
border: 1px solid #444;
|
| 205 |
-
height: 420px;
|
| 206 |
-
overflow-y: scroll;
|
| 207 |
-
white-space: pre-wrap;
|
| 208 |
-
}
|
| 209 |
-
"""
|
| 210 |
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
#
|
| 214 |
-
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
css=TERMINAL_CSS,
|
| 220 |
-
) as demo:
|
| 221 |
|
| 222 |
-
gr.Markdown("""
|
| 223 |
-
### 🔧 Environment
|
| 224 |
-
- `K8S_API_BASE_URL`
|
| 225 |
-
- `K8S_NAMESPACE`
|
| 226 |
-
- `K8S_VERIFY_SSL`
|
| 227 |
-
- `OPENAI_API_KEY` (optional)
|
| 228 |
---
|
| 229 |
""")
|
| 230 |
|
| 231 |
-
#
|
|
|
|
|
|
|
| 232 |
with gr.Tab("Create Resource"):
|
| 233 |
with gr.Row():
|
| 234 |
prompt = gr.Textbox(
|
| 235 |
-
label="Describe
|
|
|
|
| 236 |
placeholder=(
|
| 237 |
-
"
|
|
|
|
| 238 |
"- create an nginx deployment with 3 replicas\n"
|
| 239 |
"- schedule a cleanup script every 5 minutes\n"
|
| 240 |
-
)
|
| 241 |
-
lines=5,
|
| 242 |
)
|
| 243 |
|
| 244 |
with gr.Column():
|
| 245 |
namespace = gr.Textbox(
|
| 246 |
label="Namespace",
|
| 247 |
-
value=
|
| 248 |
)
|
| 249 |
kind = gr.Dropdown(
|
| 250 |
-
label="
|
| 251 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 252 |
-
value="Auto"
|
| 253 |
)
|
| 254 |
|
| 255 |
generate_btn = gr.Button("Generate YAML", variant="primary")
|
| 256 |
-
yaml_box = gr.Code(
|
| 257 |
|
| 258 |
-
apply_btn = gr.Button("Apply to
|
| 259 |
-
|
| 260 |
-
|
| 261 |
|
| 262 |
generate_btn.click(
|
| 263 |
-
|
| 264 |
inputs=[prompt, namespace, kind],
|
| 265 |
-
outputs=yaml_box
|
| 266 |
)
|
|
|
|
| 267 |
apply_btn.click(
|
| 268 |
-
|
| 269 |
inputs=[yaml_box],
|
| 270 |
-
outputs=[
|
| 271 |
)
|
| 272 |
|
| 273 |
-
#
|
|
|
|
|
|
|
| 274 |
with gr.Tab("Cluster Dashboard"):
|
| 275 |
-
|
| 276 |
-
snapshot_box = gr.Textbox(
|
| 277 |
|
| 278 |
-
|
| 279 |
-
get_cluster_snapshot,
|
| 280 |
-
inputs=[],
|
| 281 |
-
outputs=snapshot_box,
|
| 282 |
-
)
|
| 283 |
|
| 284 |
-
#
|
|
|
|
|
|
|
| 285 |
with gr.Tab("Pod Logs"):
|
| 286 |
-
pod_name = gr.Textbox(label="Pod
|
| 287 |
-
tail_lines = gr.Slider(
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
| 291 |
|
| 292 |
-
|
|
|
|
|
|
|
| 293 |
|
| 294 |
get_logs_btn.click(
|
| 295 |
-
|
| 296 |
inputs=[pod_name, tail_lines],
|
| 297 |
-
outputs=
|
| 298 |
)
|
| 299 |
|
| 300 |
-
# FIXED: removed `stream=True`
|
| 301 |
follow_logs_btn.click(
|
| 302 |
-
|
| 303 |
inputs=[pod_name, tail_lines],
|
| 304 |
-
outputs=
|
| 305 |
)
|
| 306 |
|
| 307 |
-
return
|
| 308 |
|
| 309 |
|
| 310 |
-
|
|
|
|
| 311 |
|
| 312 |
if __name__ == "__main__":
|
| 313 |
-
|
| 314 |
-
app.launch(server_name="0.0.0.0", server_port=port)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AgentCTL Gradio UI (Frontend)
|
| 4 |
+
Communicates with FastAPI backend instead of Kubernetes directly
|
| 5 |
+
"""
|
| 6 |
|
| 7 |
import os
|
|
|
|
| 8 |
import time
|
| 9 |
+
import requests
|
|
|
|
|
|
|
|
|
|
| 10 |
import gradio as gr
|
| 11 |
|
| 12 |
+
# -------------------------------------------------------------------
|
| 13 |
+
# Backend URL (FastAPI)
|
| 14 |
+
# -------------------------------------------------------------------
|
| 15 |
+
BACKEND_URL = os.getenv(
|
| 16 |
+
"AGENTCTL_BACKEND_URL",
|
| 17 |
+
"http://127.0.0.1:9000" # default for local development
|
| 18 |
+
)
|
|
|
|
| 19 |
|
| 20 |
+
# Helper to join backend URL
|
| 21 |
+
def api(path: str) -> str:
|
| 22 |
+
return f"{BACKEND_URL}{path}"
|
| 23 |
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
# -------------------------------------------------------------------
|
| 26 |
+
# API Client Helpers
|
| 27 |
+
# -------------------------------------------------------------------
|
| 28 |
|
| 29 |
+
def generate_yaml(prompt: str, namespace: str, kind: str) -> str:
|
| 30 |
+
"""Call FastAPI: POST /api/generate-yaml"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
try:
|
| 32 |
+
payload = {
|
| 33 |
+
"prompt": prompt,
|
| 34 |
+
"namespace": namespace,
|
| 35 |
+
"kind": kind
|
| 36 |
+
}
|
| 37 |
+
r = requests.post(api("/api/generate-yaml"), json=payload)
|
| 38 |
+
r.raise_for_status()
|
| 39 |
+
return r.json().get("yaml", "# No YAML returned")
|
| 40 |
+
except Exception as e:
|
| 41 |
+
return f"# ERROR: {e}"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def apply_yaml(yaml_text: str) -> tuple[str, str]:
|
| 45 |
+
"""Call FastAPI: POST /api/apply"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
try:
|
| 47 |
+
r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
|
| 48 |
+
r.raise_for_status()
|
| 49 |
+
data = r.json()
|
| 50 |
+
return data.get("status", ""), data.get("raw", "")
|
| 51 |
+
except Exception as e:
|
| 52 |
+
return f"❌ ERROR: {e}", ""
|
| 53 |
|
| 54 |
|
| 55 |
+
def get_snapshot() -> str:
|
| 56 |
+
"""Call FastAPI: GET /api/snapshot"""
|
| 57 |
+
try:
|
| 58 |
+
r = requests.get(api("/api/snapshot"))
|
| 59 |
+
r.raise_for_status()
|
| 60 |
+
return r.json().get("snapshot", "No snapshot available")
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return f"ERROR fetching snapshot: {e}"
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
def get_logs_once(pod_name: str, tail: int) -> str:
|
| 66 |
+
"""Call FastAPI: GET /api/logs"""
|
| 67 |
+
try:
|
| 68 |
+
params = {"pod_name": pod_name, "tail": tail}
|
| 69 |
+
r = requests.get(api("/api/logs"), params=params)
|
| 70 |
+
r.raise_for_status()
|
| 71 |
+
return r.json().get("logs", "No logs received")
|
| 72 |
+
except Exception as e:
|
| 73 |
+
return f"<span style='color:red;'>ERROR: {e}</span>"
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
+
def follow_logs(pod_name: str, tail: int):
|
| 77 |
+
"""Streaming generator for follow logs."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
while True:
|
| 79 |
+
html = get_logs_once(pod_name, tail)
|
| 80 |
+
yield html
|
| 81 |
time.sleep(2)
|
| 82 |
|
| 83 |
|
| 84 |
+
# -------------------------------------------------------------------
|
| 85 |
+
# Gradio UI
|
| 86 |
+
# -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
| 88 |
+
def build_ui():
|
| 89 |
+
with gr.Blocks(title="AgentCTL Frontend (FastAPI Backend)") as ui:
|
| 90 |
|
| 91 |
+
gr.Markdown(f"""
|
| 92 |
+
# 🚀 AgentCTL (Frontend)
|
| 93 |
+
FastAPI Backend URL in use:
|
| 94 |
|
| 95 |
+
### **🔗 {BACKEND_URL}**
|
| 96 |
+
|
| 97 |
+
_(Set AGENTCTL_BACKEND_URL environment variable to override)_
|
|
|
|
|
|
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
---
|
| 100 |
""")
|
| 101 |
|
| 102 |
+
# ----------------------------------------------------------
|
| 103 |
+
# TAB 1 — Create Resource
|
| 104 |
+
# ----------------------------------------------------------
|
| 105 |
with gr.Tab("Create Resource"):
|
| 106 |
with gr.Row():
|
| 107 |
prompt = gr.Textbox(
|
| 108 |
+
label="Describe your resource",
|
| 109 |
+
lines=5,
|
| 110 |
placeholder=(
|
| 111 |
+
"Examples:\n"
|
| 112 |
+
"- run a python job to preprocess data\n"
|
| 113 |
"- create an nginx deployment with 3 replicas\n"
|
| 114 |
"- schedule a cleanup script every 5 minutes\n"
|
| 115 |
+
)
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
with gr.Column():
|
| 119 |
namespace = gr.Textbox(
|
| 120 |
label="Namespace",
|
| 121 |
+
value="default"
|
| 122 |
)
|
| 123 |
kind = gr.Dropdown(
|
| 124 |
+
label="Kind",
|
| 125 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 126 |
+
value="Auto"
|
| 127 |
)
|
| 128 |
|
| 129 |
generate_btn = gr.Button("Generate YAML", variant="primary")
|
| 130 |
+
yaml_box = gr.Code(label="Generated YAML", language="yaml")
|
| 131 |
|
| 132 |
+
apply_btn = gr.Button("Apply YAML to Cluster", variant="primary")
|
| 133 |
+
apply_status = gr.Textbox(label="Status")
|
| 134 |
+
apply_raw = gr.Textbox(label="Raw API Response")
|
| 135 |
|
| 136 |
generate_btn.click(
|
| 137 |
+
fn=generate_yaml,
|
| 138 |
inputs=[prompt, namespace, kind],
|
| 139 |
+
outputs=yaml_box
|
| 140 |
)
|
| 141 |
+
|
| 142 |
apply_btn.click(
|
| 143 |
+
fn=apply_yaml,
|
| 144 |
inputs=[yaml_box],
|
| 145 |
+
outputs=[apply_status, apply_raw]
|
| 146 |
)
|
| 147 |
|
| 148 |
+
# ----------------------------------------------------------
|
| 149 |
+
# TAB 2 — Dashboard
|
| 150 |
+
# ----------------------------------------------------------
|
| 151 |
with gr.Tab("Cluster Dashboard"):
|
| 152 |
+
refresh_button = gr.Button("Refresh Snapshot", variant="primary")
|
| 153 |
+
snapshot_box = gr.Textbox(label="Cluster Snapshot", lines=25)
|
| 154 |
|
| 155 |
+
refresh_button.click(fn=get_snapshot, outputs=snapshot_box)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
+
# ----------------------------------------------------------
|
| 158 |
+
# TAB 3 — Pods Logs
|
| 159 |
+
# ----------------------------------------------------------
|
| 160 |
with gr.Tab("Pod Logs"):
|
| 161 |
+
pod_name = gr.Textbox(label="Pod Name")
|
| 162 |
+
tail_lines = gr.Slider(
|
| 163 |
+
label="Tail Lines",
|
| 164 |
+
minimum=10,
|
| 165 |
+
maximum=500,
|
| 166 |
+
value=100
|
| 167 |
+
)
|
| 168 |
|
| 169 |
+
get_logs_btn = gr.Button("Get Logs")
|
| 170 |
+
follow_logs_btn = gr.Button("Follow Logs (Live)", variant="primary")
|
| 171 |
+
logs_html = gr.HTML(label="Logs Terminal")
|
| 172 |
|
| 173 |
get_logs_btn.click(
|
| 174 |
+
fn=get_logs_once,
|
| 175 |
inputs=[pod_name, tail_lines],
|
| 176 |
+
outputs=logs_html
|
| 177 |
)
|
| 178 |
|
|
|
|
| 179 |
follow_logs_btn.click(
|
| 180 |
+
fn=follow_logs,
|
| 181 |
inputs=[pod_name, tail_lines],
|
| 182 |
+
outputs=logs_html
|
| 183 |
)
|
| 184 |
|
| 185 |
+
return ui
|
| 186 |
|
| 187 |
|
| 188 |
+
# Build UI
|
| 189 |
+
app = build_ui()
|
| 190 |
|
| 191 |
if __name__ == "__main__":
|
| 192 |
+
app.launch(server_name="0.0.0.0", server_port=7860)
|
|
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py
CHANGED
|
@@ -9,22 +9,24 @@ app = FastAPI(
|
|
| 9 |
description="Backend API for Kubernetes AgentCTL automation",
|
| 10 |
)
|
| 11 |
|
| 12 |
-
# CORS (UI
|
| 13 |
app.add_middleware(
|
| 14 |
CORSMiddleware,
|
| 15 |
-
allow_origins=["*"], # tighten later
|
| 16 |
allow_credentials=True,
|
| 17 |
allow_methods=["*"],
|
| 18 |
allow_headers=["*"],
|
| 19 |
)
|
| 20 |
|
| 21 |
-
# Routers
|
| 22 |
-
app.include_router(yaml_gen.router)
|
| 23 |
-
app.include_router(apply.router)
|
| 24 |
-
app.include_router(snapshot.router)
|
| 25 |
-
app.include_router(logs.router)
|
| 26 |
|
| 27 |
-
|
| 28 |
-
@app.get("/")
|
| 29 |
def root():
|
|
|
|
| 30 |
return {"status": "AgentCTL Backend Running"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
description="Backend API for Kubernetes AgentCTL automation",
|
| 10 |
)
|
| 11 |
|
| 12 |
+
# CORS (for Gradio UI, HF Space, other tools)
|
| 13 |
app.add_middleware(
|
| 14 |
CORSMiddleware,
|
| 15 |
+
allow_origins=["*"], # tighten later for prod
|
| 16 |
allow_credentials=True,
|
| 17 |
allow_methods=["*"],
|
| 18 |
allow_headers=["*"],
|
| 19 |
)
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
+
@app.get("/", tags=["Status"])
|
|
|
|
| 23 |
def root():
|
| 24 |
+
"""Simple health endpoint."""
|
| 25 |
return {"status": "AgentCTL Backend Running"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Attach routers under /api/*
|
| 29 |
+
app.include_router(yaml_gen.router, prefix="/api")
|
| 30 |
+
app.include_router(apply.router, prefix="/api")
|
| 31 |
+
app.include_router(snapshot.router, prefix="/api")
|
| 32 |
+
app.include_router(logs.router, prefix="/api")
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/apply.py
CHANGED
|
@@ -1,37 +1,88 @@
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
from pydantic import BaseModel
|
| 3 |
-
import uuid
|
|
|
|
| 4 |
|
| 5 |
from agentctl.k8s_client import K8sClient
|
| 6 |
|
| 7 |
-
router = APIRouter(
|
| 8 |
|
| 9 |
-
client = K8sClient()
|
| 10 |
|
| 11 |
class ApplyRequest(BaseModel):
|
| 12 |
yaml: str
|
| 13 |
|
|
|
|
| 14 |
class ApplyResponse(BaseModel):
|
| 15 |
success: bool
|
| 16 |
message: str
|
| 17 |
-
raw: str
|
|
|
|
| 18 |
|
| 19 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
try:
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
return yaml.safe_dump(
|
| 29 |
-
except:
|
|
|
|
| 30 |
return yaml_text
|
| 31 |
|
|
|
|
| 32 |
@router.post("/apply", response_model=ApplyResponse)
|
| 33 |
-
def
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
result = client.apply_manifest(final_yaml)
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routers/apply.py
|
| 2 |
+
|
| 3 |
from fastapi import APIRouter
|
| 4 |
from pydantic import BaseModel
|
| 5 |
+
import uuid
|
| 6 |
+
import yaml
|
| 7 |
|
| 8 |
from agentctl.k8s_client import K8sClient
|
| 9 |
|
| 10 |
+
router = APIRouter(tags=["Apply Manifest"])
|
| 11 |
|
|
|
|
| 12 |
|
| 13 |
class ApplyRequest(BaseModel):
|
| 14 |
yaml: str
|
| 15 |
|
| 16 |
+
|
| 17 |
class ApplyResponse(BaseModel):
|
| 18 |
success: bool
|
| 19 |
message: str
|
| 20 |
+
raw: str | None = None
|
| 21 |
+
|
| 22 |
|
| 23 |
+
def _inject_unique_name(yaml_text: str) -> str:
|
| 24 |
+
"""
|
| 25 |
+
Add a short UUID suffix to metadata.name for Job/CronJob/Deployment
|
| 26 |
+
to avoid 409 AlreadyExists errors when users re-apply the same YAML.
|
| 27 |
+
"""
|
| 28 |
try:
|
| 29 |
+
data = yaml.safe_load(yaml_text)
|
| 30 |
+
if not isinstance(data, dict):
|
| 31 |
+
return yaml_text
|
| 32 |
+
|
| 33 |
+
kind = data.get("kind")
|
| 34 |
+
if kind not in {"Job", "CronJob", "Deployment"}:
|
| 35 |
+
return yaml_text
|
| 36 |
+
|
| 37 |
+
meta = data.get("metadata") or {}
|
| 38 |
+
base_name = meta.get("name")
|
| 39 |
+
if not base_name:
|
| 40 |
+
return yaml_text
|
| 41 |
+
|
| 42 |
+
suffix = str(uuid.uuid4())[:5]
|
| 43 |
+
new_name = f"{base_name}-{suffix}"
|
| 44 |
+
meta["name"] = new_name
|
| 45 |
+
data["metadata"] = meta
|
| 46 |
|
| 47 |
+
# For CronJob make sure template labels are consistent
|
| 48 |
+
if kind == "CronJob":
|
| 49 |
+
tmpl = (
|
| 50 |
+
data.get("spec", {})
|
| 51 |
+
.get("jobTemplate", {})
|
| 52 |
+
.get("spec", {})
|
| 53 |
+
.get("template", {})
|
| 54 |
+
)
|
| 55 |
+
if isinstance(tmpl, dict):
|
| 56 |
+
md = tmpl.get("metadata") or {}
|
| 57 |
+
labels = md.get("labels") or {}
|
| 58 |
+
labels["app"] = new_name
|
| 59 |
+
md["labels"] = labels
|
| 60 |
+
tmpl["metadata"] = md
|
| 61 |
|
| 62 |
+
return yaml.safe_dump(data)
|
| 63 |
+
except Exception:
|
| 64 |
+
# on failure, just return original yaml
|
| 65 |
return yaml_text
|
| 66 |
|
| 67 |
+
|
| 68 |
@router.post("/apply", response_model=ApplyResponse)
|
| 69 |
+
def apply_manifest(req: ApplyRequest):
|
| 70 |
+
"""
|
| 71 |
+
Apply a YAML manifest to the Kubernetes cluster via K8sClient.
|
| 72 |
+
"""
|
| 73 |
+
# Lazy K8sClient instantiation so missing env vars don't crash import
|
| 74 |
+
client = K8sClient()
|
| 75 |
+
|
| 76 |
+
final_yaml = _inject_unique_name(req.yaml)
|
| 77 |
result = client.apply_manifest(final_yaml)
|
| 78 |
+
|
| 79 |
+
try:
|
| 80 |
+
raw_str = str(result.raw_response)
|
| 81 |
+
except Exception:
|
| 82 |
+
raw_str = None
|
| 83 |
+
|
| 84 |
+
return ApplyResponse(
|
| 85 |
+
success=result.success,
|
| 86 |
+
message=result.message,
|
| 87 |
+
raw=raw_str,
|
| 88 |
+
)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/logs.py
CHANGED
|
@@ -1,11 +1,20 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
| 2 |
from agentctl.k8s_client import K8sClient
|
| 3 |
|
| 4 |
-
router = APIRouter(
|
| 5 |
|
| 6 |
-
client = K8sClient()
|
| 7 |
|
| 8 |
@router.get("/logs")
|
| 9 |
-
def get_logs(
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routers/logs.py
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Query
|
| 4 |
+
|
| 5 |
from agentctl.k8s_client import K8sClient
|
| 6 |
|
| 7 |
+
router = APIRouter(tags=["Logs"])
|
| 8 |
|
|
|
|
| 9 |
|
| 10 |
@router.get("/logs")
|
| 11 |
+
def get_logs(
|
| 12 |
+
pod_name: str = Query(..., description="Pod name from snapshot"),
|
| 13 |
+
tail: int = Query(100, ge=1, le=1000, description="Tail lines"),
|
| 14 |
+
):
|
| 15 |
+
"""
|
| 16 |
+
Return raw logs for a given pod (no HTML, just plain text).
|
| 17 |
+
"""
|
| 18 |
+
client = K8sClient()
|
| 19 |
+
text = client.get_pod_logs(pod_name.strip(), tail_lines=tail)
|
| 20 |
+
return {"pod_name": pod_name, "tail": tail, "logs": text}
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/snapshot.py
CHANGED
|
@@ -1,19 +1,54 @@
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter
|
|
|
|
| 2 |
from agentctl.k8s_client import K8sClient
|
| 3 |
|
| 4 |
-
router = APIRouter(
|
| 5 |
|
| 6 |
-
client = K8sClient()
|
| 7 |
|
| 8 |
@router.get("/snapshot")
|
| 9 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
snap = client.snapshot()
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
-
return data
|
|
|
|
| 1 |
+
# backend/routers/snapshot.py
|
| 2 |
+
|
| 3 |
from fastapi import APIRouter
|
| 4 |
+
|
| 5 |
from agentctl.k8s_client import K8sClient
|
| 6 |
|
| 7 |
+
router = APIRouter(tags=["Cluster Snapshot"])
|
| 8 |
|
|
|
|
| 9 |
|
| 10 |
@router.get("/snapshot")
|
| 11 |
+
def get_snapshot():
|
| 12 |
+
"""
|
| 13 |
+
Return a JSON snapshot of Jobs, Pods, Deployments, CronJobs in the namespace.
|
| 14 |
+
"""
|
| 15 |
+
client = K8sClient()
|
| 16 |
snap = client.snapshot()
|
| 17 |
|
| 18 |
+
def job_to_dict(j):
|
| 19 |
+
return {
|
| 20 |
+
"name": getattr(j, "name", None),
|
| 21 |
+
"succeeded": getattr(j, "succeeded", None),
|
| 22 |
+
"failed": getattr(j, "failed", None),
|
| 23 |
+
"active": getattr(j, "active", None),
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
def pod_to_dict(p):
|
| 27 |
+
return {
|
| 28 |
+
"name": getattr(p, "name", None),
|
| 29 |
+
"phase": getattr(p, "phase", None),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def deploy_to_dict(d):
|
| 33 |
+
return {
|
| 34 |
+
"name": getattr(d, "name", None),
|
| 35 |
+
"replicas": getattr(d, "replicas", None),
|
| 36 |
+
"ready": getattr(d, "ready", None),
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
def cron_to_dict(c):
|
| 40 |
+
return {
|
| 41 |
+
"name": getattr(c, "name", None),
|
| 42 |
+
"active": getattr(c, "active", None),
|
| 43 |
+
# your schema likely calls this last_schedule or lastScheduleTime
|
| 44 |
+
"last_schedule": getattr(c, "last_schedule", None)
|
| 45 |
+
or getattr(c, "lastScheduleTime", None),
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
"namespace": getattr(snap, "namespace", None),
|
| 50 |
+
"jobs": [job_to_dict(j) for j in snap.jobs],
|
| 51 |
+
"pods": [pod_to_dict(p) for p in snap.pods],
|
| 52 |
+
"deployments": [deploy_to_dict(d) for d in snap.deployments],
|
| 53 |
+
"cronjobs": [cron_to_dict(c) for c in snap.cronjobs],
|
| 54 |
}
|
|
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/yaml_gen.py
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
from pydantic import BaseModel
|
|
|
|
| 3 |
from agentctl.agent import K8sAgent
|
| 4 |
from agentctl.config import settings
|
| 5 |
|
| 6 |
-
router = APIRouter(
|
| 7 |
|
| 8 |
agent = K8sAgent()
|
| 9 |
|
| 10 |
-
|
|
|
|
| 11 |
prompt: str
|
| 12 |
namespace: str | None = None
|
| 13 |
kind: str | None = "Auto"
|
| 14 |
|
| 15 |
-
|
|
|
|
| 16 |
yaml: str
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
ns = req.namespace or settings.k8s_namespace
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend/routers/yaml_gen.py
|
| 2 |
+
|
| 3 |
from fastapi import APIRouter
|
| 4 |
from pydantic import BaseModel
|
| 5 |
+
|
| 6 |
from agentctl.agent import K8sAgent
|
| 7 |
from agentctl.config import settings
|
| 8 |
|
| 9 |
+
router = APIRouter(tags=["YAML Generation"])
|
| 10 |
|
| 11 |
agent = K8sAgent()
|
| 12 |
|
| 13 |
+
|
| 14 |
+
class GenerateYAMLRequest(BaseModel):
|
| 15 |
prompt: str
|
| 16 |
namespace: str | None = None
|
| 17 |
kind: str | None = "Auto"
|
| 18 |
|
| 19 |
+
|
| 20 |
+
class GenerateYAMLResponse(BaseModel):
|
| 21 |
yaml: str
|
| 22 |
|
| 23 |
+
|
| 24 |
+
@router.post("/generate-yaml", response_model=GenerateYAMLResponse)
|
| 25 |
+
def generate_yaml(req: GenerateYAMLRequest):
|
| 26 |
+
"""
|
| 27 |
+
Natural language → Kubernetes YAML using your K8sAgent.
|
| 28 |
+
"""
|
| 29 |
ns = req.namespace or settings.k8s_namespace
|
| 30 |
+
kind_sel = None if (req.kind is None or req.kind == "Auto") else req.kind
|
| 31 |
+
|
| 32 |
+
# your existing agent logic returns (explanation, yaml_text)
|
| 33 |
+
_, yaml_text = agent.nl_to_resource_yaml(
|
| 34 |
+
req.prompt,
|
| 35 |
+
namespace=ns,
|
| 36 |
+
kind=kind_sel,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
return GenerateYAMLResponse(yaml=yaml_text)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/__init__.py
ADDED
|
File without changes
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/main.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
|
| 4 |
+
from backend.routers import yaml_gen, apply, snapshot, logs
|
| 5 |
+
|
| 6 |
+
app = FastAPI(
|
| 7 |
+
title="AgentCTL Backend API",
|
| 8 |
+
version="1.0.0",
|
| 9 |
+
description="Backend API for Kubernetes AgentCTL automation",
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
# CORS (UI → API)
|
| 13 |
+
app.add_middleware(
|
| 14 |
+
CORSMiddleware,
|
| 15 |
+
allow_origins=["*"], # tighten later if needed
|
| 16 |
+
allow_credentials=True,
|
| 17 |
+
allow_methods=["*"],
|
| 18 |
+
allow_headers=["*"],
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Routers
|
| 22 |
+
app.include_router(yaml_gen.router)
|
| 23 |
+
app.include_router(apply.router)
|
| 24 |
+
app.include_router(snapshot.router)
|
| 25 |
+
app.include_router(logs.router)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@app.get("/")
|
| 29 |
+
def root():
|
| 30 |
+
return {"status": "AgentCTL Backend Running"}
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/__init__.py
ADDED
|
File without changes
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/apply.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
import uuid, yaml
|
| 4 |
+
|
| 5 |
+
from agentctl.k8s_client import K8sClient
|
| 6 |
+
|
| 7 |
+
router = APIRouter(prefix="/api", tags=["Apply"])
|
| 8 |
+
|
| 9 |
+
client = K8sClient()
|
| 10 |
+
|
| 11 |
+
class ApplyRequest(BaseModel):
|
| 12 |
+
yaml: str
|
| 13 |
+
|
| 14 |
+
class ApplyResponse(BaseModel):
|
| 15 |
+
success: bool
|
| 16 |
+
message: str
|
| 17 |
+
raw: str
|
| 18 |
+
|
| 19 |
+
def inject_unique(yaml_text: str) -> str:
|
| 20 |
+
try:
|
| 21 |
+
obj = yaml.safe_load(yaml_text)
|
| 22 |
+
kind = obj.get("kind")
|
| 23 |
+
|
| 24 |
+
if kind in ["Job", "CronJob", "Deployment"]:
|
| 25 |
+
suffix = str(uuid.uuid4())[:5]
|
| 26 |
+
obj["metadata"]["name"] += f"-{suffix}"
|
| 27 |
+
|
| 28 |
+
return yaml.safe_dump(obj)
|
| 29 |
+
except:
|
| 30 |
+
return yaml_text
|
| 31 |
+
|
| 32 |
+
@router.post("/apply", response_model=ApplyResponse)
|
| 33 |
+
def apply_resource(req: ApplyRequest):
|
| 34 |
+
final_yaml = inject_unique(req.yaml)
|
| 35 |
+
result = client.apply_manifest(final_yaml)
|
| 36 |
+
raw = str(result.raw_response)
|
| 37 |
+
return ApplyResponse(success=result.success, message=result.message, raw=raw)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/logs.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from agentctl.k8s_client import K8sClient
|
| 3 |
+
|
| 4 |
+
router = APIRouter(prefix="/api", tags=["Logs"])
|
| 5 |
+
|
| 6 |
+
client = K8sClient()
|
| 7 |
+
|
| 8 |
+
@router.get("/logs")
|
| 9 |
+
def get_logs(pod_name: str, tail: int = 100):
|
| 10 |
+
logs = client.get_pod_logs(pod_name, tail)
|
| 11 |
+
return {"pod": pod_name, "tail": tail, "logs": logs}
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/snapshot.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from agentctl.k8s_client import K8sClient
|
| 3 |
+
|
| 4 |
+
router = APIRouter(prefix="/api", tags=["Snapshot"])
|
| 5 |
+
|
| 6 |
+
client = K8sClient()
|
| 7 |
+
|
| 8 |
+
@router.get("/snapshot")
|
| 9 |
+
def snapshot():
|
| 10 |
+
snap = client.snapshot()
|
| 11 |
+
|
| 12 |
+
data = {
|
| 13 |
+
"namespace": snap.namespace,
|
| 14 |
+
"jobs": [vars(j) for j in snap.jobs],
|
| 15 |
+
"pods": [vars(p) for p in snap.pods],
|
| 16 |
+
"deployments": [vars(d) for d in snap.deployments],
|
| 17 |
+
"cronjobs": [vars(c) for c in snap.cronjobs],
|
| 18 |
+
}
|
| 19 |
+
return data
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/routers/yaml_gen.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from agentctl.agent import K8sAgent
|
| 4 |
+
from agentctl.config import settings
|
| 5 |
+
|
| 6 |
+
router = APIRouter(prefix="/api", tags=["YAML Generation"])
|
| 7 |
+
|
| 8 |
+
agent = K8sAgent()
|
| 9 |
+
|
| 10 |
+
class GenerateRequest(BaseModel):
|
| 11 |
+
prompt: str
|
| 12 |
+
namespace: str | None = None
|
| 13 |
+
kind: str | None = "Auto"
|
| 14 |
+
|
| 15 |
+
class GenerateResponse(BaseModel):
|
| 16 |
+
yaml: str
|
| 17 |
+
|
| 18 |
+
@router.post("/generate-yaml", response_model=GenerateResponse)
|
| 19 |
+
def generate_yaml(req: GenerateRequest):
|
| 20 |
+
ns = req.namespace or settings.k8s_namespace
|
| 21 |
+
kind = None if req.kind == "Auto" else req.kind
|
| 22 |
+
_, yaml_text = agent.nl_to_resource_yaml(req.prompt, ns, kind)
|
| 23 |
+
return GenerateResponse(yaml=yaml_text)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/__init__.py
ADDED
|
File without changes
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/agent_service.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agentctl.agent import K8sAgent
|
| 2 |
+
from agentctl.config import settings
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class AgentService:
|
| 6 |
+
"""
|
| 7 |
+
Wrapper for the LLM → YAML agent.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
def __init__(self):
|
| 11 |
+
self.agent = K8sAgent()
|
| 12 |
+
|
| 13 |
+
def generate_yaml(self, prompt: str, namespace: str | None, kind: str | None):
|
| 14 |
+
# fallback to default namespace from config
|
| 15 |
+
ns = namespace or settings.k8s_namespace
|
| 16 |
+
kind_sel = None if (kind is None or kind == "Auto") else kind
|
| 17 |
+
|
| 18 |
+
# returns: (explanation, yaml_text)
|
| 19 |
+
_, yaml_text = self.agent.nl_to_resource_yaml(
|
| 20 |
+
prompt,
|
| 21 |
+
namespace=ns,
|
| 22 |
+
kind=kind_sel,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
return yaml_text
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# Shared singleton
|
| 29 |
+
agent_service = AgentService()
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/backend/services/k8s_service.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from agentctl.k8s_client import K8sClient
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class K8sService:
|
| 5 |
+
"""
|
| 6 |
+
Simple wrapper around your existing K8sClient.
|
| 7 |
+
This allows FastAPI routes to call Kubernetes operations
|
| 8 |
+
without importing K8sClient directly in every router.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.client = K8sClient()
|
| 13 |
+
|
| 14 |
+
# ---- Apply YAML ----
|
| 15 |
+
def apply_manifest(self, yaml_text: str):
|
| 16 |
+
return self.client.apply_manifest(yaml_text)
|
| 17 |
+
|
| 18 |
+
# ---- Snapshot ----
|
| 19 |
+
def snapshot(self):
|
| 20 |
+
return self.client.snapshot()
|
| 21 |
+
|
| 22 |
+
# ---- Logs ----
|
| 23 |
+
def get_pod_logs(self, pod_name: str, tail_lines: int = 100):
|
| 24 |
+
return self.client.get_pod_logs(pod_name, tail_lines=tail_lines)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# Shared singleton instance
|
| 28 |
+
k8s_service = K8sService()
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -81,7 +81,6 @@ def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
|
| 81 |
if not yaml_text.strip():
|
| 82 |
return "No YAML to apply.", ""
|
| 83 |
|
| 84 |
-
# Add unique suffix before applying
|
| 85 |
yaml_text = _inject_unique_name(yaml_text)
|
| 86 |
|
| 87 |
client = _get_client()
|
|
@@ -229,9 +228,7 @@ def build_app() -> gr.Blocks:
|
|
| 229 |
---
|
| 230 |
""")
|
| 231 |
|
| 232 |
-
# -----------------------------------------------------------------
|
| 233 |
# Create Resource
|
| 234 |
-
# -----------------------------------------------------------------
|
| 235 |
with gr.Tab("Create Resource"):
|
| 236 |
with gr.Row():
|
| 237 |
prompt = gr.Textbox(
|
|
@@ -273,9 +270,7 @@ def build_app() -> gr.Blocks:
|
|
| 273 |
outputs=[apply_msg, apply_debug],
|
| 274 |
)
|
| 275 |
|
| 276 |
-
# -----------------------------------------------------------------
|
| 277 |
# Dashboard
|
| 278 |
-
# -----------------------------------------------------------------
|
| 279 |
with gr.Tab("Cluster Dashboard"):
|
| 280 |
snapshot_btn = gr.Button("Refresh snapshot", variant="primary")
|
| 281 |
snapshot_box = gr.Textbox(lines=25, label="Cluster Overview")
|
|
@@ -286,9 +281,7 @@ def build_app() -> gr.Blocks:
|
|
| 286 |
outputs=snapshot_box,
|
| 287 |
)
|
| 288 |
|
| 289 |
-
# -----------------------------------------------------------------
|
| 290 |
# Logs
|
| 291 |
-
# -----------------------------------------------------------------
|
| 292 |
with gr.Tab("Pod Logs"):
|
| 293 |
pod_name = gr.Textbox(label="Pod name")
|
| 294 |
tail_lines = gr.Slider(10, 500, step=10, value=100, label="Tail lines")
|
|
@@ -304,11 +297,11 @@ def build_app() -> gr.Blocks:
|
|
| 304 |
outputs=logs_box,
|
| 305 |
)
|
| 306 |
|
|
|
|
| 307 |
follow_logs_btn.click(
|
| 308 |
follow_pod_logs,
|
| 309 |
inputs=[pod_name, tail_lines],
|
| 310 |
outputs=logs_box,
|
| 311 |
-
stream=True,
|
| 312 |
)
|
| 313 |
|
| 314 |
return demo
|
|
|
|
| 81 |
if not yaml_text.strip():
|
| 82 |
return "No YAML to apply.", ""
|
| 83 |
|
|
|
|
| 84 |
yaml_text = _inject_unique_name(yaml_text)
|
| 85 |
|
| 86 |
client = _get_client()
|
|
|
|
| 228 |
---
|
| 229 |
""")
|
| 230 |
|
|
|
|
| 231 |
# Create Resource
|
|
|
|
| 232 |
with gr.Tab("Create Resource"):
|
| 233 |
with gr.Row():
|
| 234 |
prompt = gr.Textbox(
|
|
|
|
| 270 |
outputs=[apply_msg, apply_debug],
|
| 271 |
)
|
| 272 |
|
|
|
|
| 273 |
# Dashboard
|
|
|
|
| 274 |
with gr.Tab("Cluster Dashboard"):
|
| 275 |
snapshot_btn = gr.Button("Refresh snapshot", variant="primary")
|
| 276 |
snapshot_box = gr.Textbox(lines=25, label="Cluster Overview")
|
|
|
|
| 281 |
outputs=snapshot_box,
|
| 282 |
)
|
| 283 |
|
|
|
|
| 284 |
# Logs
|
|
|
|
| 285 |
with gr.Tab("Pod Logs"):
|
| 286 |
pod_name = gr.Textbox(label="Pod name")
|
| 287 |
tail_lines = gr.Slider(10, 500, step=10, value=100, label="Tail lines")
|
|
|
|
| 297 |
outputs=logs_box,
|
| 298 |
)
|
| 299 |
|
| 300 |
+
# FIXED: removed `stream=True`
|
| 301 |
follow_logs_btn.click(
|
| 302 |
follow_pod_logs,
|
| 303 |
inputs=[pod_name, tail_lines],
|
| 304 |
outputs=logs_box,
|
|
|
|
| 305 |
)
|
| 306 |
|
| 307 |
return demo
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from typing import Tuple
|
| 5 |
|
| 6 |
import gradio as gr
|
|
@@ -10,26 +14,66 @@ from agentctl.k8s_client import K8sClient
|
|
| 10 |
from agentctl.agent import K8sAgent
|
| 11 |
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
|
|
|
|
| 15 |
|
| 16 |
def _get_client() -> K8sClient:
|
| 17 |
return K8sClient()
|
| 18 |
|
| 19 |
|
| 20 |
-
# ------------------------------------------------------------
|
| 21 |
-
#
|
| 22 |
-
# ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
| 26 |
if not prompt.strip():
|
| 27 |
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
)
|
| 33 |
return yaml_text
|
| 34 |
|
| 35 |
|
|
@@ -37,118 +81,173 @@ def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
|
| 37 |
if not yaml_text.strip():
|
| 38 |
return "No YAML to apply.", ""
|
| 39 |
|
|
|
|
|
|
|
|
|
|
| 40 |
client = _get_client()
|
| 41 |
result = client.apply_manifest(yaml_text)
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
def get_cluster_snapshot() -> str:
|
| 50 |
client = _get_client()
|
| 51 |
snap = client.snapshot()
|
| 52 |
|
| 53 |
-
|
| 54 |
|
| 55 |
-
|
| 56 |
-
lines.append("Jobs:")
|
| 57 |
if not snap.jobs:
|
| 58 |
-
|
| 59 |
else:
|
| 60 |
for j in snap.jobs:
|
| 61 |
-
|
| 62 |
f" - {j.name}: succeeded={j.succeeded}, failed={j.failed}, active={j.active}"
|
| 63 |
)
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
lines.append("Pods:")
|
| 68 |
if not snap.pods:
|
| 69 |
-
|
| 70 |
else:
|
| 71 |
for p in snap.pods:
|
| 72 |
-
|
| 73 |
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
lines.append("Deployments:")
|
| 77 |
if not snap.deployments:
|
| 78 |
-
|
| 79 |
else:
|
| 80 |
for d in snap.deployments:
|
| 81 |
-
|
| 82 |
-
f" - {d.name}: replicas={d.replicas}, ready={d.ready}"
|
| 83 |
-
)
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
lines.append("CronJobs:")
|
| 88 |
if not snap.cronjobs:
|
| 89 |
-
|
| 90 |
else:
|
| 91 |
for c in snap.cronjobs:
|
| 92 |
last = c.last_schedule or "never"
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
return "\n".join(lines)
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
def get_pod_logs_handler(pod_name: str, tail_lines: int) -> str:
|
| 101 |
-
if not pod_name.strip():
|
| 102 |
-
return "Enter a pod name from the snapshot above."
|
| 103 |
client = _get_client()
|
| 104 |
-
|
| 105 |
-
return
|
| 106 |
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
def build_app() -> gr.Blocks:
|
| 114 |
-
with gr.Blocks(
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
"""
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
# --------- Create Resource tab --------- #
|
| 135 |
with gr.Tab("Create Resource"):
|
| 136 |
with gr.Row():
|
| 137 |
prompt = gr.Textbox(
|
| 138 |
-
label="Describe the resource
|
| 139 |
placeholder=(
|
| 140 |
-
"
|
| 141 |
-
"- run a python job to preprocess data\n"
|
| 142 |
"- create an nginx deployment with 3 replicas\n"
|
| 143 |
-
"- schedule a
|
| 144 |
),
|
| 145 |
lines=5,
|
| 146 |
)
|
|
|
|
| 147 |
with gr.Column():
|
| 148 |
namespace = gr.Textbox(
|
| 149 |
-
label="
|
| 150 |
value=settings.k8s_namespace,
|
| 151 |
-
lines=1,
|
| 152 |
)
|
| 153 |
kind = gr.Dropdown(
|
| 154 |
label="Resource kind",
|
|
@@ -156,50 +255,60 @@ def build_app() -> gr.Blocks:
|
|
| 156 |
value="Auto",
|
| 157 |
)
|
| 158 |
|
| 159 |
-
generate_btn = gr.Button("Generate YAML")
|
| 160 |
-
yaml_box = gr.Code(
|
| 161 |
|
| 162 |
-
apply_btn = gr.Button("Apply to
|
| 163 |
-
apply_msg = gr.Textbox(label="Status"
|
| 164 |
-
|
| 165 |
|
| 166 |
generate_btn.click(
|
| 167 |
-
|
| 168 |
inputs=[prompt, namespace, kind],
|
| 169 |
-
outputs=
|
| 170 |
)
|
| 171 |
-
|
| 172 |
apply_btn.click(
|
| 173 |
-
|
| 174 |
inputs=[yaml_box],
|
| 175 |
-
outputs=[apply_msg,
|
| 176 |
)
|
| 177 |
|
| 178 |
-
# ---------
|
|
|
|
|
|
|
| 179 |
with gr.Tab("Cluster Dashboard"):
|
| 180 |
-
gr.
|
| 181 |
-
|
| 182 |
-
snapshot_box = gr.Textbox(label="Cluster Overview", lines=25)
|
| 183 |
|
| 184 |
-
snapshot_btn.click(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
# ---------
|
|
|
|
|
|
|
| 187 |
with gr.Tab("Pod Logs"):
|
| 188 |
pod_name = gr.Textbox(label="Pod name")
|
| 189 |
-
tail_lines = gr.Slider(
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
)
|
| 196 |
-
logs_btn = gr.Button("Get logs")
|
| 197 |
-
logs_box = gr.Textbox(label="Logs", lines=25)
|
| 198 |
|
| 199 |
-
|
| 200 |
-
|
| 201 |
inputs=[pod_name, tail_lines],
|
| 202 |
-
outputs=
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
return demo
|
|
@@ -208,4 +317,5 @@ def build_app() -> gr.Blocks:
|
|
| 208 |
app = build_app()
|
| 209 |
|
| 210 |
if __name__ == "__main__":
|
| 211 |
-
|
|
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
import uuid
|
| 7 |
+
import yaml
|
| 8 |
from typing import Tuple
|
| 9 |
|
| 10 |
import gradio as gr
|
|
|
|
| 14 |
from agentctl.agent import K8sAgent
|
| 15 |
|
| 16 |
|
| 17 |
+
# ---------------------------------------------------------------------
|
| 18 |
+
# Core - Agent + Client
|
| 19 |
+
# ---------------------------------------------------------------------
|
| 20 |
|
| 21 |
+
agent = K8sAgent()
|
| 22 |
|
| 23 |
def _get_client() -> K8sClient:
|
| 24 |
return K8sClient()
|
| 25 |
|
| 26 |
|
| 27 |
+
# ---------------------------------------------------------------------
|
| 28 |
+
# Unique Name Injection
|
| 29 |
+
# ---------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
def _inject_unique_name(yaml_text: str) -> str:
|
| 32 |
+
"""
|
| 33 |
+
Add unique suffix to metadata.name for Job, Deployment, CronJob.
|
| 34 |
+
Prevents 409 AlreadyExists errors.
|
| 35 |
+
"""
|
| 36 |
+
try:
|
| 37 |
+
data = yaml.safe_load(yaml_text)
|
| 38 |
+
if not isinstance(data, dict):
|
| 39 |
+
return yaml_text
|
| 40 |
+
|
| 41 |
+
kind = data.get("kind")
|
| 42 |
+
if kind not in {"Job", "CronJob", "Deployment"}:
|
| 43 |
+
return yaml_text
|
| 44 |
+
|
| 45 |
+
base = data["metadata"]["name"]
|
| 46 |
+
suffix = str(uuid.uuid4())[:5]
|
| 47 |
+
new_name = f"{base}-{suffix}"
|
| 48 |
+
data["metadata"]["name"] = new_name
|
| 49 |
+
|
| 50 |
+
# Patch CronJob template labels
|
| 51 |
+
if kind == "CronJob":
|
| 52 |
+
jt = data["spec"]["jobTemplate"]["spec"]["template"]
|
| 53 |
+
if "metadata" in jt:
|
| 54 |
+
if "labels" in jt["metadata"]:
|
| 55 |
+
jt["metadata"]["labels"]["app"] = new_name
|
| 56 |
+
else:
|
| 57 |
+
jt["metadata"]["labels"] = {"app": new_name}
|
| 58 |
+
|
| 59 |
+
return yaml.safe_dump(data)
|
| 60 |
+
|
| 61 |
+
except Exception:
|
| 62 |
+
return yaml_text
|
| 63 |
|
| 64 |
|
| 65 |
+
# ---------------------------------------------------------------------
|
| 66 |
+
# YAML Generation + Apply
|
| 67 |
+
# ---------------------------------------------------------------------
|
| 68 |
+
|
| 69 |
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
| 70 |
if not prompt.strip():
|
| 71 |
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
| 72 |
+
|
| 73 |
+
ns = namespace.strip() or settings.k8s_namespace
|
| 74 |
+
kind_sel = None if kind == "Auto" else kind
|
| 75 |
+
|
| 76 |
+
_, yaml_text = agent.nl_to_resource_yaml(prompt, namespace=ns, kind=kind_sel)
|
| 77 |
return yaml_text
|
| 78 |
|
| 79 |
|
|
|
|
| 81 |
if not yaml_text.strip():
|
| 82 |
return "No YAML to apply.", ""
|
| 83 |
|
| 84 |
+
# Add unique suffix before applying
|
| 85 |
+
yaml_text = _inject_unique_name(yaml_text)
|
| 86 |
+
|
| 87 |
client = _get_client()
|
| 88 |
result = client.apply_manifest(yaml_text)
|
| 89 |
|
| 90 |
+
try:
|
| 91 |
+
raw = str(result.raw_response)
|
| 92 |
+
except:
|
| 93 |
+
raw = "<unserialisable>"
|
| 94 |
+
|
| 95 |
+
prefix = "✅" if result.success else "❌"
|
| 96 |
+
return f"{prefix} {result.message}", raw
|
| 97 |
|
| 98 |
|
| 99 |
+
# ---------------------------------------------------------------------
|
| 100 |
+
# Cluster Snapshot
|
| 101 |
+
# ---------------------------------------------------------------------
|
| 102 |
+
|
| 103 |
def get_cluster_snapshot() -> str:
|
| 104 |
client = _get_client()
|
| 105 |
snap = client.snapshot()
|
| 106 |
|
| 107 |
+
out = [f"Namespace: {snap.namespace}", ""]
|
| 108 |
|
| 109 |
+
out.append("Jobs:")
|
|
|
|
| 110 |
if not snap.jobs:
|
| 111 |
+
out.append(" (no jobs)")
|
| 112 |
else:
|
| 113 |
for j in snap.jobs:
|
| 114 |
+
out.append(
|
| 115 |
f" - {j.name}: succeeded={j.succeeded}, failed={j.failed}, active={j.active}"
|
| 116 |
)
|
| 117 |
|
| 118 |
+
out.append("")
|
| 119 |
+
out.append("Pods:")
|
|
|
|
| 120 |
if not snap.pods:
|
| 121 |
+
out.append(" (no pods)")
|
| 122 |
else:
|
| 123 |
for p in snap.pods:
|
| 124 |
+
out.append(f" - {p.name}: phase={p.phase}")
|
| 125 |
|
| 126 |
+
out.append("")
|
| 127 |
+
out.append("Deployments:")
|
|
|
|
| 128 |
if not snap.deployments:
|
| 129 |
+
out.append(" (no deployments)")
|
| 130 |
else:
|
| 131 |
for d in snap.deployments:
|
| 132 |
+
out.append(f" - {d.name}: replicas={d.replicas}, ready={d.ready}")
|
|
|
|
|
|
|
| 133 |
|
| 134 |
+
out.append("")
|
| 135 |
+
out.append("CronJobs:")
|
|
|
|
| 136 |
if not snap.cronjobs:
|
| 137 |
+
out.append(" (no cronjobs)")
|
| 138 |
else:
|
| 139 |
for c in snap.cronjobs:
|
| 140 |
last = c.last_schedule or "never"
|
| 141 |
+
out.append(f" - {c.name}: active={c.active}, lastScheduleTime={last}")
|
| 142 |
+
|
| 143 |
+
return "\n".join(out)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# ---------------------------------------------------------------------
|
| 147 |
+
# Logs System
|
| 148 |
+
# ---------------------------------------------------------------------
|
| 149 |
+
|
| 150 |
+
def _colorize_logs(text: str) -> str:
|
| 151 |
+
if not text:
|
| 152 |
+
return "<span style='color:#888;'>No logs</span>"
|
| 153 |
+
|
| 154 |
+
html_lines = []
|
| 155 |
+
for line in text.splitlines():
|
| 156 |
+
if re.search(r"(error|failed|exception)", line, re.IGNORECASE):
|
| 157 |
+
color = "#ff4b4b"
|
| 158 |
+
elif re.search(r"warn", line, re.IGNORECASE):
|
| 159 |
+
color = "#f7c843"
|
| 160 |
+
elif re.search(r"(info|started|running|completed)", line, re.IGNORECASE):
|
| 161 |
+
color = "#5ad55a"
|
| 162 |
+
else:
|
| 163 |
+
color = "#d0d0d0"
|
| 164 |
+
|
| 165 |
+
safe = line.replace("<", "<").replace(">", ">")
|
| 166 |
+
html_lines.append(f"<span style='color:{color};'>{safe}</span>")
|
| 167 |
+
|
| 168 |
+
return "<br>".join(html_lines)
|
| 169 |
|
|
|
|
| 170 |
|
| 171 |
+
def get_pod_logs_once(pod_name: str, tail_lines: int) -> str:
|
| 172 |
+
pod_name = pod_name.strip()
|
| 173 |
+
if not pod_name:
|
| 174 |
+
return "<span style='color:#ff4b4b;'>Enter a pod name.</span>"
|
| 175 |
|
|
|
|
|
|
|
|
|
|
| 176 |
client = _get_client()
|
| 177 |
+
raw = client.get_pod_logs(pod_name, tail_lines)
|
| 178 |
+
return _colorize_logs(raw)
|
| 179 |
|
| 180 |
|
| 181 |
+
def follow_pod_logs(pod_name: str, tail_lines: int):
|
| 182 |
+
pod_name = pod_name.strip()
|
| 183 |
+
if not pod_name:
|
| 184 |
+
yield "<span style='color:#ff4b4b;'>Enter a pod name.</span>"
|
| 185 |
+
return
|
| 186 |
|
| 187 |
+
client = _get_client()
|
| 188 |
+
while True:
|
| 189 |
+
raw = client.get_pod_logs(pod_name, tail_lines)
|
| 190 |
+
yield _colorize_logs(raw)
|
| 191 |
+
time.sleep(2)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
# ---------------------------------------------------------------------
|
| 195 |
+
# Logs CSS
|
| 196 |
+
# ---------------------------------------------------------------------
|
| 197 |
+
|
| 198 |
+
TERMINAL_CSS = """
|
| 199 |
+
#logs_terminal {
|
| 200 |
+
background-color: #111;
|
| 201 |
+
color: #ddd;
|
| 202 |
+
font-family: monospace;
|
| 203 |
+
padding: 14px;
|
| 204 |
+
border-radius: 8px;
|
| 205 |
+
border: 1px solid #444;
|
| 206 |
+
height: 420px;
|
| 207 |
+
overflow-y: scroll;
|
| 208 |
+
white-space: pre-wrap;
|
| 209 |
+
}
|
| 210 |
+
"""
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# ---------------------------------------------------------------------
|
| 214 |
+
# UI
|
| 215 |
+
# ---------------------------------------------------------------------
|
| 216 |
|
| 217 |
def build_app() -> gr.Blocks:
|
| 218 |
+
with gr.Blocks(
|
| 219 |
+
title="AgentCTL: LLM-Driven Kubernetes Automation",
|
| 220 |
+
css=TERMINAL_CSS,
|
| 221 |
+
) as demo:
|
| 222 |
+
|
| 223 |
+
gr.Markdown("""
|
| 224 |
+
### 🔧 Environment
|
| 225 |
+
- `K8S_API_BASE_URL`
|
| 226 |
+
- `K8S_NAMESPACE`
|
| 227 |
+
- `K8S_VERIFY_SSL`
|
| 228 |
+
- `OPENAI_API_KEY` (optional)
|
| 229 |
+
---
|
| 230 |
+
""")
|
| 231 |
+
|
| 232 |
+
# -----------------------------------------------------------------
|
| 233 |
+
# Create Resource
|
| 234 |
+
# -----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
with gr.Tab("Create Resource"):
|
| 236 |
with gr.Row():
|
| 237 |
prompt = gr.Textbox(
|
| 238 |
+
label="Describe the resource",
|
| 239 |
placeholder=(
|
| 240 |
+
"- run a python job\n"
|
|
|
|
| 241 |
"- create an nginx deployment with 3 replicas\n"
|
| 242 |
+
"- schedule a cleanup script every 5 minutes\n"
|
| 243 |
),
|
| 244 |
lines=5,
|
| 245 |
)
|
| 246 |
+
|
| 247 |
with gr.Column():
|
| 248 |
namespace = gr.Textbox(
|
| 249 |
+
label="Namespace",
|
| 250 |
value=settings.k8s_namespace,
|
|
|
|
| 251 |
)
|
| 252 |
kind = gr.Dropdown(
|
| 253 |
label="Resource kind",
|
|
|
|
| 255 |
value="Auto",
|
| 256 |
)
|
| 257 |
|
| 258 |
+
generate_btn = gr.Button("Generate YAML", variant="primary")
|
| 259 |
+
yaml_box = gr.Code(language="yaml", lines=22, label="Generated YAML")
|
| 260 |
|
| 261 |
+
apply_btn = gr.Button("Apply to cluster")
|
| 262 |
+
apply_msg = gr.Textbox(label="Status")
|
| 263 |
+
apply_debug = gr.Textbox(label="Raw API Response")
|
| 264 |
|
| 265 |
generate_btn.click(
|
| 266 |
+
generate_yaml_from_prompt,
|
| 267 |
inputs=[prompt, namespace, kind],
|
| 268 |
+
outputs=yaml_box,
|
| 269 |
)
|
|
|
|
| 270 |
apply_btn.click(
|
| 271 |
+
apply_yaml_to_cluster,
|
| 272 |
inputs=[yaml_box],
|
| 273 |
+
outputs=[apply_msg, apply_debug],
|
| 274 |
)
|
| 275 |
|
| 276 |
+
# -----------------------------------------------------------------
|
| 277 |
+
# Dashboard
|
| 278 |
+
# -----------------------------------------------------------------
|
| 279 |
with gr.Tab("Cluster Dashboard"):
|
| 280 |
+
snapshot_btn = gr.Button("Refresh snapshot", variant="primary")
|
| 281 |
+
snapshot_box = gr.Textbox(lines=25, label="Cluster Overview")
|
|
|
|
| 282 |
|
| 283 |
+
snapshot_btn.click(
|
| 284 |
+
get_cluster_snapshot,
|
| 285 |
+
inputs=[],
|
| 286 |
+
outputs=snapshot_box,
|
| 287 |
+
)
|
| 288 |
|
| 289 |
+
# -----------------------------------------------------------------
|
| 290 |
+
# Logs
|
| 291 |
+
# -----------------------------------------------------------------
|
| 292 |
with gr.Tab("Pod Logs"):
|
| 293 |
pod_name = gr.Textbox(label="Pod name")
|
| 294 |
+
tail_lines = gr.Slider(10, 500, step=10, value=100, label="Tail lines")
|
| 295 |
+
|
| 296 |
+
get_logs_btn = gr.Button("Get logs once")
|
| 297 |
+
follow_logs_btn = gr.Button("Follow logs (live)", variant="primary")
|
| 298 |
+
|
| 299 |
+
logs_box = gr.HTML(elem_id="logs_terminal", label="Logs")
|
| 300 |
+
|
| 301 |
+
get_logs_btn.click(
|
| 302 |
+
get_pod_logs_once,
|
| 303 |
+
inputs=[pod_name, tail_lines],
|
| 304 |
+
outputs=logs_box,
|
| 305 |
)
|
|
|
|
|
|
|
| 306 |
|
| 307 |
+
follow_logs_btn.click(
|
| 308 |
+
follow_pod_logs,
|
| 309 |
inputs=[pod_name, tail_lines],
|
| 310 |
+
outputs=logs_box,
|
| 311 |
+
stream=True,
|
| 312 |
)
|
| 313 |
|
| 314 |
return demo
|
|
|
|
| 317 |
app = build_app()
|
| 318 |
|
| 319 |
if __name__ == "__main__":
|
| 320 |
+
port = int(os.getenv("PORT", "7860"))
|
| 321 |
+
app.launch(server_name="0.0.0.0", server_port=port)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/.github/workflows/sync-to-hf.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync GitHub (master) → Hugging Face Space (main)
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- master
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
sync:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout repository
|
| 14 |
+
uses: actions/checkout@v3
|
| 15 |
+
|
| 16 |
+
- name: Sync files to Hugging Face Space
|
| 17 |
+
env:
|
| 18 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 19 |
+
run: |
|
| 20 |
+
echo "🔐 Configuring Git"
|
| 21 |
+
git config --global user.email "actions@github.com"
|
| 22 |
+
git config --global user.name "GitHub Actions"
|
| 23 |
+
|
| 24 |
+
echo "📥 Cloning Hugging Face Space repo"
|
| 25 |
+
git clone https://codeninja3d:${HF_TOKEN}@huggingface.co/spaces/codeninja3d/agentctl hf-space
|
| 26 |
+
|
| 27 |
+
echo "📤 Syncing GitHub files → HF Space (rsync)"
|
| 28 |
+
rsync -av --delete --exclude=".git" ./ hf-space/ || true
|
| 29 |
+
|
| 30 |
+
cd hf-space
|
| 31 |
+
|
| 32 |
+
echo "📌 Committing changes"
|
| 33 |
+
git add .
|
| 34 |
+
git commit -m "Sync from GitHub on master ($(date))" || echo "No changes"
|
| 35 |
+
|
| 36 |
+
echo "⬆️ Pushing to Hugging Face Space (main branch)"
|
| 37 |
+
git push https://codeninja3d:${HF_TOKEN}@huggingface.co/spaces/codeninja3d/agentctl HEAD:main
|
| 38 |
+
|
| 39 |
+
echo "🎉 Sync complete!"
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/README.md
CHANGED
|
@@ -1,16 +1,225 @@
|
|
| 1 |
---
|
| 2 |
-
title: AgentCTL
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
|
| 13 |
-
AgentCTL
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AgentCTL – Kubernetes Agent UI
|
| 3 |
+
emoji: 🧠
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.42.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
|
| 13 |
+
# AgentCTL — LLM-Driven Kubernetes Automation
|
| 14 |
+
### Natural Language → Kubernetes Jobs, Deployments & CronJobs
|
| 15 |
+
Built for real clusters via Minikube + kubectl proxy + ngrok or Cloudflare Tunnel.
|
| 16 |
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 🚀 Overview
|
| 20 |
+
|
| 21 |
+
**AgentCTL** is an agentic workflow system that converts **natural language instructions** into **Kubernetes manifests** (Jobs, Deployments, CronJobs) and applies them to a **real Kubernetes cluster**.
|
| 22 |
+
|
| 23 |
+
It works by exposing your cluster via:
|
| 24 |
+
|
| 25 |
+
- `kubectl proxy`
|
| 26 |
+
- a public tunnel (`ngrok` or `cloudflared`)
|
| 27 |
+
- a Gradio web UI
|
| 28 |
+
- a lightweight NL → YAML agent (heuristic + optional OpenAI LLM)
|
| 29 |
+
|
| 30 |
+
---
|
| 31 |
+
|
| 32 |
+
## ✨ Features
|
| 33 |
+
|
| 34 |
+
- Natural language → Kubernetes YAML
|
| 35 |
+
- Supports:
|
| 36 |
+
- Jobs
|
| 37 |
+
- Deployments
|
| 38 |
+
- CronJobs
|
| 39 |
+
- Optional OpenAI LLM YAML refinement
|
| 40 |
+
- Cluster dashboard (Jobs, Pods, Deployments, CronJobs)
|
| 41 |
+
- Pod logs viewer
|
| 42 |
+
- Pure HTTP client — no kubeconfig required
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 🧱 Architecture
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
Gradio UI → AgentCTL (NL→YAML) → K8sClient (HTTP) → Kubernetes API
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## 📦 Installation
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
pip install -r requirements.txt
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
or
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
conda create -n agentctl python=3.10 -y
|
| 64 |
+
conda activate agentctl
|
| 65 |
+
pip install -r requirements.txt
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
---
|
| 69 |
+
|
| 70 |
+
## 🧪 Minikube Setup
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
minikube start --driver=docker
|
| 74 |
+
kubectl get pods -A
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Expose API publicly:
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
kubectl proxy --port=8001 --address=0.0.0.0 --accept-hosts='.*' --accept-paths='.*'
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
Tunnel:
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
ngrok http 8001
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 🌐 Environment Variables
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
export K8S_API_BASE_URL="https://xxxx.ngrok-free.dev"
|
| 95 |
+
export K8S_NAMESPACE="default"
|
| 96 |
+
export K8S_VERIFY_SSL=false
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Enable LLM YAML:
|
| 100 |
+
|
| 101 |
+
```bash
|
| 102 |
+
export AGENTCTL_USE_LLM=true
|
| 103 |
+
export OPENAI_API_KEY="sk-..."
|
| 104 |
+
export OPENAI_MODEL="gpt-4o-mini"
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## ▶️ Run the App
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
python app.py
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
Open:
|
| 116 |
+
|
| 117 |
+
```
|
| 118 |
+
http://localhost:7860
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## 📝 Usage Examples
|
| 124 |
+
|
| 125 |
+
### 🟦 Job
|
| 126 |
+
```
|
| 127 |
+
run a python job to preprocess data
|
| 128 |
+
```
|
| 129 |
+
|
| 130 |
+
### 🟧 Deployment
|
| 131 |
+
```
|
| 132 |
+
Prompt
|
| 133 |
+
create an nginx deployment with 3 replicas
|
| 134 |
+
|
| 135 |
+
Example
|
| 136 |
+
**Generated YAML:**
|
| 137 |
+
|
| 138 |
+
```yaml
|
| 139 |
+
apiVersion: apps/v1
|
| 140 |
+
kind: Deployment
|
| 141 |
+
metadata:
|
| 142 |
+
name: nginx-deployment
|
| 143 |
+
namespace: default
|
| 144 |
+
labels:
|
| 145 |
+
app: agentctl-deployment
|
| 146 |
+
spec:
|
| 147 |
+
replicas: 3
|
| 148 |
+
selector:
|
| 149 |
+
matchLabels:
|
| 150 |
+
app: agentctl-deployment
|
| 151 |
+
template:
|
| 152 |
+
metadata:
|
| 153 |
+
labels:
|
| 154 |
+
app: agentctl-deployment
|
| 155 |
+
spec:
|
| 156 |
+
containers:
|
| 157 |
+
- name: main
|
| 158 |
+
image: nginx:1.27-alpine
|
| 159 |
+
ports:
|
| 160 |
+
- containerPort: 80
|
| 161 |
+
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
### 🟩 CronJob
|
| 165 |
+
```
|
| 166 |
+
Prompt
|
| 167 |
+
schedule a python cleanup script every 5 minutes
|
| 168 |
+
|
| 169 |
+
example
|
| 170 |
+
apiVersion: batch/v1
|
| 171 |
+
kind: CronJob
|
| 172 |
+
metadata:
|
| 173 |
+
name: agentctl-cronjob
|
| 174 |
+
namespace: default
|
| 175 |
+
spec:
|
| 176 |
+
schedule: "*/5 * * * *"
|
| 177 |
+
jobTemplate:
|
| 178 |
+
spec:
|
| 179 |
+
template:
|
| 180 |
+
metadata:
|
| 181 |
+
labels:
|
| 182 |
+
app: agentctl-cronjob
|
| 183 |
+
spec:
|
| 184 |
+
restartPolicy: Never
|
| 185 |
+
containers:
|
| 186 |
+
- name: main
|
| 187 |
+
image: python:3.11-slim
|
| 188 |
+
command:
|
| 189 |
+
- /bin/sh
|
| 190 |
+
- -c
|
| 191 |
+
- python cleanup.py
|
| 192 |
+
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
---
|
| 196 |
+
|
| 197 |
+
## 🚀 Deploy to Hugging Face Spaces
|
| 198 |
+
|
| 199 |
+
1. Create a Space (Gradio)
|
| 200 |
+
2. Upload project files
|
| 201 |
+
3. Set environment variables
|
| 202 |
+
4. Run the UI remotely controlling your real cluster
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## 🔐 Security Notes
|
| 207 |
+
|
| 208 |
+
- ngrok + kubectl proxy is for demos only
|
| 209 |
+
- For real environments use:
|
| 210 |
+
- Cloudflare Tunnel
|
| 211 |
+
- RBAC service accounts
|
| 212 |
+
- Namespaced permissions
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
## ❤️ Credits
|
| 217 |
+
|
| 218 |
+
Built by **Murali Chandran (codeninja3d)**
|
| 219 |
+
AI Agents • Kubernetes • MLOps
|
| 220 |
+
|
| 221 |
+
---
|
| 222 |
+
|
| 223 |
+
## 📄 License
|
| 224 |
+
|
| 225 |
+
MIT License
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
| 4 |
-
import re
|
| 5 |
-
import time
|
| 6 |
from typing import Tuple
|
| 7 |
|
| 8 |
import gradio as gr
|
|
@@ -12,65 +10,43 @@ from agentctl.k8s_client import K8sClient
|
|
| 12 |
from agentctl.agent import K8sAgent
|
| 13 |
|
| 14 |
|
| 15 |
-
# ---------------------------------------------------------------------
|
| 16 |
-
# Core objects
|
| 17 |
-
# ---------------------------------------------------------------------
|
| 18 |
-
|
| 19 |
agent = K8sAgent()
|
| 20 |
|
| 21 |
|
| 22 |
def _get_client() -> K8sClient:
|
| 23 |
-
"""Create a Kubernetes HTTP client using env-based config."""
|
| 24 |
return K8sClient()
|
| 25 |
|
| 26 |
|
| 27 |
-
# ------------------------------------------------------------
|
| 28 |
-
#
|
| 29 |
-
# ------------------------------------------------------------
|
| 30 |
|
| 31 |
|
| 32 |
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
| 33 |
-
"""Convert natural language + kind selection into YAML."""
|
| 34 |
if not prompt.strip():
|
| 35 |
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
| 36 |
-
|
| 37 |
-
ns = namespace.strip() or settings.k8s_namespace
|
| 38 |
-
kind_sel = None if kind == "Auto" else kind
|
| 39 |
-
|
| 40 |
_, yaml_text = agent.nl_to_resource_yaml(
|
| 41 |
prompt,
|
| 42 |
-
namespace=
|
| 43 |
-
kind=
|
| 44 |
)
|
| 45 |
return yaml_text
|
| 46 |
|
| 47 |
|
| 48 |
def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
| 49 |
-
"""Apply a YAML manifest to the cluster via K8s API."""
|
| 50 |
if not yaml_text.strip():
|
| 51 |
return "No YAML to apply.", ""
|
| 52 |
|
| 53 |
client = _get_client()
|
| 54 |
result = client.apply_manifest(yaml_text)
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
except Exception:
|
| 61 |
-
raw_str = "<unserialisable response>"
|
| 62 |
-
|
| 63 |
-
prefix = "✅" if result.success else "❌"
|
| 64 |
-
return f"{prefix} {result.message}", raw_str
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# ---------------------------------------------------------------------
|
| 68 |
-
# Dashboard snapshot
|
| 69 |
-
# ---------------------------------------------------------------------
|
| 70 |
|
| 71 |
|
| 72 |
def get_cluster_snapshot() -> str:
|
| 73 |
-
"""Return a human-readable snapshot of Jobs, Pods, Deployments, CronJobs."""
|
| 74 |
client = _get_client()
|
| 75 |
snap = client.snapshot()
|
| 76 |
|
|
@@ -83,8 +59,7 @@ def get_cluster_snapshot() -> str:
|
|
| 83 |
else:
|
| 84 |
for j in snap.jobs:
|
| 85 |
lines.append(
|
| 86 |
-
f" - {j.name}: succeeded={j.succeeded}, "
|
| 87 |
-
f"failed={j.failed}, active={j.active}"
|
| 88 |
)
|
| 89 |
|
| 90 |
# Pods
|
|
@@ -122,109 +97,41 @@ def get_cluster_snapshot() -> str:
|
|
| 122 |
return "\n".join(lines)
|
| 123 |
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
|
| 130 |
-
|
| 131 |
-
"""Internal: raw log text from Kubernetes."""
|
| 132 |
-
client = _get_client()
|
| 133 |
-
return client.get_pod_logs(pod_name.strip(), tail_lines=tail_lines)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def _colorize_logs(text: str) -> str:
|
| 137 |
-
"""Convert log text into HTML with basic severity colouring."""
|
| 138 |
-
if not text:
|
| 139 |
-
return "<span style='color:#888;'>No logs</span>"
|
| 140 |
-
|
| 141 |
-
lines = text.splitlines()
|
| 142 |
-
out: list[str] = []
|
| 143 |
-
|
| 144 |
-
for line in lines:
|
| 145 |
-
if re.search(r"(error|failed|exception)", line, re.IGNORECASE):
|
| 146 |
-
colour = "#ff4b4b" # red
|
| 147 |
-
elif re.search(r"(warn)", line, re.IGNORECASE):
|
| 148 |
-
colour = "#f7c843" # yellow
|
| 149 |
-
elif re.search(r"(info|started|running|completed)", line, re.IGNORECASE):
|
| 150 |
-
colour = "#5ad55a" # green
|
| 151 |
-
else:
|
| 152 |
-
colour = "#d0d0d0" # grey
|
| 153 |
-
|
| 154 |
-
safe = line.replace("<", "<").replace(">", ">")
|
| 155 |
-
out.append(f"<span style='color:{colour};'>{safe}</span>")
|
| 156 |
-
|
| 157 |
-
return "<br>".join(out)
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def get_pod_logs_once(pod_name: str, tail_lines: int) -> str:
|
| 161 |
-
"""One-shot logs fetch, used for the 'Get logs' button."""
|
| 162 |
-
pod_name = pod_name.strip()
|
| 163 |
-
if not pod_name:
|
| 164 |
-
return "<span style='color:#ff4b4b;'>Enter a pod name from the dashboard.</span>"
|
| 165 |
-
|
| 166 |
-
raw = _fetch_pod_logs_raw(pod_name, tail_lines)
|
| 167 |
-
return _colorize_logs(raw)
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def follow_pod_logs(pod_name: str, tail_lines: int):
|
| 171 |
-
"""Live streaming logs using Gradio generator output."""
|
| 172 |
-
pod_name = pod_name.strip()
|
| 173 |
-
if not pod_name:
|
| 174 |
-
yield "<span style='color:#ff4b4b;'>Enter a pod name from the dashboard.</span>"
|
| 175 |
-
return
|
| 176 |
-
|
| 177 |
-
while True:
|
| 178 |
-
raw = _fetch_pod_logs_raw(pod_name, tail_lines)
|
| 179 |
-
html = _colorize_logs(raw)
|
| 180 |
-
yield html
|
| 181 |
-
time.sleep(2)
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# CSS for the logs "terminal"
|
| 185 |
-
TERMINAL_CSS = """
|
| 186 |
-
#logs_terminal {
|
| 187 |
-
background-color: #111111 !important;
|
| 188 |
-
color: #d0d0d0 !important;
|
| 189 |
-
font-family: monospace !important;
|
| 190 |
-
padding: 14px;
|
| 191 |
-
border-radius: 8px;
|
| 192 |
-
border: 1px solid #444444;
|
| 193 |
-
height: 420px;
|
| 194 |
-
overflow-y: scroll;
|
| 195 |
-
white-space: pre-wrap;
|
| 196 |
-
}
|
| 197 |
-
"""
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
# ---------------------------------------------------------------------
|
| 201 |
# Gradio UI
|
| 202 |
-
# ------------------------------------------------------------
|
| 203 |
|
| 204 |
|
| 205 |
def build_app() -> gr.Blocks:
|
| 206 |
-
with gr.Blocks(
|
| 207 |
-
title="AgentCTL: LLM-Driven Kubernetes Automation",
|
| 208 |
-
css=TERMINAL_CSS,
|
| 209 |
-
) as demo:
|
| 210 |
-
# Top environment hint (like your screenshot)
|
| 211 |
gr.Markdown(
|
| 212 |
"""
|
| 213 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
-
|
| 216 |
-
- `K8S_NAMESPACE` → **default namespace** (e.g. `default`)
|
| 217 |
-
- `K8S_VERIFY_SSL` → **true/false** (set `false` for self-signed ngrok endpoints)
|
| 218 |
-
- `AGENTCTL_USE_LLM` → **true/false** (optional; uses OpenAI if enabled in `agent.py`)
|
| 219 |
-
- `OPENAI_API_KEY` → your key (if LLM mode enabled)
|
| 220 |
|
| 221 |
-
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
)
|
| 224 |
|
| 225 |
-
#
|
| 226 |
-
# Create Resource tab
|
| 227 |
-
# ============================================================
|
| 228 |
with gr.Tab("Create Resource"):
|
| 229 |
with gr.Row():
|
| 230 |
prompt = gr.Textbox(
|
|
@@ -237,7 +144,6 @@ def build_app() -> gr.Blocks:
|
|
| 237 |
),
|
| 238 |
lines=5,
|
| 239 |
)
|
| 240 |
-
|
| 241 |
with gr.Column():
|
| 242 |
namespace = gr.Textbox(
|
| 243 |
label="Kubernetes namespace",
|
|
@@ -249,25 +155,18 @@ def build_app() -> gr.Blocks:
|
|
| 249 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 250 |
value="Auto",
|
| 251 |
)
|
| 252 |
-
gr.Markdown(
|
| 253 |
-
"Tip: leave **Auto** to let the agent infer Job vs Deployment vs CronJob."
|
| 254 |
-
)
|
| 255 |
|
| 256 |
-
generate_btn = gr.Button("Generate YAML"
|
| 257 |
-
yaml_box = gr.Code(
|
| 258 |
-
label="Generated manifest (YAML)",
|
| 259 |
-
language="yaml",
|
| 260 |
-
lines=22,
|
| 261 |
-
)
|
| 262 |
|
| 263 |
-
apply_btn = gr.Button("Apply to
|
| 264 |
apply_msg = gr.Textbox(label="Status", interactive=False)
|
| 265 |
apply_raw = gr.Textbox(label="Raw API response (debug)")
|
| 266 |
|
| 267 |
generate_btn.click(
|
| 268 |
fn=generate_yaml_from_prompt,
|
| 269 |
inputs=[prompt, namespace, kind],
|
| 270 |
-
outputs=yaml_box,
|
| 271 |
)
|
| 272 |
|
| 273 |
apply_btn.click(
|
|
@@ -276,39 +175,17 @@ def build_app() -> gr.Blocks:
|
|
| 276 |
outputs=[apply_msg, apply_raw],
|
| 277 |
)
|
| 278 |
|
| 279 |
-
#
|
| 280 |
-
# Cluster Dashboard tab
|
| 281 |
-
# ============================================================
|
| 282 |
with gr.Tab("Cluster Dashboard"):
|
| 283 |
-
gr.Markdown(
|
| 284 |
-
|
| 285 |
-
)
|
| 286 |
-
snapshot_btn = gr.Button("Refresh snapshot", variant="primary")
|
| 287 |
-
snapshot_box = gr.Textbox(
|
| 288 |
-
label="Cluster overview",
|
| 289 |
-
lines=25,
|
| 290 |
-
interactive=False,
|
| 291 |
-
)
|
| 292 |
|
| 293 |
-
snapshot_btn.click(
|
| 294 |
-
fn=get_cluster_snapshot,
|
| 295 |
-
inputs=[],
|
| 296 |
-
outputs=[snapshot_box],
|
| 297 |
-
)
|
| 298 |
|
| 299 |
-
#
|
| 300 |
-
# Pod Logs tab
|
| 301 |
-
# ============================================================
|
| 302 |
with gr.Tab("Pod Logs"):
|
| 303 |
-
gr.
|
| 304 |
-
"Paste a pod name from the **Cluster Dashboard** (e.g. `agentctl-job-lnrcp`) "
|
| 305 |
-
"and fetch its logs. Use **Follow logs** for a live `kubectl logs -f` style view."
|
| 306 |
-
)
|
| 307 |
-
|
| 308 |
-
pod_name = gr.Textbox(
|
| 309 |
-
label="Pod name",
|
| 310 |
-
placeholder="e.g. preprocess-job-z48rz",
|
| 311 |
-
)
|
| 312 |
tail_lines = gr.Slider(
|
| 313 |
label="Tail lines",
|
| 314 |
minimum=10,
|
|
@@ -316,24 +193,14 @@ def build_app() -> gr.Blocks:
|
|
| 316 |
step=10,
|
| 317 |
value=100,
|
| 318 |
)
|
|
|
|
|
|
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
follow_logs_btn = gr.Button("Follow logs (live)", variant="primary")
|
| 323 |
-
|
| 324 |
-
logs_box = gr.HTML(label="Logs", elem_id="logs_terminal")
|
| 325 |
-
|
| 326 |
-
get_logs_btn.click(
|
| 327 |
-
fn=get_pod_logs_once,
|
| 328 |
inputs=[pod_name, tail_lines],
|
| 329 |
outputs=[logs_box],
|
| 330 |
)
|
| 331 |
-
|
| 332 |
-
follow_logs_btn.click(
|
| 333 |
-
fn=follow_pod_logs,
|
| 334 |
-
inputs=[pod_name, tail_lines],
|
| 335 |
-
outputs=logs_box,
|
| 336 |
-
)
|
| 337 |
|
| 338 |
return demo
|
| 339 |
|
|
@@ -341,5 +208,4 @@ def build_app() -> gr.Blocks:
|
|
| 341 |
app = build_app()
|
| 342 |
|
| 343 |
if __name__ == "__main__":
|
| 344 |
-
|
| 345 |
-
app.launch(server_name="0.0.0.0", server_port=port)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
|
| 3 |
import os
|
|
|
|
|
|
|
| 4 |
from typing import Tuple
|
| 5 |
|
| 6 |
import gradio as gr
|
|
|
|
| 10 |
from agentctl.agent import K8sAgent
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
agent = K8sAgent()
|
| 14 |
|
| 15 |
|
| 16 |
def _get_client() -> K8sClient:
|
|
|
|
| 17 |
return K8sClient()
|
| 18 |
|
| 19 |
|
| 20 |
+
# ------------------------------------------------------------
|
| 21 |
+
# Handlers
|
| 22 |
+
# ------------------------------------------------------------
|
| 23 |
|
| 24 |
|
| 25 |
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
|
|
|
| 26 |
if not prompt.strip():
|
| 27 |
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
_, yaml_text = agent.nl_to_resource_yaml(
|
| 29 |
prompt,
|
| 30 |
+
namespace=namespace or settings.k8s_namespace,
|
| 31 |
+
kind=kind if kind != "Auto" else None,
|
| 32 |
)
|
| 33 |
return yaml_text
|
| 34 |
|
| 35 |
|
| 36 |
def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
|
|
|
| 37 |
if not yaml_text.strip():
|
| 38 |
return "No YAML to apply.", ""
|
| 39 |
|
| 40 |
client = _get_client()
|
| 41 |
result = client.apply_manifest(yaml_text)
|
| 42 |
|
| 43 |
+
if result.success:
|
| 44 |
+
return result.message, (result.raw_response and str(result.raw_response) or "")
|
| 45 |
+
else:
|
| 46 |
+
return f"❌ {result.message}", (result.raw_response and str(result.raw_response) or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
def get_cluster_snapshot() -> str:
|
|
|
|
| 50 |
client = _get_client()
|
| 51 |
snap = client.snapshot()
|
| 52 |
|
|
|
|
| 59 |
else:
|
| 60 |
for j in snap.jobs:
|
| 61 |
lines.append(
|
| 62 |
+
f" - {j.name}: succeeded={j.succeeded}, failed={j.failed}, active={j.active}"
|
|
|
|
| 63 |
)
|
| 64 |
|
| 65 |
# Pods
|
|
|
|
| 97 |
return "\n".join(lines)
|
| 98 |
|
| 99 |
|
| 100 |
+
def get_pod_logs_handler(pod_name: str, tail_lines: int) -> str:
|
| 101 |
+
if not pod_name.strip():
|
| 102 |
+
return "Enter a pod name from the snapshot above."
|
| 103 |
+
client = _get_client()
|
| 104 |
+
logs = client.get_pod_logs(pod_name.strip(), tail_lines=tail_lines)
|
| 105 |
+
return logs
|
| 106 |
|
| 107 |
|
| 108 |
+
# ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
# Gradio UI
|
| 110 |
+
# ------------------------------------------------------------
|
| 111 |
|
| 112 |
|
| 113 |
def build_app() -> gr.Blocks:
|
| 114 |
+
with gr.Blocks(title="AgentCTL: LLM-Driven Kubernetes Automation") as demo:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
gr.Markdown(
|
| 116 |
"""
|
| 117 |
+
# AgentCTL: LLM-Driven Kubernetes Automation
|
| 118 |
+
|
| 119 |
+
This Space connects to a **real Kubernetes cluster** (e.g. Minikube
|
| 120 |
+
exposed via `kubectl proxy` + ngrok). It demonstrates an
|
| 121 |
+
**agentic workflow** where natural language is converted into
|
| 122 |
+
Kubernetes Job / Deployment / CronJob manifests.
|
| 123 |
|
| 124 |
+
Environment variables:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
- `K8S_API_BASE_URL` → public K8s API URL (ngrok over `kubectl proxy`)
|
| 127 |
+
- `K8S_NAMESPACE` → default namespace
|
| 128 |
+
- `K8S_VERIFY_SSL` → true/false
|
| 129 |
+
- `AGENTCTL_USE_LLM` → true/false (optional; uses OpenAI if enabled)
|
| 130 |
+
- `OPENAI_API_KEY` → your key (if LLM enabled)
|
| 131 |
+
"""
|
| 132 |
)
|
| 133 |
|
| 134 |
+
# --------- Create Resource tab --------- #
|
|
|
|
|
|
|
| 135 |
with gr.Tab("Create Resource"):
|
| 136 |
with gr.Row():
|
| 137 |
prompt = gr.Textbox(
|
|
|
|
| 144 |
),
|
| 145 |
lines=5,
|
| 146 |
)
|
|
|
|
| 147 |
with gr.Column():
|
| 148 |
namespace = gr.Textbox(
|
| 149 |
label="Kubernetes namespace",
|
|
|
|
| 155 |
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 156 |
value="Auto",
|
| 157 |
)
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
+
generate_btn = gr.Button("Generate YAML")
|
| 160 |
+
yaml_box = gr.Code(label="Generated Manifest (YAML)", language="yaml", lines=22)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
+
apply_btn = gr.Button("Apply to Cluster")
|
| 163 |
apply_msg = gr.Textbox(label="Status", interactive=False)
|
| 164 |
apply_raw = gr.Textbox(label="Raw API response (debug)")
|
| 165 |
|
| 166 |
generate_btn.click(
|
| 167 |
fn=generate_yaml_from_prompt,
|
| 168 |
inputs=[prompt, namespace, kind],
|
| 169 |
+
outputs=[yaml_box],
|
| 170 |
)
|
| 171 |
|
| 172 |
apply_btn.click(
|
|
|
|
| 175 |
outputs=[apply_msg, apply_raw],
|
| 176 |
)
|
| 177 |
|
| 178 |
+
# --------- Dashboard tab --------- #
|
|
|
|
|
|
|
| 179 |
with gr.Tab("Cluster Dashboard"):
|
| 180 |
+
gr.Markdown("Snapshot of Jobs, Pods, Deployments and CronJobs in the namespace.")
|
| 181 |
+
snapshot_btn = gr.Button("Refresh snapshot")
|
| 182 |
+
snapshot_box = gr.Textbox(label="Cluster Overview", lines=25)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
+
snapshot_btn.click(fn=get_cluster_snapshot, inputs=[], outputs=[snapshot_box])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
+
# --------- Pod Logs tab --------- #
|
|
|
|
|
|
|
| 187 |
with gr.Tab("Pod Logs"):
|
| 188 |
+
pod_name = gr.Textbox(label="Pod name")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
tail_lines = gr.Slider(
|
| 190 |
label="Tail lines",
|
| 191 |
minimum=10,
|
|
|
|
| 193 |
step=10,
|
| 194 |
value=100,
|
| 195 |
)
|
| 196 |
+
logs_btn = gr.Button("Get logs")
|
| 197 |
+
logs_box = gr.Textbox(label="Logs", lines=25)
|
| 198 |
|
| 199 |
+
logs_btn.click(
|
| 200 |
+
fn=get_pod_logs_handler,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
inputs=[pod_name, tail_lines],
|
| 202 |
outputs=[logs_box],
|
| 203 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
return demo
|
| 206 |
|
|
|
|
| 208 |
app = build_app()
|
| 209 |
|
| 210 |
if __name__ == "__main__":
|
| 211 |
+
app.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
|
|
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/.gitignore
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# --- Python ---
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
*.pyo
|
| 5 |
+
*.pyd
|
| 6 |
+
*.pkl
|
| 7 |
+
*.pyd
|
| 8 |
+
*.so
|
| 9 |
+
|
| 10 |
+
# Virtual environment
|
| 11 |
+
.venv/
|
| 12 |
+
env/
|
| 13 |
+
venv/
|
| 14 |
+
|
| 15 |
+
# Jupyter
|
| 16 |
+
.ipynb_checkpoints/
|
| 17 |
+
*.ipynb
|
| 18 |
+
|
| 19 |
+
# --- OS files ---
|
| 20 |
+
.DS_Store
|
| 21 |
+
Thumbs.db
|
| 22 |
+
|
| 23 |
+
# --- VSCode / JetBrains ---
|
| 24 |
+
.vscode/
|
| 25 |
+
.idea/
|
| 26 |
+
|
| 27 |
+
# --- Logs ---
|
| 28 |
+
*.log
|
| 29 |
+
logs/
|
| 30 |
+
*.out
|
| 31 |
+
*.err
|
| 32 |
+
|
| 33 |
+
# --- Build / Packaging ---
|
| 34 |
+
build/
|
| 35 |
+
dist/
|
| 36 |
+
*.egg-info/
|
| 37 |
+
.eggs/
|
| 38 |
+
|
| 39 |
+
# --- Cache ---
|
| 40 |
+
.cache/
|
| 41 |
+
*.cache
|
| 42 |
+
*.mypy_cache/
|
| 43 |
+
*.pytest_cache/
|
| 44 |
+
*.ruff_cache/
|
| 45 |
+
|
| 46 |
+
# --- Hugging Face / HF Spaces ---
|
| 47 |
+
hf_cache/
|
| 48 |
+
huggingface/
|
| 49 |
+
.environment/
|
| 50 |
+
|
| 51 |
+
# --- Temp artifacts ---
|
| 52 |
+
tmp/
|
| 53 |
+
temp/
|
| 54 |
+
*.tmp
|
| 55 |
+
|
| 56 |
+
# --- Credentials / secrets (DO NOT COMMIT) ---
|
| 57 |
+
.env
|
| 58 |
+
.env.local
|
| 59 |
+
.env.* # safe because no secrets go into repo
|
| 60 |
+
secrets.yml
|
| 61 |
+
*.json.secret
|
| 62 |
+
*.key
|
| 63 |
+
*.pem
|
| 64 |
+
|
| 65 |
+
# --- Kubernetes ---
|
| 66 |
+
# kubeconfigs or kube generated files
|
| 67 |
+
kubeconfig
|
| 68 |
+
.kube/
|
| 69 |
+
*.yaml.bak
|
| 70 |
+
*~ # backups
|
| 71 |
+
|
| 72 |
+
# --- Models / Checkpoints ---
|
| 73 |
+
*.pt
|
| 74 |
+
*.bin
|
| 75 |
+
*.onnx
|
| 76 |
+
models/
|
| 77 |
+
checkpoints/
|
| 78 |
+
|
| 79 |
+
# --- Node (if future UI additions) ---
|
| 80 |
+
node_modules/
|
| 81 |
+
npm-debug.log*
|
| 82 |
+
|
| 83 |
+
# --- Gradio ---
|
| 84 |
+
gradio_cached_examples/
|
| 85 |
+
|
| 86 |
+
# --- Ngrok (just in case) ---
|
| 87 |
+
ngrok.yml
|
| 88 |
+
.ngrok/
|
| 89 |
+
|
| 90 |
+
# --- Minikube artifacts ---
|
| 91 |
+
minikube/
|
| 92 |
+
.kubectl-proxy/
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# Block OpenAI keys if accidentally named
|
| 96 |
+
openai_key.txt
|
| 97 |
+
openai_api_key.txt
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AgentCTL
|
| 3 |
+
emoji: ⚙️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.49.1
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
AgentCTL is a next-generation, agentic control plane for Kubernetes.
|
| 14 |
+
It allows you to create, manage, and observe Kubernetes resources using natural language, backed by a minimal REST client and optional LLM-powered YAML generation.
|
| 15 |
+
|
| 16 |
+
AgentCTL runs locally or can be deployed to Hugging Face Spaces, allowing cloud-hosted UI + locally hosted Kubernetes clusters.
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
__all__ = ["config", "k8s_client", "agent", "schemas"]
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/agent.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Natural language → Kubernetes YAML generator (Job / Deployment / CronJob).
|
| 2 |
+
|
| 3 |
+
Heuristic templates + optional LLM (OpenAI) for richer YAML.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from typing import Optional, Tuple
|
| 10 |
+
import os
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
# Optional LLM support (OpenAI)
|
| 14 |
+
try:
|
| 15 |
+
from openai import OpenAI # pip install openai
|
| 16 |
+
except Exception: # pragma: no cover
|
| 17 |
+
OpenAI = None # type: ignore
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class JobSpecRequest:
|
| 22 |
+
prompt: str
|
| 23 |
+
name: str = "agentctl-job"
|
| 24 |
+
image: str = "busybox:1.36"
|
| 25 |
+
command: Optional[str] = "echo Hello && sleep 5"
|
| 26 |
+
cpu: str = "500m"
|
| 27 |
+
memory: str = "512Mi"
|
| 28 |
+
namespace: str = "default"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class K8sAgent:
|
| 32 |
+
"""Agent that turns NL into Job / Deployment / CronJob YAML."""
|
| 33 |
+
|
| 34 |
+
# --------------------- Kind detection --------------------- #
|
| 35 |
+
|
| 36 |
+
def infer_kind(self, text: str) -> str:
|
| 37 |
+
t = text.lower()
|
| 38 |
+
|
| 39 |
+
# CronJob-ish language
|
| 40 |
+
if any(w in t for w in ["every minute", "every 5 minutes", "every hour", "cron", "schedule", "scheduled job"]):
|
| 41 |
+
return "CronJob"
|
| 42 |
+
|
| 43 |
+
# Deployment-ish language
|
| 44 |
+
if any(w in t for w in ["deployment", "service", "web", "api", "server", "nginx", "frontend", "backend"]):
|
| 45 |
+
return "Deployment"
|
| 46 |
+
|
| 47 |
+
# Default to Job
|
| 48 |
+
return "Job"
|
| 49 |
+
|
| 50 |
+
# --------------------- Job templates --------------------- #
|
| 51 |
+
|
| 52 |
+
def nl_to_job_request(self, text: str, namespace: str = "default") -> JobSpecRequest:
|
| 53 |
+
t = text.lower()
|
| 54 |
+
name = "agentctl-job"
|
| 55 |
+
|
| 56 |
+
if "preprocess" in t:
|
| 57 |
+
name = "preprocess-job"
|
| 58 |
+
elif "inference" in t:
|
| 59 |
+
name = "inference-job"
|
| 60 |
+
elif "train" in t or "training" in t:
|
| 61 |
+
name = "training-job"
|
| 62 |
+
|
| 63 |
+
image = "busybox:1.36"
|
| 64 |
+
if "python" in t:
|
| 65 |
+
image = "python:3.11-slim"
|
| 66 |
+
if "pytorch" in t:
|
| 67 |
+
image = "pytorch/pytorch:2.3.0-cuda12.1-cudnn9-runtime"
|
| 68 |
+
|
| 69 |
+
command = "echo Hello && sleep 5"
|
| 70 |
+
if "run python" in t:
|
| 71 |
+
idx = t.find("run python")
|
| 72 |
+
if idx != -1:
|
| 73 |
+
# everything from "python" onwards becomes the command
|
| 74 |
+
fragment = text[idx:].replace("run ", "").strip()
|
| 75 |
+
command = fragment
|
| 76 |
+
|
| 77 |
+
cpu = "500m"
|
| 78 |
+
memory = "512Mi"
|
| 79 |
+
if "gpu" in t:
|
| 80 |
+
cpu = "2000m"
|
| 81 |
+
memory = "8Gi"
|
| 82 |
+
|
| 83 |
+
return JobSpecRequest(
|
| 84 |
+
prompt=text,
|
| 85 |
+
name=name,
|
| 86 |
+
image=image,
|
| 87 |
+
command=command,
|
| 88 |
+
cpu=cpu,
|
| 89 |
+
memory=memory,
|
| 90 |
+
namespace=namespace,
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
def job_request_to_yaml(self, req: JobSpecRequest) -> str:
|
| 94 |
+
cmd = ["/bin/sh", "-c", req.command] if req.command else None
|
| 95 |
+
|
| 96 |
+
lines = [
|
| 97 |
+
"apiVersion: batch/v1",
|
| 98 |
+
"kind: Job",
|
| 99 |
+
"metadata:",
|
| 100 |
+
f" name: {req.name}",
|
| 101 |
+
f" namespace: {req.namespace}",
|
| 102 |
+
"spec:",
|
| 103 |
+
" template:",
|
| 104 |
+
" metadata:",
|
| 105 |
+
" labels:",
|
| 106 |
+
" app: agentctl",
|
| 107 |
+
" spec:",
|
| 108 |
+
" restartPolicy: Never",
|
| 109 |
+
" containers:",
|
| 110 |
+
" - name: main",
|
| 111 |
+
f" image: {req.image}",
|
| 112 |
+
]
|
| 113 |
+
if cmd:
|
| 114 |
+
lines.append(" command:")
|
| 115 |
+
for c in cmd:
|
| 116 |
+
lines.append(f" - {c}")
|
| 117 |
+
lines.extend(
|
| 118 |
+
[
|
| 119 |
+
" resources:",
|
| 120 |
+
" requests:",
|
| 121 |
+
f" cpu: {req.cpu}",
|
| 122 |
+
f" memory: {req.memory}",
|
| 123 |
+
" limits:",
|
| 124 |
+
f" cpu: {req.cpu}",
|
| 125 |
+
f" memory: {req.memory}",
|
| 126 |
+
]
|
| 127 |
+
)
|
| 128 |
+
return "\n".join(lines) + "\n"
|
| 129 |
+
|
| 130 |
+
# --------------------- Deployment template --------------------- #
|
| 131 |
+
|
| 132 |
+
def nl_to_deployment_yaml(self, text: str, namespace: str = "default") -> str:
|
| 133 |
+
t = text.lower()
|
| 134 |
+
name = "agentctl-deployment"
|
| 135 |
+
image = "nginx:1.27-alpine"
|
| 136 |
+
replicas = 1
|
| 137 |
+
|
| 138 |
+
if "nginx" in t:
|
| 139 |
+
name = "nginx-deployment"
|
| 140 |
+
if "python" in t:
|
| 141 |
+
image = "python:3.11-slim"
|
| 142 |
+
if "pytorch" in t:
|
| 143 |
+
image = "pytorch/pytorch:2.3.0-cuda12.1-cudnn9-runtime"
|
| 144 |
+
|
| 145 |
+
m = re.search(r"(\d+)\s+replica", t)
|
| 146 |
+
if m:
|
| 147 |
+
replicas = int(m.group(1))
|
| 148 |
+
|
| 149 |
+
lines = [
|
| 150 |
+
"apiVersion: apps/v1",
|
| 151 |
+
"kind: Deployment",
|
| 152 |
+
"metadata:",
|
| 153 |
+
f" name: {name}",
|
| 154 |
+
f" namespace: {namespace}",
|
| 155 |
+
" labels:",
|
| 156 |
+
" app: agentctl-deployment",
|
| 157 |
+
"spec:",
|
| 158 |
+
f" replicas: {replicas}",
|
| 159 |
+
" selector:",
|
| 160 |
+
" matchLabels:",
|
| 161 |
+
" app: agentctl-deployment",
|
| 162 |
+
" template:",
|
| 163 |
+
" metadata:",
|
| 164 |
+
" labels:",
|
| 165 |
+
" app: agentctl-deployment",
|
| 166 |
+
" spec:",
|
| 167 |
+
" containers:",
|
| 168 |
+
" - name: main",
|
| 169 |
+
f" image: {image}",
|
| 170 |
+
" ports:",
|
| 171 |
+
" - containerPort: 80",
|
| 172 |
+
]
|
| 173 |
+
return "\n".join(lines) + "\n"
|
| 174 |
+
|
| 175 |
+
# --------------------- CronJob template --------------------- #
|
| 176 |
+
|
| 177 |
+
def nl_to_cronjob_yaml(self, text: str, namespace: str = "default") -> str:
|
| 178 |
+
t = text.lower()
|
| 179 |
+
name = "agentctl-cronjob"
|
| 180 |
+
image = "busybox:1.36"
|
| 181 |
+
command = "echo Hello from CronJob && date"
|
| 182 |
+
schedule = "*/5 * * * *" # default every 5 minutes
|
| 183 |
+
|
| 184 |
+
if "every minute" in t:
|
| 185 |
+
schedule = "* * * * *"
|
| 186 |
+
elif "every 5 minutes" in t:
|
| 187 |
+
schedule = "*/5 * * * *"
|
| 188 |
+
elif "every hour" in t:
|
| 189 |
+
schedule = "0 * * * *"
|
| 190 |
+
elif "every day" in t:
|
| 191 |
+
schedule = "0 0 * * *"
|
| 192 |
+
|
| 193 |
+
if "python" in t:
|
| 194 |
+
image = "python:3.11-slim"
|
| 195 |
+
if "run python" in t:
|
| 196 |
+
idx = t.find("run python")
|
| 197 |
+
fragment = text[idx:].replace("run ", "").strip()
|
| 198 |
+
command = fragment
|
| 199 |
+
|
| 200 |
+
lines = [
|
| 201 |
+
"apiVersion: batch/v1",
|
| 202 |
+
"kind: CronJob",
|
| 203 |
+
"metadata:",
|
| 204 |
+
f" name: {name}",
|
| 205 |
+
f" namespace: {namespace}",
|
| 206 |
+
"spec:",
|
| 207 |
+
f" schedule: \"{schedule}\"",
|
| 208 |
+
" jobTemplate:",
|
| 209 |
+
" spec:",
|
| 210 |
+
" template:",
|
| 211 |
+
" metadata:",
|
| 212 |
+
" labels:",
|
| 213 |
+
" app: agentctl-cronjob",
|
| 214 |
+
" spec:",
|
| 215 |
+
" restartPolicy: Never",
|
| 216 |
+
" containers:",
|
| 217 |
+
" - name: main",
|
| 218 |
+
f" image: {image}",
|
| 219 |
+
" command:",
|
| 220 |
+
" - /bin/sh",
|
| 221 |
+
" - -c",
|
| 222 |
+
f" - {command}",
|
| 223 |
+
]
|
| 224 |
+
return "\n".join(lines) + "\n"
|
| 225 |
+
|
| 226 |
+
# --------------------- Optional OpenAI LLM --------------------- #
|
| 227 |
+
|
| 228 |
+
def maybe_llm_yaml(self, text: str, kind: str, namespace: str, fallback_yaml: str) -> str:
|
| 229 |
+
"""If AGENTCTL_USE_LLM=true and OpenAI is configured, ask the LLM
|
| 230 |
+
to generate/clean YAML. Otherwise return fallback_yaml.
|
| 231 |
+
"""
|
| 232 |
+
use_llm = os.getenv("AGENTCTL_USE_LLM", "false").lower() == "true"
|
| 233 |
+
api_key = os.getenv("OPENAI_API_KEY")
|
| 234 |
+
|
| 235 |
+
if not use_llm or not api_key or OpenAI is None:
|
| 236 |
+
return fallback_yaml
|
| 237 |
+
|
| 238 |
+
client = OpenAI(api_key=api_key)
|
| 239 |
+
model = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
| 240 |
+
|
| 241 |
+
system_msg = (
|
| 242 |
+
"You are an expert Kubernetes engineer. "
|
| 243 |
+
"Given a natural language request, output a single valid Kubernetes "
|
| 244 |
+
f"{kind} manifest in YAML. Do not include explanations or code fences."
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
user_msg = (
|
| 248 |
+
f"Namespace: {namespace}\n"
|
| 249 |
+
f"Kind: {kind}\n"
|
| 250 |
+
f"Request: {text}\n\n"
|
| 251 |
+
"Start from this template and improve it if needed:\n"
|
| 252 |
+
f"{fallback_yaml}"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
resp = client.chat.completions.create(
|
| 256 |
+
model=model,
|
| 257 |
+
messages=[
|
| 258 |
+
{"role": "system", "content": system_msg},
|
| 259 |
+
{"role": "user", "content": user_msg},
|
| 260 |
+
],
|
| 261 |
+
temperature=0.1,
|
| 262 |
+
)
|
| 263 |
+
content = resp.choices[0].message.content or fallback_yaml
|
| 264 |
+
|
| 265 |
+
# Strip ```yaml fences if present
|
| 266 |
+
content = content.strip()
|
| 267 |
+
if content.startswith("```"):
|
| 268 |
+
content = re.sub(r"^```[a-zA-Z]*", "", content).strip()
|
| 269 |
+
if content.endswith("```"):
|
| 270 |
+
content = content[:-3].strip()
|
| 271 |
+
|
| 272 |
+
return content
|
| 273 |
+
|
| 274 |
+
# --------------------- Public entrypoint --------------------- #
|
| 275 |
+
|
| 276 |
+
def nl_to_resource_yaml(
|
| 277 |
+
self,
|
| 278 |
+
text: str,
|
| 279 |
+
namespace: str = "default",
|
| 280 |
+
kind: Optional[str] = None,
|
| 281 |
+
) -> Tuple[str, str]:
|
| 282 |
+
"""Return (kind, yaml) for a given prompt."""
|
| 283 |
+
if not kind or kind.lower() == "auto":
|
| 284 |
+
kind = self.infer_kind(text)
|
| 285 |
+
|
| 286 |
+
if kind == "Job":
|
| 287 |
+
req = self.nl_to_job_request(text, namespace)
|
| 288 |
+
base_yaml = self.job_request_to_yaml(req)
|
| 289 |
+
elif kind == "Deployment":
|
| 290 |
+
base_yaml = self.nl_to_deployment_yaml(text, namespace)
|
| 291 |
+
elif kind == "CronJob":
|
| 292 |
+
base_yaml = self.nl_to_cronjob_yaml(text, namespace)
|
| 293 |
+
else:
|
| 294 |
+
raise ValueError(f"Unsupported kind: {kind}")
|
| 295 |
+
|
| 296 |
+
final_yaml = self.maybe_llm_yaml(text, kind=kind, namespace=namespace, fallback_yaml=base_yaml)
|
| 297 |
+
return kind, final_yaml
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/config.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
|
| 4 |
+
@dataclass
|
| 5 |
+
class Settings:
|
| 6 |
+
k8s_api_base_url: str = os.getenv("K8S_API_BASE_URL", "")
|
| 7 |
+
k8s_namespace: str = os.getenv("K8S_NAMESPACE", "default")
|
| 8 |
+
verify_ssl: bool = os.getenv("K8S_VERIFY_SSL", "false").lower() == "true"
|
| 9 |
+
k8s_bearer_token: str | None = os.getenv("K8S_BEARER_TOKEN") or None
|
| 10 |
+
|
| 11 |
+
settings = Settings()
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/k8s_client.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal Kubernetes HTTP client using the public API exposed via ngrok."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Dict, Any, List
|
| 7 |
+
|
| 8 |
+
import requests
|
| 9 |
+
import yaml
|
| 10 |
+
|
| 11 |
+
from .config import settings
|
| 12 |
+
from .schemas import (
|
| 13 |
+
ApplyResult,
|
| 14 |
+
JobStatus,
|
| 15 |
+
PodInfo,
|
| 16 |
+
ClusterSnapshot,
|
| 17 |
+
DeploymentStatus,
|
| 18 |
+
CronJobStatus,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class K8sClient:
|
| 23 |
+
def __init__(self, base_url: str | None = None, namespace: str | None = None):
|
| 24 |
+
if not base_url:
|
| 25 |
+
base_url = settings.k8s_api_base_url
|
| 26 |
+
if not base_url:
|
| 27 |
+
raise ValueError("K8S_API_BASE_URL is not set. Configure it in env.")
|
| 28 |
+
|
| 29 |
+
self.base_url = base_url.rstrip("/")
|
| 30 |
+
self.namespace = namespace or settings.k8s_namespace
|
| 31 |
+
self.verify_ssl = settings.verify_ssl
|
| 32 |
+
self.bearer_token = settings.k8s_bearer_token
|
| 33 |
+
|
| 34 |
+
# ------------------------------------------------------------------ #
|
| 35 |
+
# Internal helpers #
|
| 36 |
+
# ------------------------------------------------------------------ #
|
| 37 |
+
|
| 38 |
+
def _headers(self) -> Dict[str, str]:
|
| 39 |
+
headers = {"Content-Type": "application/json"}
|
| 40 |
+
if self.bearer_token:
|
| 41 |
+
headers["Authorization"] = f"Bearer {self.bearer_token}"
|
| 42 |
+
return headers
|
| 43 |
+
|
| 44 |
+
def _request(
|
| 45 |
+
self,
|
| 46 |
+
method: str,
|
| 47 |
+
path: str,
|
| 48 |
+
json_body: Dict[str, Any] | None = None,
|
| 49 |
+
) -> requests.Response:
|
| 50 |
+
url = f"{self.base_url}{path}"
|
| 51 |
+
resp = requests.request(
|
| 52 |
+
method=method,
|
| 53 |
+
url=url,
|
| 54 |
+
headers=self._headers(),
|
| 55 |
+
json=json_body,
|
| 56 |
+
verify=self.verify_ssl,
|
| 57 |
+
timeout=30,
|
| 58 |
+
)
|
| 59 |
+
return resp
|
| 60 |
+
|
| 61 |
+
# ------------------------------------------------------------------ #
|
| 62 |
+
# Apply a manifest (Job / Deployment / CronJob) #
|
| 63 |
+
# ------------------------------------------------------------------ #
|
| 64 |
+
|
| 65 |
+
def apply_manifest(self, manifest_yaml: str) -> ApplyResult:
|
| 66 |
+
"""Create a resource (Job / Deployment / CronJob) from YAML."""
|
| 67 |
+
try:
|
| 68 |
+
manifest = yaml.safe_load(manifest_yaml)
|
| 69 |
+
except yaml.YAMLError as e:
|
| 70 |
+
return ApplyResult(success=False, message=f"Invalid YAML: {e}")
|
| 71 |
+
|
| 72 |
+
if not isinstance(manifest, dict):
|
| 73 |
+
return ApplyResult(success=False, message="Manifest must be a single YAML object.")
|
| 74 |
+
|
| 75 |
+
kind = manifest.get("kind")
|
| 76 |
+
metadata = manifest.get("metadata", {})
|
| 77 |
+
name = metadata.get("name", "<unnamed>")
|
| 78 |
+
namespace = metadata.get("namespace", self.namespace)
|
| 79 |
+
|
| 80 |
+
if kind == "Job":
|
| 81 |
+
path = f"/apis/batch/v1/namespaces/{namespace}/jobs"
|
| 82 |
+
elif kind == "Deployment":
|
| 83 |
+
path = f"/apis/apps/v1/namespaces/{namespace}/deployments"
|
| 84 |
+
elif kind == "CronJob":
|
| 85 |
+
path = f"/apis/batch/v1/namespaces/{namespace}/cronjobs"
|
| 86 |
+
else:
|
| 87 |
+
return ApplyResult(success=False, message=f"Unsupported kind: {kind}")
|
| 88 |
+
|
| 89 |
+
resp = self._request("POST", path, json_body=manifest)
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
raw = resp.json()
|
| 93 |
+
except json.JSONDecodeError:
|
| 94 |
+
raw = {"raw_text": resp.text}
|
| 95 |
+
|
| 96 |
+
if resp.status_code in (200, 201):
|
| 97 |
+
return ApplyResult(
|
| 98 |
+
success=True,
|
| 99 |
+
message=f"Created {kind} '{name}' in ns '{namespace}'.",
|
| 100 |
+
raw_response=raw,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
return ApplyResult(
|
| 104 |
+
success=False,
|
| 105 |
+
message=f"K8s API error {resp.status_code}: {raw}",
|
| 106 |
+
raw_response=raw,
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# ------------------------------------------------------------------ #
|
| 110 |
+
# List helpers for dashboard #
|
| 111 |
+
# ------------------------------------------------------------------ #
|
| 112 |
+
|
| 113 |
+
def list_jobs(self) -> List[JobStatus]:
|
| 114 |
+
path = f"/apis/batch/v1/namespaces/{self.namespace}/jobs"
|
| 115 |
+
resp = self._request("GET", path)
|
| 116 |
+
data = resp.json()
|
| 117 |
+
items = data.get("items", [])
|
| 118 |
+
jobs: List[JobStatus] = []
|
| 119 |
+
for item in items:
|
| 120 |
+
meta = item.get("metadata", {})
|
| 121 |
+
status = item.get("status", {})
|
| 122 |
+
jobs.append(
|
| 123 |
+
JobStatus(
|
| 124 |
+
name=meta.get("name", "<unknown>"),
|
| 125 |
+
namespace=meta.get("namespace", self.namespace),
|
| 126 |
+
succeeded=status.get("succeeded", 0) or 0,
|
| 127 |
+
failed=status.get("failed", 0) or 0,
|
| 128 |
+
active=status.get("active", 0) or 0,
|
| 129 |
+
)
|
| 130 |
+
)
|
| 131 |
+
return jobs
|
| 132 |
+
|
| 133 |
+
def list_pods(self) -> List[PodInfo]:
|
| 134 |
+
path = f"/api/v1/namespaces/{self.namespace}/pods"
|
| 135 |
+
resp = self._request("GET", path)
|
| 136 |
+
data = resp.json()
|
| 137 |
+
items = data.get("items", [])
|
| 138 |
+
pods: List[PodInfo] = []
|
| 139 |
+
for item in items:
|
| 140 |
+
meta = item.get("metadata", {})
|
| 141 |
+
status = item.get("status", {})
|
| 142 |
+
pods.append(
|
| 143 |
+
PodInfo(
|
| 144 |
+
name=meta.get("name", "<unknown>"),
|
| 145 |
+
phase=status.get("phase", "Unknown"),
|
| 146 |
+
node_name=status.get("nodeName"),
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
return pods
|
| 150 |
+
|
| 151 |
+
def list_deployments(self) -> List[DeploymentStatus]:
|
| 152 |
+
path = f"/apis/apps/v1/namespaces/{self.namespace}/deployments"
|
| 153 |
+
resp = self._request("GET", path)
|
| 154 |
+
data = resp.json()
|
| 155 |
+
items = data.get("items", [])
|
| 156 |
+
out: List[DeploymentStatus] = []
|
| 157 |
+
for item in items:
|
| 158 |
+
meta = item.get("metadata", {})
|
| 159 |
+
st = item.get("status", {})
|
| 160 |
+
out.append(
|
| 161 |
+
DeploymentStatus(
|
| 162 |
+
name=meta.get("name", "<unknown>"),
|
| 163 |
+
namespace=meta.get("namespace", self.namespace),
|
| 164 |
+
replicas=st.get("replicas", 0) or 0,
|
| 165 |
+
ready=st.get("readyReplicas", 0) or 0,
|
| 166 |
+
)
|
| 167 |
+
)
|
| 168 |
+
return out
|
| 169 |
+
|
| 170 |
+
def list_cronjobs(self) -> List[CronJobStatus]:
|
| 171 |
+
path = f"/apis/batch/v1/namespaces/{self.namespace}/cronjobs"
|
| 172 |
+
resp = self._request("GET", path)
|
| 173 |
+
data = resp.json()
|
| 174 |
+
items = data.get("items", [])
|
| 175 |
+
out: List[CronJobStatus] = []
|
| 176 |
+
for item in items:
|
| 177 |
+
meta = item.get("metadata", {})
|
| 178 |
+
st = item.get("status", {})
|
| 179 |
+
last = st.get("lastScheduleTime")
|
| 180 |
+
out.append(
|
| 181 |
+
CronJobStatus(
|
| 182 |
+
name=meta.get("name", "<unknown>"),
|
| 183 |
+
namespace=meta.get("namespace", self.namespace),
|
| 184 |
+
active=len(st.get("active", []) or []),
|
| 185 |
+
last_schedule=str(last) if last is not None else None,
|
| 186 |
+
)
|
| 187 |
+
)
|
| 188 |
+
return out
|
| 189 |
+
|
| 190 |
+
def get_pod_logs(self, pod_name: str, container: str | None = None, tail_lines: int = 100) -> str:
|
| 191 |
+
params = {"tailLines": str(tail_lines)}
|
| 192 |
+
if container:
|
| 193 |
+
params["container"] = container
|
| 194 |
+
|
| 195 |
+
path = f"/api/v1/namespaces/{self.namespace}/pods/{pod_name}/log"
|
| 196 |
+
url = f"{self.base_url}{path}"
|
| 197 |
+
resp = requests.get(url, headers=self._headers(), params=params, verify=self.verify_ssl, timeout=30)
|
| 198 |
+
if resp.status_code == 200:
|
| 199 |
+
return resp.text
|
| 200 |
+
return f"Error {resp.status_code}: {resp.text}"
|
| 201 |
+
|
| 202 |
+
def snapshot(self) -> ClusterSnapshot:
|
| 203 |
+
return ClusterSnapshot(
|
| 204 |
+
namespace=self.namespace,
|
| 205 |
+
jobs=self.list_jobs(),
|
| 206 |
+
pods=self.list_pods(),
|
| 207 |
+
deployments=self.list_deployments(),
|
| 208 |
+
cronjobs=self.list_cronjobs(),
|
| 209 |
+
)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/agentctl/schemas.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional, List, Dict, Any
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ApplyResult(BaseModel):
|
| 6 |
+
success: bool
|
| 7 |
+
message: str
|
| 8 |
+
raw_response: Optional[Dict[str, Any]] = None
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class JobStatus(BaseModel):
|
| 12 |
+
name: str
|
| 13 |
+
namespace: str
|
| 14 |
+
succeeded: int = 0
|
| 15 |
+
failed: int = 0
|
| 16 |
+
active: int = 0
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class PodInfo(BaseModel):
|
| 20 |
+
name: str
|
| 21 |
+
phase: str
|
| 22 |
+
node_name: Optional[str] = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class DeploymentStatus(BaseModel):
|
| 26 |
+
name: str
|
| 27 |
+
namespace: str
|
| 28 |
+
replicas: int = 0
|
| 29 |
+
ready: int = 0
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class CronJobStatus(BaseModel):
|
| 33 |
+
name: str
|
| 34 |
+
namespace: str
|
| 35 |
+
active: int = 0
|
| 36 |
+
last_schedule: Optional[str] = None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ClusterSnapshot(BaseModel):
|
| 40 |
+
namespace: str
|
| 41 |
+
jobs: List[JobStatus] = Field(default_factory=list)
|
| 42 |
+
pods: List[PodInfo] = Field(default_factory=list)
|
| 43 |
+
deployments: List[DeploymentStatus] = Field(default_factory=list)
|
| 44 |
+
cronjobs: List[CronJobStatus] = Field(default_factory=list)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/app.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import time
|
| 6 |
+
from typing import Tuple
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
|
| 10 |
+
from agentctl.config import settings
|
| 11 |
+
from agentctl.k8s_client import K8sClient
|
| 12 |
+
from agentctl.agent import K8sAgent
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# ---------------------------------------------------------------------
|
| 16 |
+
# Core objects
|
| 17 |
+
# ---------------------------------------------------------------------
|
| 18 |
+
|
| 19 |
+
agent = K8sAgent()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_client() -> K8sClient:
|
| 23 |
+
"""Create a Kubernetes HTTP client using env-based config."""
|
| 24 |
+
return K8sClient()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# ---------------------------------------------------------------------
|
| 28 |
+
# YAML generation + apply
|
| 29 |
+
# ---------------------------------------------------------------------
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def generate_yaml_from_prompt(prompt: str, namespace: str, kind: str) -> str:
|
| 33 |
+
"""Convert natural language + kind selection into YAML."""
|
| 34 |
+
if not prompt.strip():
|
| 35 |
+
return "# Enter a description, e.g. 'run a python preprocessing job'"
|
| 36 |
+
|
| 37 |
+
ns = namespace.strip() or settings.k8s_namespace
|
| 38 |
+
kind_sel = None if kind == "Auto" else kind
|
| 39 |
+
|
| 40 |
+
_, yaml_text = agent.nl_to_resource_yaml(
|
| 41 |
+
prompt,
|
| 42 |
+
namespace=ns,
|
| 43 |
+
kind=kind_sel,
|
| 44 |
+
)
|
| 45 |
+
return yaml_text
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def apply_yaml_to_cluster(yaml_text: str) -> Tuple[str, str]:
|
| 49 |
+
"""Apply a YAML manifest to the cluster via K8s API."""
|
| 50 |
+
if not yaml_text.strip():
|
| 51 |
+
return "No YAML to apply.", ""
|
| 52 |
+
|
| 53 |
+
client = _get_client()
|
| 54 |
+
result = client.apply_manifest(yaml_text)
|
| 55 |
+
|
| 56 |
+
raw_str = ""
|
| 57 |
+
if result.raw_response is not None:
|
| 58 |
+
try:
|
| 59 |
+
raw_str = str(result.raw_response)
|
| 60 |
+
except Exception:
|
| 61 |
+
raw_str = "<unserialisable response>"
|
| 62 |
+
|
| 63 |
+
prefix = "✅" if result.success else "❌"
|
| 64 |
+
return f"{prefix} {result.message}", raw_str
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
# ---------------------------------------------------------------------
|
| 68 |
+
# Dashboard snapshot
|
| 69 |
+
# ---------------------------------------------------------------------
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def get_cluster_snapshot() -> str:
|
| 73 |
+
"""Return a human-readable snapshot of Jobs, Pods, Deployments, CronJobs."""
|
| 74 |
+
client = _get_client()
|
| 75 |
+
snap = client.snapshot()
|
| 76 |
+
|
| 77 |
+
lines: list[str] = [f"Namespace: {snap.namespace}", ""]
|
| 78 |
+
|
| 79 |
+
# Jobs
|
| 80 |
+
lines.append("Jobs:")
|
| 81 |
+
if not snap.jobs:
|
| 82 |
+
lines.append(" (no jobs)")
|
| 83 |
+
else:
|
| 84 |
+
for j in snap.jobs:
|
| 85 |
+
lines.append(
|
| 86 |
+
f" - {j.name}: succeeded={j.succeeded}, "
|
| 87 |
+
f"failed={j.failed}, active={j.active}"
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Pods
|
| 91 |
+
lines.append("")
|
| 92 |
+
lines.append("Pods:")
|
| 93 |
+
if not snap.pods:
|
| 94 |
+
lines.append(" (no pods)")
|
| 95 |
+
else:
|
| 96 |
+
for p in snap.pods:
|
| 97 |
+
lines.append(f" - {p.name}: phase={p.phase}")
|
| 98 |
+
|
| 99 |
+
# Deployments
|
| 100 |
+
lines.append("")
|
| 101 |
+
lines.append("Deployments:")
|
| 102 |
+
if not snap.deployments:
|
| 103 |
+
lines.append(" (no deployments)")
|
| 104 |
+
else:
|
| 105 |
+
for d in snap.deployments:
|
| 106 |
+
lines.append(
|
| 107 |
+
f" - {d.name}: replicas={d.replicas}, ready={d.ready}"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
# CronJobs
|
| 111 |
+
lines.append("")
|
| 112 |
+
lines.append("CronJobs:")
|
| 113 |
+
if not snap.cronjobs:
|
| 114 |
+
lines.append(" (no cronjobs)")
|
| 115 |
+
else:
|
| 116 |
+
for c in snap.cronjobs:
|
| 117 |
+
last = c.last_schedule or "never"
|
| 118 |
+
lines.append(
|
| 119 |
+
f" - {c.name}: active={c.active}, lastScheduleTime={last}"
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
return "\n".join(lines)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# ---------------------------------------------------------------------
|
| 126 |
+
# Logs helpers (colorised terminal + live follow)
|
| 127 |
+
# ---------------------------------------------------------------------
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _fetch_pod_logs_raw(pod_name: str, tail_lines: int) -> str:
|
| 131 |
+
"""Internal: raw log text from Kubernetes."""
|
| 132 |
+
client = _get_client()
|
| 133 |
+
return client.get_pod_logs(pod_name.strip(), tail_lines=tail_lines)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _colorize_logs(text: str) -> str:
|
| 137 |
+
"""Convert log text into HTML with basic severity colouring."""
|
| 138 |
+
if not text:
|
| 139 |
+
return "<span style='color:#888;'>No logs</span>"
|
| 140 |
+
|
| 141 |
+
lines = text.splitlines()
|
| 142 |
+
out: list[str] = []
|
| 143 |
+
|
| 144 |
+
for line in lines:
|
| 145 |
+
if re.search(r"(error|failed|exception)", line, re.IGNORECASE):
|
| 146 |
+
colour = "#ff4b4b" # red
|
| 147 |
+
elif re.search(r"(warn)", line, re.IGNORECASE):
|
| 148 |
+
colour = "#f7c843" # yellow
|
| 149 |
+
elif re.search(r"(info|started|running|completed)", line, re.IGNORECASE):
|
| 150 |
+
colour = "#5ad55a" # green
|
| 151 |
+
else:
|
| 152 |
+
colour = "#d0d0d0" # grey
|
| 153 |
+
|
| 154 |
+
safe = line.replace("<", "<").replace(">", ">")
|
| 155 |
+
out.append(f"<span style='color:{colour};'>{safe}</span>")
|
| 156 |
+
|
| 157 |
+
return "<br>".join(out)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def get_pod_logs_once(pod_name: str, tail_lines: int) -> str:
|
| 161 |
+
"""One-shot logs fetch, used for the 'Get logs' button."""
|
| 162 |
+
pod_name = pod_name.strip()
|
| 163 |
+
if not pod_name:
|
| 164 |
+
return "<span style='color:#ff4b4b;'>Enter a pod name from the dashboard.</span>"
|
| 165 |
+
|
| 166 |
+
raw = _fetch_pod_logs_raw(pod_name, tail_lines)
|
| 167 |
+
return _colorize_logs(raw)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def follow_pod_logs(pod_name: str, tail_lines: int):
|
| 171 |
+
"""Live streaming logs using Gradio generator output."""
|
| 172 |
+
pod_name = pod_name.strip()
|
| 173 |
+
if not pod_name:
|
| 174 |
+
yield "<span style='color:#ff4b4b;'>Enter a pod name from the dashboard.</span>"
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
while True:
|
| 178 |
+
raw = _fetch_pod_logs_raw(pod_name, tail_lines)
|
| 179 |
+
html = _colorize_logs(raw)
|
| 180 |
+
yield html
|
| 181 |
+
time.sleep(2)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
# CSS for the logs "terminal"
|
| 185 |
+
TERMINAL_CSS = """
|
| 186 |
+
#logs_terminal {
|
| 187 |
+
background-color: #111111 !important;
|
| 188 |
+
color: #d0d0d0 !important;
|
| 189 |
+
font-family: monospace !important;
|
| 190 |
+
padding: 14px;
|
| 191 |
+
border-radius: 8px;
|
| 192 |
+
border: 1px solid #444444;
|
| 193 |
+
height: 420px;
|
| 194 |
+
overflow-y: scroll;
|
| 195 |
+
white-space: pre-wrap;
|
| 196 |
+
}
|
| 197 |
+
"""
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
# ---------------------------------------------------------------------
|
| 201 |
+
# Gradio UI
|
| 202 |
+
# ---------------------------------------------------------------------
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def build_app() -> gr.Blocks:
|
| 206 |
+
with gr.Blocks(
|
| 207 |
+
title="AgentCTL: LLM-Driven Kubernetes Automation",
|
| 208 |
+
css=TERMINAL_CSS,
|
| 209 |
+
) as demo:
|
| 210 |
+
# Top environment hint (like your screenshot)
|
| 211 |
+
gr.Markdown(
|
| 212 |
+
"""
|
| 213 |
+
### 🔧 Environment configuration
|
| 214 |
+
|
| 215 |
+
- `K8S_API_BASE_URL` → **public K8s API URL** (ngrok / Cloudflare tunnel over `kubectl proxy`)
|
| 216 |
+
- `K8S_NAMESPACE` → **default namespace** (e.g. `default`)
|
| 217 |
+
- `K8S_VERIFY_SSL` → **true/false** (set `false` for self-signed ngrok endpoints)
|
| 218 |
+
- `AGENTCTL_USE_LLM` → **true/false** (optional; uses OpenAI if enabled in `agent.py`)
|
| 219 |
+
- `OPENAI_API_KEY` → your key (if LLM mode enabled)
|
| 220 |
+
|
| 221 |
+
---
|
| 222 |
+
"""
|
| 223 |
+
)
|
| 224 |
+
|
| 225 |
+
# ============================================================
|
| 226 |
+
# Create Resource tab
|
| 227 |
+
# ============================================================
|
| 228 |
+
with gr.Tab("Create Resource"):
|
| 229 |
+
with gr.Row():
|
| 230 |
+
prompt = gr.Textbox(
|
| 231 |
+
label="Describe the resource you want",
|
| 232 |
+
placeholder=(
|
| 233 |
+
"Examples:\n"
|
| 234 |
+
"- run a python job to preprocess data\n"
|
| 235 |
+
"- create an nginx deployment with 3 replicas\n"
|
| 236 |
+
"- schedule a python cleanup job every 5 minutes\n"
|
| 237 |
+
),
|
| 238 |
+
lines=5,
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
with gr.Column():
|
| 242 |
+
namespace = gr.Textbox(
|
| 243 |
+
label="Kubernetes namespace",
|
| 244 |
+
value=settings.k8s_namespace,
|
| 245 |
+
lines=1,
|
| 246 |
+
)
|
| 247 |
+
kind = gr.Dropdown(
|
| 248 |
+
label="Resource kind",
|
| 249 |
+
choices=["Auto", "Job", "Deployment", "CronJob"],
|
| 250 |
+
value="Auto",
|
| 251 |
+
)
|
| 252 |
+
gr.Markdown(
|
| 253 |
+
"Tip: leave **Auto** to let the agent infer Job vs Deployment vs CronJob."
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
generate_btn = gr.Button("Generate YAML", variant="primary")
|
| 257 |
+
yaml_box = gr.Code(
|
| 258 |
+
label="Generated manifest (YAML)",
|
| 259 |
+
language="yaml",
|
| 260 |
+
lines=22,
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
apply_btn = gr.Button("Apply to cluster")
|
| 264 |
+
apply_msg = gr.Textbox(label="Status", interactive=False)
|
| 265 |
+
apply_raw = gr.Textbox(label="Raw API response (debug)")
|
| 266 |
+
|
| 267 |
+
generate_btn.click(
|
| 268 |
+
fn=generate_yaml_from_prompt,
|
| 269 |
+
inputs=[prompt, namespace, kind],
|
| 270 |
+
outputs=yaml_box,
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
apply_btn.click(
|
| 274 |
+
fn=apply_yaml_to_cluster,
|
| 275 |
+
inputs=[yaml_box],
|
| 276 |
+
outputs=[apply_msg, apply_raw],
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# ============================================================
|
| 280 |
+
# Cluster Dashboard tab
|
| 281 |
+
# ============================================================
|
| 282 |
+
with gr.Tab("Cluster Dashboard"):
|
| 283 |
+
gr.Markdown(
|
| 284 |
+
"Snapshot of **Jobs, Pods, Deployments, CronJobs** in the target namespace."
|
| 285 |
+
)
|
| 286 |
+
snapshot_btn = gr.Button("Refresh snapshot", variant="primary")
|
| 287 |
+
snapshot_box = gr.Textbox(
|
| 288 |
+
label="Cluster overview",
|
| 289 |
+
lines=25,
|
| 290 |
+
interactive=False,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
snapshot_btn.click(
|
| 294 |
+
fn=get_cluster_snapshot,
|
| 295 |
+
inputs=[],
|
| 296 |
+
outputs=[snapshot_box],
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
# ============================================================
|
| 300 |
+
# Pod Logs tab
|
| 301 |
+
# ============================================================
|
| 302 |
+
with gr.Tab("Pod Logs"):
|
| 303 |
+
gr.Markdown(
|
| 304 |
+
"Paste a pod name from the **Cluster Dashboard** (e.g. `agentctl-job-lnrcp`) "
|
| 305 |
+
"and fetch its logs. Use **Follow logs** for a live `kubectl logs -f` style view."
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
pod_name = gr.Textbox(
|
| 309 |
+
label="Pod name",
|
| 310 |
+
placeholder="e.g. preprocess-job-z48rz",
|
| 311 |
+
)
|
| 312 |
+
tail_lines = gr.Slider(
|
| 313 |
+
label="Tail lines",
|
| 314 |
+
minimum=10,
|
| 315 |
+
maximum=500,
|
| 316 |
+
step=10,
|
| 317 |
+
value=100,
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
with gr.Row():
|
| 321 |
+
get_logs_btn = gr.Button("Get logs once")
|
| 322 |
+
follow_logs_btn = gr.Button("Follow logs (live)", variant="primary")
|
| 323 |
+
|
| 324 |
+
logs_box = gr.HTML(label="Logs", elem_id="logs_terminal")
|
| 325 |
+
|
| 326 |
+
get_logs_btn.click(
|
| 327 |
+
fn=get_pod_logs_once,
|
| 328 |
+
inputs=[pod_name, tail_lines],
|
| 329 |
+
outputs=[logs_box],
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
follow_logs_btn.click(
|
| 333 |
+
fn=follow_pod_logs,
|
| 334 |
+
inputs=[pod_name, tail_lines],
|
| 335 |
+
outputs=logs_box,
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
return demo
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
app = build_app()
|
| 342 |
+
|
| 343 |
+
if __name__ == "__main__":
|
| 344 |
+
port = int(os.getenv("PORT", "7860"))
|
| 345 |
+
app.launch(server_name="0.0.0.0", server_port=port)
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
requests>=2.31.0
|
| 3 |
+
PyYAML>=6.0
|
| 4 |
+
pydantic>=2.7.0
|
| 5 |
+
python-dotenv>=1.0.0
|
| 6 |
+
openai>=1.0.0 # for LLM-based YAML generation
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/k8s/deployment-example.yaml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apiVersion: apps/v1
|
| 2 |
+
kind: Deployment
|
| 3 |
+
metadata:
|
| 4 |
+
name: example-nginx-deployment
|
| 5 |
+
namespace: default
|
| 6 |
+
labels:
|
| 7 |
+
app: agentctl-nginx
|
| 8 |
+
spec:
|
| 9 |
+
replicas: 1
|
| 10 |
+
selector:
|
| 11 |
+
matchLabels:
|
| 12 |
+
app: agentctl-nginx
|
| 13 |
+
template:
|
| 14 |
+
metadata:
|
| 15 |
+
labels:
|
| 16 |
+
app: agentctl-nginx
|
| 17 |
+
spec:
|
| 18 |
+
containers:
|
| 19 |
+
- name: nginx
|
| 20 |
+
image: nginx:1.27-alpine
|
| 21 |
+
ports:
|
| 22 |
+
- containerPort: 80
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/k8s/job-example.yaml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apiVersion: batch/v1
|
| 2 |
+
kind: Job
|
| 3 |
+
metadata:
|
| 4 |
+
name: example-echo-job
|
| 5 |
+
namespace: default
|
| 6 |
+
spec:
|
| 7 |
+
template:
|
| 8 |
+
metadata:
|
| 9 |
+
labels:
|
| 10 |
+
app: agentctl-example
|
| 11 |
+
spec:
|
| 12 |
+
restartPolicy: Never
|
| 13 |
+
containers:
|
| 14 |
+
- name: main
|
| 15 |
+
image: busybox:1.36
|
| 16 |
+
command:
|
| 17 |
+
- /bin/sh
|
| 18 |
+
- -c
|
| 19 |
+
- |
|
| 20 |
+
echo "Hello from example job";
|
| 21 |
+
sleep 10;
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
gradio
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
| 1 |
+
gradio==4.26.0
|
| 2 |
+
huggingface_hub==0.19.4
|
| 3 |
+
requests==2.31.0
|
| 4 |
+
PyYAML==6.0
|
| 5 |
+
pydantic==2.7.0
|
| 6 |
+
python-dotenv==1.0.0
|
| 7 |
+
openai==1.0.0
|
hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/hf-space/requirements.txt
CHANGED
|
@@ -5,3 +5,5 @@ PyYAML==6.0
|
|
| 5 |
pydantic==2.7.0
|
| 6 |
python-dotenv==1.0.0
|
| 7 |
openai==1.0.0
|
|
|
|
|
|
|
|
|
| 5 |
pydantic==2.7.0
|
| 6 |
python-dotenv==1.0.0
|
| 7 |
openai==1.0.0
|
| 8 |
+
fastapi
|
| 9 |
+
uvicorn
|
hf-space/hf-space/requirements.txt
CHANGED
|
@@ -7,3 +7,6 @@ python-dotenv==1.0.0
|
|
| 7 |
openai==1.0.0
|
| 8 |
fastapi
|
| 9 |
uvicorn
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
openai==1.0.0
|
| 8 |
fastapi
|
| 9 |
uvicorn
|
| 10 |
+
|
| 11 |
+
# For free LLMs via HuggingFace Inference API
|
| 12 |
+
huggingface_hub==0.19.4
|