# Project export: VibePrompting

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: smarter agentic workflows.
- Devpost: https://devpost.com/software/vibeprompting
- GitHub: https://github.com/anyakath/VibePrompting
- Video: https://www.youtube.com/embed/sepB84w6xk8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — roselyn (39 commits), Aayush Grover (31 commits), Ching Lam Lau (17 commits), anyakath (4 commits)

## Devpost submission (written by the team)

### Inspiration

With bigtech embracing AI agentic models in their workflow and working towards seamless integration with the infrastructure, there can be many errors during this phase. We wanted to work on this very phase of shifting and deploying AI models and also not break production while doing it. There are several cases wherein agents do not perform to their full potential due to reasons completely unrelated to the design or deployment but due to improper prompting. Even though it may seem like debugging these cases can be straightforward, our team has experienced designing near perfect workflows (of course from a human standpoint) and can still have erratic results due to semantic differences and language processing errors. That is how we thought of VibePrompting. We aim to automate the entire phase of designing agentic workflows.

### What it does

VibePrompting is a dashboard which takes as input the AI model that the user is working on, and works on refining the workflow to help the AI model perform better. Our product has the following features: Real time AI agent refinement: On uploading the agent to the platform the user can see the workflow and input commands to customise the agent without the additional burden of overcoming precise prompting. Enabling editing different configs of the agent: Our platform allows the user to revise specific part of the field of the json config of the agent thereby giving the user more control and flexibility and mimicking the experience of working on the json itself rather than a black box intermediary which is not the best experience for devs. Version control and activity logs: We enabled version control features like branching and building off from different branches so that the dev can easily test out models made from the different branches rather than struggle with figuring the most optimised prompts which give the best results. The platform even has a log history highlighting the different changes made for better UI. Reinforcement learning with workflow performance metrics: We used RL to analyse the performance of different agents designed by the user and automatically improve the agentic prompts under the hood.

### How we built it

We built a robust frontend using Next.js, React, and TypeScript. The responsive frontend features an interactive graph for managing prompts, an AI-powered interface for modifying the prompts, and a JSON editor. The interface is styled with TailwindCSS and uses Framer Motion for smooth animations. The backend server is built with Flask and Python, which is designed to handle the infrastructure logic, version control, and branching for agent workflows. The backend provides API endpoints that integrate with Gemini API and custom Gemini agents to refine prompts and apply reinforcement learning based on feedback loops to improve user prompts. It features a versioning system with branching and history logs, allowing users to track and manage different agent versions. The server seamlessly interacts with Google ADK, enabling users to run tests, refine their agents, and switch between the dashboard and ADK for custom evaluations.

### Challenges we ran into

Managing complex state: One of the major challenges was creating our own version control software for tracking changes. This meant maintaining history of the different changes in the activity log. We needed to work on maintaining the different states of the agentic workflows as well as a way to quickly and seamlessly transition between them. The logic behind this took a lot of time to streamline and develop. Integration with google ADK: While working on this platform there were a lot of design considerations in terms of the dev kit to be used and how we could integrate it into our platform. One of the errors we faces was triggering the google adk every time the user changes the workflow.

### Accomplishments we're proud of

Finishing our project in 24 hours 🥳 Integrating reinforcement learning with real-time prompt refinement Implementing our own version control system

### What's next

The next focus for Vibe Prompting is to step out of the MVP and scale the infrastructure to enable traffic and deployment. Currently, our platform allows agent development in the Google ADK framework but we will be working on compatibility with more frameworks. We will also be working on making the version control more seamless and improve the state changes to make them faster.

## README (from the GitHub repository)

# VibePrompting for AI Agents

Transform your Google ADK agents with intelligent prompting and dynamic conversation flows.

## Features

- **AI Agent Enhancement**: Upload your Google ADK agent and enhance it with intelligent prompting capabilities
- **Dynamic Conversations**: Create branching conversation flows and dynamic agent responses
- **Real-time Processing**: Process and modify your agents in real-time with instant feedback
- **File Upload**: Upload ZIP files containing Google ADK agent folders for processing

## Getting Started

### Prerequisites

- Python 3.8+
- Node.js 18+
- Google ADK (Agent Development Kit)

### Installation

1. Clone the repository:

```bash
git clone <repository-url>
cd BerkeleyAI
```

2. Install backend dependencies:

```bash
pip install -r requirements.txt
```

3. Install frontend dependencies:

```bash
cd frontend
npm install
```

### Running the Application

1. Start the Flask backend (from the project root):

```bash
python app.py
```

2. Start the Next.js frontend (from the frontend folder):

```bash
cd frontend
npm run dev
```

3. Open your browser and navigate to `http://localhost:3000`

## Usage

1. **Landing Page**: Visit the root URL to see the VibePrompting landing page
2. **Upload Agent**: Upload a ZIP file containing your Google ADK agent folder
3. **Process Agent**: Use the train interface to enhance your agent with intelligent prompting
4. **Navigate**: Use the navigation to switch between the landing page and the main application

## File Upload

The application accepts ZIP files containing Google ADK agent folders. The upload process:

1. Validates the file is a ZIP archive
2. Extracts the contents to a unique directory
3. Identifies agent configuration files
4. Stores the upload information for processing

## API Endpoints

- `POST /upload_agent` - Upload Google ADK agent ZIP file
- `POST /process_json/general/<session_id>/<node_id>` - Process JSON
- `GET /history/<session_id>/<node_id>` - Retrieve processing history
- `POST /retrigger_adk_web` - Restart ADK web server

## Project Structure

```
BerkeleyAI/
├── app.py                      # Flask backend server
├── prompt.py                   # Prompt processing logic
├── requirements.txt            # Python dependencies
├── uploads/                    # Uploaded agent files
├── history/                    # Processing history (per session)
├── hotels_com_api_agent/       # Example agent implementation
│   ├── __init__.py
│   ├── agent.py
│   ├── agent.json
│   └── tools/
│       ├── get_hotel_details_endpoint.py
│       ├── search_hotel_destination_endpoint.py
│       └── search_hotels_endpoint.py
├── frontend/                   # Next.js frontend
│   ├── app/                    # App router pages and layout
│   │   ├── layout.tsx
│   │   ├── globals.css
│   │   ├── page.tsx
│   │   ├── landing.tsx
│   │   ├── History.tsx
│   │   ├── AgentEditor.tsx
│   │   ├── ChatInput.tsx
│   │   ├── Logs.tsx
│   │   └── train/
│   ├── components/             # UI components
│   │   └── ui/
│   │       ├── button.tsx
│   │       ├── input.tsx
│   │       ├── scroll-area.tsx
│   │       └── select.tsx
│   ├── lib/                    # Utilities and types
│   │   ├── agent.json
│   │   ├── types.ts
│   │   └── utils.ts
│   ├── public/                 # Static assets (if any)
│   ├── package.json            # Frontend dependencies
│   ├── tsconfig.json           # TypeScript config
│   └── ... (other config files)
├── __init__.py
├── adk                         # (ADK integration stub)
└── .venv/                      # (optional, for Python virtual environment)
```

## Technologies Used

- **Backend**: Flask, Python
- **Frontend**: Next.js 15, React 19, TypeScript
- **UI**: Tailwind CSS, Radix UI
- **Agent Framework**: Google ADK

---

For more details, see the `frontend/README.md` for frontend-specific development and customization notes.


## Detected evidence (automated analysis)

Indexed codebase: 27 recognized source files, 114 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (41 of 41)

```
__init__.py
.DS_Store
.gitignore
adk
app.py
frontend/.gitignore
frontend/.nvmrc
frontend/app/AgentEditor.tsx
frontend/app/app/page.tsx
frontend/app/ChatInput.tsx
frontend/app/globals.css
frontend/app/History.tsx
frontend/app/landing.tsx
frontend/app/layout.tsx
frontend/app/Logs.tsx
frontend/app/page.tsx
frontend/app/train/page.tsx
frontend/components.json
frontend/components/ui/button.tsx
frontend/components/ui/input.tsx
frontend/components/ui/scroll-area.tsx
frontend/components/ui/select.tsx
frontend/eslint.config.mjs
frontend/lib/agent.json
frontend/lib/types.ts
frontend/lib/utils.ts
frontend/next.config.ts
frontend/package.json
frontend/postcss.config.mjs
frontend/README.md
frontend/tsconfig.json
hotels_com_api_agent/__init__.py
hotels_com_api_agent/.env
hotels_com_api_agent/agent.json
hotels_com_api_agent/agent.py
hotels_com_api_agent/tools/get_hotel_details_endpoint.py
hotels_com_api_agent/tools/search_hotel_destination_endpoint.py
hotels_com_api_agent/tools/search_hotels_endpoint.py
prompt.py
README.md
requirements.txt
```

### Dependencies

- frontend/package.json: @eslint/eslintrc@^3, @radix-ui/react-scroll-area@^1.2.9, @radix-ui/react-select@^2.2.5, @radix-ui/react-slot@^1.2.3, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@15.3.4, framer-motion@^12.18.1, lucide-react@^0.522.0, next@15.3.4, react@^19.0.0, react-d3-tree@^3.6.6, react-dom@^19.0.0, react-resizable-panels@^3.0.3, tailwind-merge@^3.3.1, tailwindcss@^4, tw-animate-css@^1.3.4, typescript@^5
- requirements.txt: annotated-types@==0.7.0, anyio@==4.9.0, blinker@==1.9.0, cachetools@==5.5.2, certifi@==2025.6.15, charset-normalizer@==3.4.2, click@==8.2.1, colorama@==0.4.6, Flask@==3.1.1, flask_cors@==6.0.1, google-auth@==2.40.3, google-genai@==1.21.1, h11@==0.16.0, httpcore@==1.0.9, httpx@==0.28.1, idna@==3.10, itsdangerous@==2.2.0, Jinja2@==3.1.6, MarkupSafe@==3.0.2, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, pydantic@==2.11.7, pydantic_core@==2.33.2, python-dotenv@==1.1.0, requests@==2.32.4, rsa@==4.9.1, sniffio@==1.3.1, tenacity@==8.5.0, typing_extensions@==4.14.0, typing-inspection@==0.4.1, urllib3@==2.5.0, websockets@==15.0.1, Werkzeug@==3.1.3

### Recent commits (newest first)

- update readme
- Fixed 4 to 3
- slay
- slay
- Merge branch 'rozlyn'
- favicon????idk
- CHANGE FONT
- Merge pull request #8 from anyakath/rozlyn
- Merge branch 'main' into rozlyn
- RAHHHHHHHHHSHDFKJSGNFDKGN
- new train your agent FE
- merge
- made train your agent thing prettier
- refresh server yippee
- Merge branch 'main' into rozlyn
- idk what im committing
- FIX MY SUMMARIZE NAME NODE
- Merge branch 'main' into rozlyn
- Merge branch 'main' of https://github.com/anyakath/BerkeleyAI
- changed styling and adding training frontend stuff

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
annotated-types==0.7.0
anyio==4.9.0
blinker==1.9.0
cachetools==5.5.2
certifi==2025.6.15
charset-normalizer==3.4.2
click==8.2.1
colorama==0.4.6
Flask==3.1.1
flask_cors==6.0.1
google-auth==2.40.3
google-genai==1.21.1
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.2
pyasn1==0.6.1
pyasn1_modules==0.4.2
pydantic==2.11.7
pydantic_core==2.33.2
python-dotenv==1.1.0
requests==2.32.4
rsa==4.9.1
sniffio==1.3.1
tenacity==8.5.0
typing-inspection==0.4.1
typing_extensions==4.14.0
urllib3==2.5.0
websockets==15.0.1
Werkzeug==3.1.3

```

### frontend/package.json

```
{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@radix-ui/react-scroll-area": "^1.2.9",
    "@radix-ui/react-select": "^2.2.5",
    "@radix-ui/react-slot": "^1.2.3",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "framer-motion": "^12.18.1",
    "lucide-react": "^0.522.0",
    "next": "15.3.4",
    "react": "^19.0.0",
    "react-d3-tree": "^3.6.6",
    "react-dom": "^19.0.0",
    "react-resizable-panels": "^3.0.3",
    "tailwind-merge": "^3.3.1"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.3.4",
    "tailwindcss": "^4",
    "tw-animate-css": "^1.3.4",
    "typescript": "^5"
  }
}

```

### app.py

```python
from flask import Flask, request, jsonify
from flask_cors import CORS
import json
import os
import subprocess # For running shell commands
import platform   # For detecting the operating system
import time       # For small delays
import zipfile    # For handling ZIP files
import shutil     # For file operations
import uuid

from prompt import get_new_json_single_edit, get_new_json_general, summarize_changes, rl_prompt

app = Flask(__name__)
CORS(app)  # Enable CORS for all routes

# Create uploads directory if it doesn't exist
UPLOADS_DIR = "uploads"
if not os.path.exists(UPLOADS_DIR):
    os.makedirs(UPLOADS_DIR)

@app.after_request
def after_request(response):
    response.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000')
    response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
    response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
    response.headers.add('Access-Control-Allow-Credentials', 'true')
    return response

# --- Upload Agent ZIP File Endpoint ---
@app.route('/upload_agent', methods=['POST'])
def upload_agent():
    if 'agent_zip' not in request.files:
        return jsonify({"error": "No ZIP file part in the request"}), 400

    zip_file = request.files['agent_zip']

    if zip_file.filename == '':
        return jsonify({"error": "No selected ZIP file"}), 400

    if not zip_file.filename.endswith('.zip'):
        return jsonify({"error": "File must be a ZIP archive"}), 400

    try:
        # Create a unique directory name for this upload in root directory
        upload_id = str(uuid.uuid4())
        upload_path = os.path.join(os.getcwd(), upload_id)

        # Delete existing folder if it exists
        if os.path.exists(upload_path):
            shutil.rmtree(upload_path)

        # Create the directory
        os.makedirs(upload_path, exist_ok=True)

        # Save the ZIP file temporarily
        zip_path = os.path.join(upload_path, zip_file.filename)
        zip_file.save(zip_path)

        # Extract the ZIP file
        with zipfile.ZipFile(zip_path, 'r') as zip_ref:
            zip_ref.extractall(upload_path)

        # Remove the ZIP file after extraction
        os.remove(zip_path)

        # Look for agent.json or similar files in the extracted content
        agent_files = []
        for root, dirs, files in os.walk(upload_path):
            for file in files:
                if file.endswith('.json') and ('agent' in file.lower() or 'config' in file.lower()):
                    agent_files.append(os.path.join(root, file))

        return jsonify({
            "success": True,
            "message": f"Agent uploaded successfully: {zip_file.filename}",
            "upload_id": upload_id,
            "agent_files": agent_files,
            "extracted_path": upload_path
        }), 200

    except zipfile.BadZipFile:
        return jsonify({"error": "Invalid ZIP file format"}), 400
    except Exception as e:
        return jsonify({"error": f"An error occurred during upload: {str(e)}"}), 500

# --- Configuration for ADK Web Server ---
ADK_WEB_PORT = 8000  # Default port for 'adk web'. Adjust if yours is different.

def call_single_edit_agent(input_json_data, prompt, param='root_agent'):
    """
    This function simulates your custom Gemini agent processing.
    In a real application, you would replace this with your
    actual Gemini agent's interaction, which might involve:
    - Calling a Gemini API
    - Using a local ML model
    - Applying business rules based on the prompt

    Args:
        input_json_data (dict): The parsed JSON data from the input file.
        prompt (str): The prompt provided by the user.

    Returns:
        tuple: (updated_json_data (dict), context_of_changes (str))
    """
    updated_json_data = input_json_data.copy()
    context_of_changes = get_new_json_single_edit(input_json_data, param, prompt)
    # TODO: need to take in param to edit

    # Example: Modify JSON based on a simple prompt
    if "add_timestamp" in prompt.lower():
        import datetime
        updated_json_data["last_updated"] = datetime.datetime.now().isoformat()
        context_of_changes += "- Added 'last_updated' timestamp.\n"

    if "change_status_to_processed" in prompt.lower():
        if "status" in updated_json_data:
            updated_json_data["status"] = "processed"
            context_of_changes += "- Changed 'status' to 'processed'.\n"
        else:
            updated_json_data["status"] = "newly_processed"
            context_of_changes += "- Added 'status' as 'newly_processed'.\n"

    if "add_notes" in prompt.lower() and "notes_content" in prompt.lower():
        # Extract notes content from prompt (a more robust solution would use regex or a more structured prompt)
        try:
            notes_start = prompt.find("notes_content:") + len("notes_content:")
            notes_end = prompt.find("'", notes_start) # Assuming notes_content ends with a single quote for simplicity
            if notes_end == -1: # if no closing quote found, take till end
                notes_content = prompt[notes_start:].strip()
            else:
                notes_content = prompt[notes_start:notes_end].strip()
            updated_json_data["notes"] = notes_content
            context_of_changes += f"- Added notes: '{notes_content}'.\n"
        except Exception as e:
            context_of_changes += f"- Failed to add notes due to error: {e}.\n"


    if not context_of_changes:
        context_of_changes = "No specific changes requested or applied based on the prompt."

    return updated_json_data, context_of_changes

def call_general_agent(input_json_data, prompt):
    updated_json_str, changelog = get_new_json_general(input_json_data, prompt)
    updated_json_data = json.loads(updated_json_str)
    context_of_changes = changelog
    return updated_json_data, context_of_changes


# --- Helper Function to find and kill processes on a port ---
def _kill_process_on_port(port)
[truncated — 13196 more characters]
```

### frontend/app/page.tsx

```typescript
import LandingPage from "@/app/landing";

export default function Home() {
  return <LandingPage />;
}

```

### frontend/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Funnel_Display } from "next/font/google";
import "./globals.css";

const funnelDisplay = Funnel_Display({
  variable: "--font-funnel-display",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "VibePrompting - AI Agent Enhancement",
  description:
    "Transform your Google ADK agents with intelligent prompting and dynamic conversation flows",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${funnelDisplay.variable} antialiased`}>
        {children}
      </body>
    </html>
  );
}

```

### frontend/app/app/page.tsx

```typescript
"use client";

import React, { useState, useRef } from "react";
import Logs from "@/app/Logs";
import History from "@/app/History";
import { Message, OrgChartNode } from "@/lib/types";
import AgentEditor from "@/app/AgentEditor";
import { Button } from "@/components/ui/button";
import { addChildToNodeByName, findNodeByName, generateNodeId } from "@/lib/utils";
import ChatInput from "@/app/ChatInput";
import {
  Panel,
  PanelGroup,
  PanelResizeHandle,
  ImperativePanelHandle,
} from "react-resizable-panels";
import AgentContent from "@/lib/agent.json";
import { Home, Sparkles } from "lucide-react";

export default function AppPage() {
  const [orgChart, setOrgChart] = useState<OrgChartNode>({
    id: "root_node",
    name: "Booking Agent V1",
    children: [],
    jsonData: AgentContent,
  });

  const [selectedNode, setSelectedNode] = useState<string>("Booking Agent V1");
  const [isLogsOpen, setIsLogsOpen] = useState(true);
  const [messages, setMessages] = useState<Message[]>([]);
  const [isProcessing, setIsProcessing] = useState(false);
  const logsPanelRef = useRef<ImperativePanelHandle>(null);

  const expandLogs = () => {
    logsPanelRef.current?.expand();
  };

  const collapseLogs = () => {
    logsPanelRef.current?.collapse();
  };

  const handleAddNode = async (inputValue: string) => {
    if (!inputValue.trim()) return;

    setIsProcessing(true);

    // Find the selected node to get its JSON data
    const selectedNodeData = findNodeByName(orgChart, selectedNode);
    if (!selectedNodeData) {
      console.error("Selected node not found");
      setIsProcessing(false);
      return;
    }

    // Use the selected node's JSON data as the base, or fall back to AgentContent
    const baseJsonData = selectedNodeData.jsonData || AgentContent;

    // Create a Blob from the JSON data
    const blob = new Blob([JSON.stringify(baseJsonData)], {
      type: "application/json",
    });

    // Create FormData for file upload
    const formData = new FormData();
    formData.append("json_file", blob, "agent.json");
    formData.append("prompt", inputValue.trim());

    try {
      // Make API call to Flask backend with file upload
      const response = await fetch("http://localhost:5000/process_json/1", {
        method: "POST",
        body: formData,
      });

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.error || "Failed to process prompt");
      }

      // Generate unique ID for the new node
      const newNodeId = generateNodeId();

      // Check if this is the first child before updating the chart
      const targetNode = findNodeByName(orgChart, selectedNode);
      const isFirstChild = targetNode && targetNode.children.length === 0;

      // Add a message for the successful API call
      const messageContent = isFirstChild
        ? `Child node "${inputValue.trim()}" added with ID: ${newNodeId}`
        : `Branch created and child node "${inputValue.trim()}" added with ID: ${newNodeId}`;

      setMessages((prev) => [
        ...prev,
        {
          id: Date.now().toString(),
          content: messageContent,
          sender: "system",
        },
      ]);

      // Store the generated JSON data from the API response
      const generatedJsonData = result.json_data || result;

      setOrgChart((prevChart) => {
        return addChildToNodeByName(
          prevChart, 
          selectedNode, 
          inputValue.trim(), 
          newNodeId,
          undefined,
          generatedJsonData
        );
      });

      setSelectedNode(inputValue.trim());
    } catch (error) {
      console.error("Error processing prompt:", error);

      // Add error message to logs
      setMessages((prev) => [
        ...prev,
        {
          id: Date.now().toString(),
          content: `Error processing prompt: ${
            error instanceof Error ? error.message : "Unknown error"
          }`,
          sender: "system",
        },
      ]);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <div className="h-screen w-full relative bg-background">
      {/* Navigation Header */}
      <div className="absolute top-0 left-0 right-0 z-20 bg-background/80 backdrop-blur-sm border-b border-border">
        <div className="flex items-center justify-between px-4 py-2">
          <div className="flex items-center space-x-2">
            <Sparkles className="w-6 h-6" />
            <span className="font-semibold text-lg">VibePrompting</span>
          </div>
          <Button
            variant="ghost"
            size="sm"
            onClick={() => window.location.href = '/'}
            className="flex items-center space-x-0"
          >
            <Home className="w-4 h-4" />
            <span>Home</span>
          </Button>
        </div>
      </div>

      {/* Main Content with top padding for header */}
      <div className="pt-12 h-full">
        <PanelGroup direction="horizontal" className="h-full">
          <Panel>
            <PanelGroup direction="vertical" className="h-full">
              <Panel>
                <div className="overflow-auto h-full scrollbar-thin">
                  <History
                    orgChart={orgChart}
                    selectedNode={selectedNode}
                    setSelectedNode={setSelectedNode}
                    isLogsOpen={isLogsOpen}
                  />
                </div>
              </Panel>
              <div>
                <ChatInput
                  selectedNode={selectedNode}
                  onSendMessage={handleAddNode}
                  isProcessing={isProcessing}
                />
              </div>
              <PanelResizeHandle className="h-1 bg-border hover:bg-ring/50 transition-colors duration-200 group">
                <div className="w-8 h-1 bg-muted-foreground/20 rounded-full mx-auto group-hover:bg-muted-foreground/40 transition-colors duration-200" />
              </PanelResizeHandle>
              <Panel d
[truncated — 1635 more characters]
```

### frontend/app/train/page.tsx

```typescript
"use client";

import React, { useState, useRef, useEffect } from "react";
import Logs from "@/app/Logs";
import History from "@/app/History";
import { Message, OrgChartNode } from "@/lib/types";
import AgentEditor from "@/app/AgentEditor";
import { Button } from "@/components/ui/button";
import { addChildToNodeByName, findNodeByName } from "@/lib/utils";
import ChatInput from "@/app/ChatInput";
import {
  Panel,
  PanelGroup,
  PanelResizeHandle,
  ImperativePanelHandle,
} from "react-resizable-panels";
import AgentContent from "@/lib/agent.json";
import { Home, Sparkles } from "lucide-react";
import { motion } from "framer-motion";

export default function AppPage() {
  const [orgChart, setOrgChart] = useState<OrgChartNode>({
    id: "root_node",
    name: "Booking Agent V1",
    children: [],
    jsonData: AgentContent,
  });

  const [selectedNode, setSelectedNode] = useState<string>("Booking Agent V1");
  const [isLogsOpen, setIsLogsOpen] = useState(true);
  const [messages, setMessages] = useState<Message[]>([]);
  const [isProcessing, setIsProcessing] = useState(false);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [nodeCounter, setNodeCounter] = useState<number>(1); // Start at 1 for first node
  const [selectedNodeJson, setSelectedNodeJson] =
    useState<object>(AgentContent);
  const logsPanelRef = useRef<ImperativePanelHandle>(null);
  const [showOverlay, setShowOverlay] = useState(true);

  // On mount, create a new session
  useEffect(() => {
    const createSession = async () => {
      const res = await fetch("http://localhost:5000/new_session", {
        method: "POST",
      });
      const data = await res.json();
      setSessionId(data.session_id);
    };
    createSession();
  }, []);

  // Fetch JSON for selected node when it changes
  useEffect(() => {
    if (!sessionId) return;
    const nodeData = findNodeByName(orgChart, selectedNode);
    if (!nodeData || !nodeData.id) return;

    // Don't fetch for root node
    if (nodeData.id === "root_node") {
      setSelectedNodeJson(AgentContent);
      // Update agent.json with root content
      updateAgentJsonFile(AgentContent);
      return;
    }

    fetch(`http://localhost:5000/history/${sessionId}/${nodeData.id}`)
      .then((res) => res.json())
      .then((data) => {
        setSelectedNodeJson(data);
        // Update agent.json with the fetched data
        updateAgentJsonFile(data);
      })
      .catch(() => {
        setSelectedNodeJson({ error: "Failed to load JSON" });
      });
  }, [selectedNode, sessionId, orgChart]);

  // Function to update the agent.json file
  const updateAgentJsonFile = async (jsonData: any) => {
    try {
      await fetch("http://localhost:5000/update_agent_json", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ json_data: jsonData }),
      });
    } catch (error) {
      console.error("Error updating agent.json file:", error);
    }
  };

  useEffect(() => {
    if (showOverlay) {
      const timeout = setTimeout(() => setShowOverlay(false), 600);
      return () => clearTimeout(timeout);
    }
  }, [showOverlay]);

  const expandLogs = () => {
    logsPanelRef.current?.expand();
  };

  const collapseLogs = () => {
    logsPanelRef.current?.collapse();
  };

  const handleAddNode = async (inputValue: string) => {
    if (!inputValue.trim() || !sessionId) return;
    setIsProcessing(true);
    const selectedNodeData = findNodeByName(orgChart, selectedNode);
    if (!selectedNodeData) {
      setIsProcessing(false);
      return;
    }
    const baseJsonData = selectedNodeData.jsonData || AgentContent;
    const blob = new Blob([JSON.stringify(baseJsonData)], {
      type: "application/json",
    });
    const formData = new FormData();
    formData.append("json_file", blob, "agent.json");
    formData.append("prompt", inputValue.trim());
    const newNodeId = nodeCounter.toString();
    try {
      const response = await fetch(
        `http://localhost:5000/process_json/general/${sessionId}/${newNodeId}`,
        {
          method: "POST",
          body: formData,
        }
      );
      const result = await response.json();
      if (!response.ok)
        throw new Error(result.error || "Failed to process prompt");
      setOrgChart((prevChart) => {
        return addChildToNodeByName(
          prevChart,
          selectedNode,
          result.node_name || inputValue.trim(),
          newNodeId,
          undefined,
          result.updated_json ? JSON.parse(result.updated_json) : result
        );
      });
      setMessages((prev) => [
        ...prev,
        {
          id: Date.now().toString(),
          content: `Node \"${
            result.node_name || inputValue.trim()
          }\" added with ID: ${newNodeId}`,
          sender: "system",
        },
      ]);
      setSelectedNode(result.node_name || inputValue.trim());
      setNodeCounter((n) => n + 1);
    } catch (error) {
      setMessages((prev) => [
        ...prev,
        {
          id: Date.now().toString(),
          content: `Error processing prompt: ${
            error instanceof Error ? error.message : "Unknown error"
          }`,
          sender: "system",
        },
      ]);
    } finally {
      setIsProcessing(false);
    }
  };

  return (
    <div className="h-screen w-full relative bg-background">
      {/* Fade-in white overlay */}
      {showOverlay && (
        <motion.div
          className="fixed inset-0 z-50 bg-white"
          initial={{ opacity: 1 }}
          animate={{ opacity: 0 }}
          transition={{ duration: 0.5 }}
        />
      )}

      {/* Navigation Header */}
      <div className="absolute top-0 left-0 right-0 z-20 bg-background/80 backdrop-blur-sm border-b border-border">
        {/* Gradient border accent */}
        <div className="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-green-500/40 via-blue-500/40 to-transparent" />
    
[truncated — 5134 more characters]
```

### __init__.py

```python
from . import agent
```

### prompt.py

```python
from google import genai
from dotenv import load_dotenv
import json

load_dotenv()

GEMINI_API_KEY="AIzaSyAQnxbGCFk1tlNWJy31g3g4ed7kaYl7ryE"

client = genai.Client(api_key=GEMINI_API_KEY)

def get_response(prompt):
    return client.models.generate_content(
        model="gemini-2.0-flash", contents=prompt
    ).text

# Note: I modified this prompt to not use single quotation marks, but since it only edits part of the JSON, it might screw up
def generate_prompt_single_edit(json, param, instruction):
    prompt = f"""
    You are an expert prompt engineer. You are given a JSON file that defines prompts for an AI agent and its tools. Read the JSON file carefully to understand what the AI agent is supposed to achieve. This is the JSON file:

    {json}

    Your task is to improve the workflow of this AI agent by editing the value of "{param}", following this instruction from the user: {instruction}.

    Guidelines:

    Do not modify any other lines, spacing, indentation, or trailing commas in the JSON.

    Do not use single quotation marks or apostrophes. Only use double quotation marks.

    Do not reorder keys or change formatting.

    Do not reformat the JSON.

    Return the entire JSON with only that single change applied.
    """

    return prompt


def generate_prompt_general_with_changelog(json, instruction):
    prompt = f"""
    You are an expert prompt engineer. You are given a JSON file that defines prompts for an AI agent and its tools. Read the JSON file carefully to understand what the AI agent is supposed to achieve. This is the JSON file:

    {json}

    Your task is to improve the workflow of this AI agent by editing the JSON file, following this instruction from the user: {instruction}.

    Guidelines:
    - No matter how absurd or unrelated, follow the prompt. Every prompt should result in a change.
    - Do not modify spacing, indentation, or trailing commas in the JSON.
    - Do not use single quotation marks or apostrophes. Only use double quotation marks.
    - Do not reorder keys or change formatting.
    - Do not add or remove any fields.
    - Do not reformat the JSON.

    After making the change, return:
    The entire new JSON in a code block (use ```json ... ```)
    """
    return prompt

def get_new_json_single_edit(input_json, param, instruction):
    prompt = generate_prompt_single_edit(str(input_json), param, instruction)
    response = get_response(prompt)

    # don't use first and last line, which contains ```json and ```
    response_lines = response.split('\n')[1:-1]
    if "```" in response_lines[0]:
        response_lines = response_lines[1:]

    final_response = '\n'.join(response_lines)
    return final_response

def get_new_json_general(input_json, instruction):
    prompt = generate_prompt_general_with_changelog(str(input_json), instruction)
    response = get_response(prompt)

    # Split response into JSON and changelog
    json_part = None
    changelog = None
    lines = response.split('\n')
    in_json = False
    json_lines = []

    for line in lines:
        if line.strip().startswith('```json'):
            in_json = True
            continue
        if in_json and line.strip().startswith('```'):
            in_json = False
            continue
        if in_json:
            json_lines.append(line)
        if line.strip().startswith('CHANGELOG:'):
            changelog = line.strip().replace('CHANGELOG:', '').strip()
    json_part = '\n'.join(json_lines)

    return json_part, changelog

def generate_summarize_prompt(changelog, max_words=6):
    return f"""
    You are an expert at summarizing changelogs. Given the following changelog, summarize the change in {max_words} words or less. The summary should be concise, clear, and describe exactly what was changed. Do not use filler words. Do not use punctuation unless necessary. Do not use single quotation marks or apostrophes. Only use double quotation marks. Do not include the word 'changelog' or any explanation. Only output the summary phrase, nothing else.

    Changelog:
    {changelog}
    """


def summarize_changes(prompt, context_of_changes=None):
    """
    Returns a short summary of the change for use as a node name.
    If context_of_changes is provided, use it; otherwise, use the prompt.
    Uses Gemini LLM to summarize to 6 words or less.
    """
    summary_source = context_of_changes if (context_of_changes and isinstance(context_of_changes, str)) else prompt

    try:
        summarize_prompt = generate_summarize_prompt(summary_source, max_words=6)
        summary = get_response(summarize_prompt).strip()
        # Only take the first line and trim to 6 words max (in case LLM over-responds)
        summary = ' '.join(summary.splitlines()[0].split()[:6])
        if summary:
            return summary
    except Exception as e:
        print(f"LLM summarization failed: {e}")
    # Fallback: use the first 6 words of the first non-empty line
    for line in summary_source.splitlines():
        line = line.strip('- ').strip()
        if line:
            return ' '.join(line.split()[:6])
    return ' '.join(prompt.strip().split()[:6])


def rl_prompt(query, output):
    return f"""
    Given the following query: {query}, the agent produced the following output: {output}.
    Given this information, iterate upon the system prompts of the agent (the steps that the agent takes), to improve the output for this given use-case. Try to keep changes relatively minimal.
    """

```

### hotels_com_api_agent/__init__.py

```python
from . import agent
```

[17 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]