File size: 9,193 Bytes
3b250b7
4e1ac04
 
 
 
3b250b7
 
08a98df
267fc17
3b250b7
 
4e1ac04
267fc17
4e1ac04
 
 
 
 
3b250b7
8280dca
b102101
4e1ac04
 
 
3b250b7
 
4e1ac04
 
 
267fc17
 
4e1ac04
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8280dca
4e1ac04
 
 
 
8280dca
d468ac7
 
 
4e1ac04
8280dca
08a98df
e5dfbda
 
 
 
 
 
 
 
 
 
 
 
d3642af
 
 
 
 
 
 
 
 
 
e5dfbda
d3642af
 
8280dca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267fc17
 
 
4e1ac04
 
 
 
 
 
 
8280dca
267fc17
 
 
d468ac7
08a98df
4e1ac04
 
08a98df
 
 
4e1ac04
 
 
3b250b7
4e1ac04
 
3b250b7
267fc17
8280dca
267fc17
8280dca
 
4e1ac04
d468ac7
08a98df
89803f1
3b250b7
4e1ac04
 
 
3b250b7
 
 
4e1ac04
267fc17
3b250b7
4e1ac04
 
 
 
 
3b250b7
 
8280dca
3b250b7
4e1ac04
3b250b7
4e1ac04
3b250b7
 
8280dca
4e1ac04
3b250b7
8280dca
4e1ac04
 
3b250b7
4e1ac04
 
3b250b7
267fc17
3b250b7
 
4e1ac04
3b250b7
4e1ac04
3b250b7
 
4e1ac04
d468ac7
4e1ac04
3b250b7
8280dca
4e1ac04
3b250b7
8280dca
 
 
 
 
 
 
 
 
 
 
 
 
3b250b7
4e1ac04
d468ac7
4e1ac04
3b250b7
267fc17
4e1ac04
8280dca
4e1ac04
3b250b7
4e1ac04
8280dca
4e1ac04
08a98df
4e1ac04
 
 
 
3b250b7
89803f1
4e1ac04
 
 
 
08a98df
3b250b7
e5dfbda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3642af
 
 
e5dfbda
 
 
 
d3642af
e5dfbda
 
d3642af
 
4e1ac04
3b250b7
 
4e1ac04
 
3b250b7
 
4e1ac04
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
#!/usr/bin/env python3
"""
AgentCTL Gradio UI (Frontend)
Communicates with FastAPI backend instead of Kubernetes directly
"""

import os
import time
import requests
import gradio as gr

# -------------------------------------------------------------------
# Backend URL (FastAPI)
# -------------------------------------------------------------------
BACKEND_URL = os.getenv(
    "AGENTCTL_BACKEND_URL",
    "http://127.0.0.1:9000"  # default for local development
)

print(f"AgentCTL Backend URL: {BACKEND_URL}")

# Helper to join backend URL
def api(path: str) -> str:
    return f"{BACKEND_URL}{path}"


# -------------------------------------------------------------------
# API Client Helpers
# -------------------------------------------------------------------

def generate_yaml(prompt: str, namespace: str, kind: str) -> str:
    """Call FastAPI: POST /api/generate-yaml"""
    try:
        payload = {
            "prompt": prompt,
            "namespace": namespace,
            "kind": kind
        }
        r = requests.post(api("/api/generate-yaml"), json=payload)
        r.raise_for_status()
        return r.json().get("yaml", "# No YAML returned")
    except Exception as e:
        return f"# ERROR: {e}"


def apply_yaml(yaml_text: str) -> tuple[str, str]:
    """Call FastAPI: POST /api/apply"""
    try:
        r = requests.post(api("/api/apply"), json={"yaml": yaml_text})
        r.raise_for_status()
        data = r.json()
        # Backend sends message + raw
        message = data.get("message", "")
        raw = data.get("raw", "")
        return message, raw
    except Exception as e:
        return f"ERROR: {e}", ""

def run_agent(instruction: str, namespace: str, kind: str):
    """Call FastAPI: POST /api/agent"""
    try:
        payload = {
            "instruction": instruction,
            "namespace": namespace,
            "kind": kind
        }
        r = requests.post(api("/api/agent"), json=payload)
        r.raise_for_status()

        data = r.json()

        yaml_text = data.get("yaml", "# No YAML")
        result = data.get("result", {})
        mode = data.get("mode", "unknown")
        model = data.get("model", "none")

        info = f"Mode: {mode}\nModel: {model}"

        return yaml_text, result, info

    except Exception as e:
        return f"# ERROR: {e}", {}, "ERROR"

def format_snapshot(data):
    lines = []

    lines.append(f"Namespace: {data.get('namespace','default')}\n")
    lines.append("==== JOBS ====\n")

    for job in data.get("jobs", []):
        if job["succeeded"] > 0:
            status = "Completed"
        elif job["failed"] > 0:
            status = "Failed"
        elif job["active"] > 0:
            status = "Active"
        else:
            status = "Pending"

        lines.append(
            f"{job['name']}: {status} | Active={job['active']} | Success={job['succeeded']} | Failed={job['failed']}"
        )

    lines.append("\n==== PODS ====\n")

    for pod in data.get("pods", []):
        phase = pod["phase"]
        lines.append(f"{pod['name']}: {phase}")

    lines.append("\n==== CRONJOBS ====\n")

    for cj in data.get("cronjobs", []):
        lines.append(
            f"{cj['name']}: Active={cj['active']} | LastSchedule={cj['last_schedule']}"
        )

    return "\n".join(lines)


def get_logs_once(pod_name: str, tail: int) -> str:
    """Call FastAPI: GET /api/logs"""
    try:
        params = {"pod_name": pod_name, "tail": tail}
        r = requests.get(api("/api/logs"), params=params)
        r.raise_for_status()
        return r.json().get("logs", "No logs received")
    except Exception as e:
        return f"ERROR: {e}"


def follow_logs(pod_name: str, tail: int):
    """Streaming logs (auto-refresh every 2s)"""
    while True:
        html = get_logs_once(pod_name, tail)
        yield html
        time.sleep(2)


# -------------------------------------------------------------------
# Gradio UI
# -------------------------------------------------------------------

def build_ui():
    with gr.Blocks(title="AgentCTL Frontend (FastAPI Backend)") as ui:

        gr.Markdown(f"""
# AgentCTL (Frontend)

Backend URL in use:
**{BACKEND_URL}**

Set `AGENTCTL_BACKEND_URL` to override.
---
""")

        # ----------------------------------------------------------
        # TAB 1 — Create Resource
        # ----------------------------------------------------------
        with gr.Tab("Create Resource"):
            with gr.Row():
                prompt = gr.Textbox(
                    label="Describe your resource",
                    lines=5,
                    placeholder=(
                        "Examples:\n"
                        "- run a python job to preprocess data\n"
                        "- create an nginx deployment with 3 replicas\n"
                        "- schedule a cleanup script every 5 minutes\n"
                    )
                )
                with gr.Column():
                    namespace = gr.Textbox(label="Namespace", value="default")
                    kind = gr.Dropdown(
                        label="Kind",
                        choices=["Auto", "Job", "Deployment", "CronJob"],
                        value="Auto"
                    )

            generate_btn = gr.Button("Generate YAML")
            yaml_box = gr.Code(label="Generated YAML", language="yaml")

            apply_btn = gr.Button("Apply YAML to Cluster")
            apply_status = gr.Textbox(label="Status")
            apply_raw = gr.Textbox(label="Raw API Response")

            generate_btn.click(
                fn=generate_yaml,
                inputs=[prompt, namespace, kind],
                outputs=yaml_box
            )
            apply_btn.click(
                fn=apply_yaml,
                inputs=[yaml_box],
                outputs=[apply_status, apply_raw]
            )

        # ----------------------------------------------------------
        # TAB 2 — Cluster Dashboard
        # ----------------------------------------------------------
        with gr.Tab("Cluster Dashboard"):
            refresh_button = gr.Button("Refresh Snapshot")
            snapshot_box = gr.Textbox(label="Cluster Snapshot", lines=25)

            def get_formatted_snapshot():
                try:
                    r = requests.get(api("/api/snapshot"))
                    r.raise_for_status()
                    data = r.json()
                    return format_snapshot(data)
                except Exception as e:
                    return f"ERROR fetching snapshot: {e}"

            refresh_button.click(
                fn=get_formatted_snapshot,
                outputs=snapshot_box
            )

        # ----------------------------------------------------------
        # TAB 3 — Pod Logs
        # ----------------------------------------------------------
        with gr.Tab("Pod Logs"):
            pod_name = gr.Textbox(label="Pod Name")
            tail_lines = gr.Slider(
                label="Tail Lines", minimum=10, maximum=500, value=100
            )

            get_logs_btn = gr.Button("Get Logs")
            follow_logs_btn = gr.Button("Follow Logs (Live)")
            logs_html = gr.HTML(label="Logs Terminal")

            get_logs_btn.click(
                fn=get_logs_once,
                inputs=[pod_name, tail_lines],
                outputs=logs_html
            )

            follow_logs_btn.click(
                fn=follow_logs,
                inputs=[pod_name, tail_lines],
                outputs=logs_html
            )

        # ----------------------------------------------------------
        # TAB 4 — Agent Mode (LLM → YAML → Apply)
        # ----------------------------------------------------------
        with gr.Tab("Agent Mode"):
            gr.Markdown("""
                ## Agentic LLM Mode
                Enter natural language instructions and let the backend agent:
                1. Plan the Kubernetes operation  
                2. Generate YAML (LLM → fallback → validated)
                3. Apply it to the cluster  
                """)

            instruction = gr.Textbox(
                label="Instruction",
                lines=4,
                placeholder="e.g. deploy a pytorch training job with 1 GPU"
            )

            namespace2 = gr.Textbox(
                label="Namespace",
                value="default"
            )

            kind2 = gr.Dropdown(
                label="Kind Override (optional)",
                choices=["Auto", "Job", "Deployment", "CronJob"],
                value="Auto"
            )

            run_btn = gr.Button("Run Agent")

            agent_yaml = gr.Code(label="Generated YAML", language="yaml")
            agent_result = gr.JSON(label="Apply Result")
            agent_info = gr.Textbox(label="Agent Info (LLM / Fallback)")


            run_btn.click(
                fn=run_agent,
                inputs=[instruction, namespace2, kind2],
                outputs=[agent_yaml, agent_result, agent_info]
            )



    return ui


# Build UI
app = build_ui()

if __name__ == "__main__":
    app.launch(server_name="0.0.0.0", server_port=7860)