├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── discord-gfi.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
└── readme.png
├── deployment
├── README.md
├── engine
│ ├── Dockerfile
│ └── entrypoint.sh
├── install.sh
├── install.yaml
├── kubernetes-controller
│ └── Dockerfile
├── local.docker-compose.yaml
├── local.install.yaml
├── local.kind.yaml
├── local.test-deploy.yaml
├── mcp
│ ├── Dockerfile
│ └── entrypoint.sh
└── ui
│ ├── Dockerfile
│ ├── nginx.conf
│ └── proxy.Dockerfile
├── docs
├── architecture.md
└── install.md
├── engine
├── .env.example
├── .gitignore
├── .python-version
├── README.md
├── aerich.ini
├── alembic.ini
├── migrations
│ └── models
│ │ └── 0_20250407154142_init.py
├── pyproject.toml
├── src
│ ├── __init__.py
│ └── api
│ │ ├── __about__.py
│ │ ├── __init__.py
│ │ ├── asgi.py
│ │ ├── config
│ │ ├── __init__.py
│ │ └── settings.py
│ │ ├── domain
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ ├── conversation.py
│ │ │ └── user.py
│ │ └── schemas
│ │ │ └── team.py
│ │ ├── llm_schemas.py
│ │ ├── repositories
│ │ ├── __init__.py
│ │ └── database.py
│ │ ├── services
│ │ ├── auth.py
│ │ ├── limiter.py
│ │ ├── llm_client.py
│ │ ├── mcp_client.py
│ │ └── rbac.py
│ │ ├── utils
│ │ ├── __init__.py
│ │ └── helpers.py
│ │ ├── web
│ │ ├── dependencies
│ │ │ ├── __init__.py
│ │ │ └── rate_limit.py
│ │ ├── endpoints
│ │ │ ├── __init__.py
│ │ │ ├── auth.py
│ │ │ ├── chat.py
│ │ │ ├── health.py
│ │ │ ├── team.py
│ │ │ └── ws.py
│ │ └── middleware
│ │ │ ├── __init__.py
│ │ │ └── logging_middleware.py
│ │ └── workflow
│ │ ├── __init__.py
│ │ ├── agents
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── executor
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── prompt_templates.py
│ │ │ └── types.py
│ │ ├── planner
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── prompt_templates.py
│ │ │ └── types.py
│ │ └── verifier
│ │ │ ├── __init__.py
│ │ │ ├── main.py
│ │ │ ├── prompt_templates.py
│ │ │ └── types.py
│ │ └── manager.py
└── uv.lock
├── kubernetes-controller
├── .gitignore
├── README.md
├── cmd
│ └── manager
│ │ └── main.go
├── config
│ └── rbac
│ │ └── role.yaml
├── controllers
│ └── skyfloai_controller.go
├── engine
│ └── v1
│ │ ├── groupversion_info.go
│ │ ├── skyfloai_types.go
│ │ └── zz_generated.deepcopy.go
├── go.mod
├── go.sum
└── hack
│ └── boilerplate.go.txt
├── mcp
├── .env.example
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
├── src
│ └── mcp_server
│ │ ├── __about__.py
│ │ ├── __init__.py
│ │ ├── api
│ │ ├── __init__.py
│ │ └── v1
│ │ │ ├── __init__.py
│ │ │ ├── health.py
│ │ │ └── tools
│ │ │ └── __init__.py
│ │ ├── asgi.py
│ │ ├── config
│ │ ├── __init__.py
│ │ └── settings.py
│ │ ├── tools
│ │ ├── __init__.py
│ │ ├── _utils.py
│ │ ├── argo
│ │ │ ├── __init__.py
│ │ │ ├── _argo_rollouts.py
│ │ │ └── tool.py
│ │ ├── common
│ │ │ ├── __init__.py
│ │ │ ├── mcp.py
│ │ │ └── models.py
│ │ ├── helm
│ │ │ ├── __init__.py
│ │ │ ├── _helm.py
│ │ │ └── tool.py
│ │ ├── k8s
│ │ │ ├── __init__.py
│ │ │ ├── _kubectl.py
│ │ │ └── tool.py
│ │ ├── permissions.py
│ │ ├── registry.py
│ │ └── utils
│ │ │ ├── __init__.py
│ │ │ └── helpers.py
│ │ └── utils
│ │ ├── __init__.py
│ │ └── retry.py
└── uv.lock
├── package.json
├── ui
├── .env.example
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── next.config.mjs
├── package.json
├── postcss.config.mjs
├── public
│ ├── logo_transparent.png
│ └── logo_vector_transparent.png
├── src
│ ├── app
│ │ ├── api
│ │ │ ├── auth
│ │ │ │ ├── admin-check
│ │ │ │ │ └── route.ts
│ │ │ │ └── me
│ │ │ │ │ └── route.ts
│ │ │ ├── conversation
│ │ │ │ └── [id]
│ │ │ │ │ └── route.ts
│ │ │ ├── history
│ │ │ │ └── route.ts
│ │ │ ├── profile
│ │ │ │ └── route.ts
│ │ │ └── team
│ │ │ │ └── route.ts
│ │ ├── chat
│ │ │ └── [id]
│ │ │ │ └── page.tsx
│ │ ├── favicon.ico
│ │ ├── globals.css
│ │ ├── history
│ │ │ └── page.tsx
│ │ ├── icon.ico
│ │ ├── layout.tsx
│ │ ├── login
│ │ │ └── page.tsx
│ │ ├── page.tsx
│ │ ├── settings
│ │ │ └── page.tsx
│ │ └── welcome
│ │ │ └── page.tsx
│ ├── components
│ │ ├── AgentWorkflow.tsx
│ │ ├── ChatInterface.tsx
│ │ ├── CommandHistory.tsx
│ │ ├── Header.tsx
│ │ ├── History.tsx
│ │ ├── Layout.tsx
│ │ ├── WebSocketProvider.tsx
│ │ ├── auth
│ │ │ ├── AuthInput.tsx
│ │ │ ├── AuthProvider.tsx
│ │ │ ├── Login.tsx
│ │ │ └── Register.tsx
│ │ ├── chat
│ │ │ ├── ChatContext.tsx
│ │ │ ├── components
│ │ │ │ ├── ApprovalButtons.tsx
│ │ │ │ ├── ChatHeader.tsx
│ │ │ │ ├── ChatInput.tsx
│ │ │ │ ├── ChatMessages.tsx
│ │ │ │ ├── ChatSuggestions.tsx
│ │ │ │ ├── TerminalOutput.tsx
│ │ │ │ ├── VerificationResults.tsx
│ │ │ │ ├── Welcome.tsx
│ │ │ │ └── WorkflowVisualizer.tsx
│ │ │ ├── hooks
│ │ │ │ └── useWebSocket.ts
│ │ │ ├── services
│ │ │ │ └── WebSocketService.ts
│ │ │ ├── types.ts
│ │ │ └── utils
│ │ │ │ └── workflowUtils.tsx
│ │ ├── navbar
│ │ │ └── Navbar.tsx
│ │ ├── settings
│ │ │ ├── ProfileSettings.tsx
│ │ │ ├── Settings.tsx
│ │ │ └── TeamSettings.tsx
│ │ └── ui
│ │ │ ├── Loader.tsx
│ │ │ ├── ToastContainer.tsx
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── code-block.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── markdown-components.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── skeleton.tsx
│ │ │ ├── tabs.tsx
│ │ │ └── tooltip.tsx
│ ├── lib
│ │ ├── api.ts
│ │ ├── auth.ts
│ │ ├── services
│ │ │ ├── chatState.ts
│ │ │ ├── conversation.ts
│ │ │ └── socket.ts
│ │ ├── team.ts
│ │ ├── toast.tsx
│ │ ├── types
│ │ │ ├── auth.ts
│ │ │ └── chat.ts
│ │ └── utils.ts
│ └── store
│ │ └── useAuthStore.ts
├── tailwind.config.ts
├── tsconfig.json
└── yarn.lock
└── yarn.lock
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Description
2 |
3 | Please include a summary of the changes and which issue is fixed. Explain the motivation for the changes.
4 |
5 | ## Related Issue(s)
6 |
7 | Fixes #(issue)
8 |
9 | ## Type of Change
10 |
11 | - [ ] Feature (new functionality)
12 | - [ ] Bug fix (fixes an issue)
13 | - [ ] Documentation update
14 | - [ ] Code refactor
15 | - [ ] Performance improvement
16 | - [ ] Tests
17 | - [ ] Infrastructure/build changes
18 | - [ ] Other (please describe):
19 |
20 | ## Testing
21 |
22 | Please describe the tests you've added/performed to verify your changes.
23 |
24 | ## Checklist
25 |
26 | - [ ] My code follows the [coding standards](CONTRIBUTING.md#coding-standards) for this project
27 | - [ ] I have added/updated necessary documentation
28 | - [ ] I have added tests that prove my fix/feature works
29 | - [ ] New and existing tests pass with my changes
30 | - [ ] I have checked for and resolved any merge conflicts
31 | - [ ] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) format
32 | - [ ] I have linked this PR to relevant issue(s)
33 |
34 | ## Screenshots (if applicable)
35 |
36 | ## Additional Notes
--------------------------------------------------------------------------------
/.github/workflows/discord-gfi.yml:
--------------------------------------------------------------------------------
1 | name: Update good-first-issues list
2 |
3 | on:
4 | workflow_dispatch:
5 | # Re-build when labels or state change
6 | issues:
7 | types: [opened, reopened, edited, labeled, unlabeled, closed]
8 | # …and every day as a safety net
9 | schedule:
10 | - cron: '0 3 * * *' # 03:00 UTC daily
11 |
12 | jobs:
13 | refresh:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | # ---------- Build the embed ----------
18 | - name: Build embed JSON (top-20)
19 | id: build
20 | uses: actions/github-script@v7
21 | with:
22 | script: |
23 | const { owner, repo } = context.repo;
24 |
25 | // pull first 20 open issues with that label
26 | const { data: issues } = await github.rest.issues.listForRepo({
27 | owner, repo,
28 | state: 'open',
29 | labels: 'good first issue',
30 | per_page: 20
31 | });
32 |
33 | // map to bullets: show other labels comma-separated after a hyphen
34 | const lines = issues.map(i => {
35 | const extras = i.labels
36 | .filter(l => l.name !== 'good first issue')
37 | .map(l => `\`${l.name}\``);
38 | const base = `• [#${i.number}](${i.html_url}) ${i.title}`;
39 | return extras.length
40 | ? `${base} - ${extras.join(', ')}`
41 | : base;
42 | });
43 |
44 | const embed = {
45 | embeds: [{
46 | title: '🆕 Good-first-issues',
47 | url: `https://github.com/${owner}/${repo}/issues?q=is%3Aissue+state%3Aopen+label%3A%22good+first+issue%22`,
48 | description: lines.join('\n'),
49 | color: 504575,
50 | timestamp: new Date().toISOString()
51 | }]
52 | };
53 |
54 | // expose as a base-64 string
55 | core.setOutput(
56 | 'b64',
57 | Buffer.from(JSON.stringify(embed)).toString('base64')
58 | );
59 |
60 | # ---------- Post via bot ----------
61 | - name: Push embed (replace old message)
62 | env:
63 | BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }}
64 | BOT_ID: ${{ secrets.DISCORD_BOT_ID }}
65 | CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID_GOOD_FIRST_ISSUES }}
66 | PAYLOAD_B64: ${{ steps.build.outputs.b64 }}
67 | run: |
68 | set -euo pipefail
69 | payload=$(printf '%s' "$PAYLOAD_B64" | base64 --decode)
70 | AUTH="Authorization: Bot ${BOT_TOKEN}"
71 |
72 | # 1) delete last bot message (if any)
73 | last=$(curl -sf -H "$AUTH" \
74 | "https://discord.com/api/channels/${CHANNEL_ID}/messages?limit=50" |
75 | jq -r --arg BID "$BOT_ID" '
76 | map(select(.author.id==$BID))[0].id // empty')
77 | if [ -n "$last" ]; then
78 | curl -s -X DELETE -H "$AUTH" \
79 | "https://discord.com/api/channels/${CHANNEL_ID}/messages/${last}" >/dev/null
80 | fi
81 |
82 | # 2) post fresh embed
83 | curl -s -H "$AUTH" -H "Content-Type: application/json" \
84 | -d "$payload" \
85 | "https://discord.com/api/channels/${CHANNEL_ID}/messages" >/dev/null
86 |
87 |
88 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .DS_store
3 | .venv
4 | .mypy_cache
5 | .idea
6 | .vscode/
7 | *.sw?
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Skyflo.ai
2 |
3 | Thank you for considering contributing to Skyflo.ai! This document provides guidelines for contributing to the project.
4 |
5 | We are committed to providing a friendly, safe, and welcoming environment for all contributors. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
6 |
7 | We highly recommend reading our [Architecture Guide](ARCHITECTURE.md) if you'd like to contribute! The repo is not as intimidating as it first seems if you read the guide!
8 |
9 | ## Quick Start
10 |
11 | 1. **Find an Issue**: Browse [issues](https://github.com/skyflo-ai/skyflo/issues) or [create one](https://github.com/skyflo-ai/skyflo/issues/new/choose)
12 | 2. **Fork & Clone**: Fork the repository and clone it locally
13 | 3. **Setup**: Install dependencies and configure development environment
14 | 4. **Create Branch**: Use `feature/issue-number-description` or `fix/issue-number-description`
15 | 5. **Make Changes**: Follow our coding standards and add tests
16 | 6. **Submit PR**: Create a pull request with a clear description of changes
17 |
18 | ## Coding Standards
19 |
20 | - **Python**: [PEP 8](https://www.python.org/dev/peps/pep-0008/), type hints, docstrings
21 | - **JavaScript/TypeScript**: [Airbnb Style Guide](https://github.com/airbnb/javascript), TypeScript for type safety
22 | - **Go**: [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
23 | - **Documentation**: Markdown, clear language, code examples
24 | - **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) format
25 | - Include component scope in commit messages: `type(scope): message`
26 | - Use the following component scopes:
27 | - `ui`: Frontend components
28 | - `engine`: Engine components
29 | - `mcp`: MCP server components
30 | - `k8s`: Kubernetes controller components
31 | - `docs`: Documentation changes
32 | - `infra`: Infrastructure or build system changes
33 | - Example: `feat (mcp): add docker tools` or `fix (ui): resolve workflow visualizer overflow`
34 |
35 | ## Pull Request Process
36 |
37 | 1. Fill out the PR template
38 | 2. Link to related issues
39 | 3. Ensure CI checks pass
40 | 4. Address review feedback
41 | 5. Await approval from maintainer
42 |
43 | ## License
44 |
45 | Skyflo.ai is fully open source and licensed under the [Apache License 2.0](LICENSE).
46 |
47 | ## Community
48 |
49 | Join our community channels:
50 |
51 | - [Discord Server](https://discord.gg/kCFNavMund)
52 | - [GitHub Discussions](https://github.com/skyflo-ai/skyflo/discussions)
53 | - [Twitter/X](https://x.com/skyflo_ai)
54 |
55 | ---
56 |
57 | Thank you for contributing to Skyflo.ai! Your efforts help make cloud infrastructure management accessible through AI.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skyflo.ai - AI Agent for Cloud Native
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | [](https://skyflo.ai)
10 | [](https://discord.gg/kCFNavMund)
11 | [](https://x.com/skyflo_ai)
12 | [](https://www.youtube.com/@SkyfloAI)
13 |
14 |
15 |
16 | Skyflo.ai is your AI-powered companion for cloud native operations, enabling seamless infrastructure management through natural language.
17 |
18 | ## How to Install
19 |
20 | Skyflo.ai offers flexible deployment options, accommodating both production and local Kubernetes environments:
21 |
22 | ```bash
23 | curl -sL https://raw.githubusercontent.com/skyflo-ai/skyflo/main/deployment/install.sh -o install.sh && chmod +x install.sh && ./install.sh
24 | ```
25 |
26 | For more details, see the [Installation Guide](docs/install.md).
27 |
28 | ## Configuration
29 |
30 | By default, Skyflo is configured to use OpenAI's GPT-4o model. For instructions on using different LLM providers (like Groq, Anthropic, Cohere, etc.) or configuring API keys, please refer to the [Installation Guide](docs/install.md).
31 |
32 | ## Architecture
33 |
34 | Read more about the architecture of Skyflo.ai in the [Architecture](docs/architecture.md) documentation.
35 |
36 | Skyflo.ai's multi-agent architecture leverages [Microsoft's AutoGen](https://github.com/microsoft/autogen) for agent orchestration and [LangGraph](https://github.com/langchain-ai/langgraph) for graph-based execution flows.
37 |
38 | ## Roadmap
39 |
40 | Read more about the [roadmap of Skyflo.ai here](https://skyflo.ai/roadmap).
41 |
42 | ## Contributing
43 |
44 | We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details on getting started.
45 |
46 | ## Code of Conduct
47 |
48 | We have a [Code of Conduct](code_of_conduct.md) that we ask all contributors to follow.
49 |
50 | ## Community
51 |
52 | - [Discord](https://discord.gg/kCFNavMund)
53 | - [Twitter/X](https://x.com/skyflo_ai)
54 | - [YouTube](https://www.youtube.com/@SkyfloAI)
55 | - [GitHub Discussions](https://github.com/skyflo-ai/skyflo/discussions)
56 |
--------------------------------------------------------------------------------
/assets/readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/assets/readme.png
--------------------------------------------------------------------------------
/deployment/README.md:
--------------------------------------------------------------------------------
1 | # Skyflo Deployment
2 |
3 | ## Local Development with KinD
4 |
5 | ## Prerequisites
6 |
7 | - Docker
8 | - KinD
9 | - kubectl
10 |
11 | ## Setup KinD Cluster
12 |
13 | ```bash
14 | kind create cluster --name skyfloai --config local.kind.yml
15 | ```
16 |
17 | ## Build the Docker Images
18 |
19 | ```bash
20 | # Build the Engine image
21 | docker buildx build -f deployment/engine/Dockerfile -t skyfloai/engine:latest .
22 |
23 | # Build the MCP image
24 | docker buildx build -f deployment/mcp/Dockerfile -t skyfloai/mcp:latest .
25 |
26 | # Build the UI image
27 | docker buildx build -f deployment/ui/Dockerfile -t skyfloai/ui:latest .
28 |
29 | # Build the Kubernetes Controller image
30 | docker buildx build -f deployment/kubernetes-controller/Dockerfile -t skyfloai/controller:latest .
31 |
32 | # Build the Proxy image
33 | docker buildx build -f deployment/ui/proxy.Dockerfile -t skyfloai/proxy:latest .
34 | ```
35 |
36 | ## Load the built images into the KinD cluster
37 | ```bash
38 | kind load docker-image --name skyfloai skyfloai/ui:latest
39 |
40 | kind load docker-image --name skyfloai skyfloai/engine:latest
41 |
42 | kind load docker-image --name skyfloai skyfloai/mcp:latest
43 |
44 | kind load docker-image --name skyfloai skyfloai/controller:latest
45 |
46 | kind load docker-image --name skyfloai skyfloai/proxy:latest
47 | ```
48 |
49 | ## Install the Controller and Resources
50 |
51 | ```bash
52 | k delete -f local.install.yaml
53 | k apply -f local.install.yaml
54 | ```
55 |
56 | ## How to test
57 |
58 | The Nginx deployment contains an incorrect image tag. This is a good basic test to see if the Sky AI agent catches the error and fixes it.
59 |
60 | ```bash
61 | k apply -f local.test-deploy.yaml
62 |
63 | k delete -f local.test-deploy.yaml
64 | ```
--------------------------------------------------------------------------------
/deployment/engine/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use official Python image
2 | FROM python:3.11-slim
3 |
4 | ARG TARGETARCH
5 |
6 | # Install system dependencies
7 | RUN apt-get update && apt-get install -y --no-install-recommends \
8 | git \
9 | build-essential \
10 | curl \
11 | ca-certificates \
12 | netcat-openbsd \
13 | bash \
14 | postgresql-client \
15 | && apt-get clean \
16 | && rm -rf /var/lib/apt/lists/*
17 |
18 | # Create app user and directory
19 | RUN groupadd -g 1002 skyflogroup \
20 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \
21 | && mkdir -p /app \
22 | && chown -R skyflo:skyflogroup /app
23 |
24 | # Set up application
25 | WORKDIR /app
26 |
27 | # Create and activate virtual environment early
28 | ENV VIRTUAL_ENV="/app/venv"
29 | RUN python -m venv $VIRTUAL_ENV
30 | ENV PATH="$VIRTUAL_ENV/bin:$PATH"
31 |
32 | # Create the source directory structure first
33 | RUN mkdir -p src/api
34 |
35 | # Copy dependency-related files and source code needed for installation
36 | COPY engine/pyproject.toml engine/.python-version engine/uv.lock engine/README.md ./
37 | COPY engine/src/api/__about__.py ./src/api/
38 |
39 | # Install dependencies
40 | RUN pip install --upgrade pip && \
41 | pip install -e . && \
42 | pip install uvicorn[standard]
43 |
44 | # Now copy all remaining application files
45 | COPY engine/src ./src
46 | COPY engine/.env ./.env
47 | COPY engine/migrations ./migrations
48 | COPY engine/aerich.ini ./aerich.ini
49 | COPY engine/alembic.ini ./alembic.ini
50 |
51 | # Copy and set up entrypoint script
52 | COPY deployment/engine/entrypoint.sh /app/entrypoint.sh
53 | RUN chmod +x /app/entrypoint.sh
54 |
55 | # Set final permissions
56 | RUN chown -R skyflo:skyflogroup /app && \
57 | chmod -R 755 /app
58 |
59 | # Update PATH and PYTHONPATH for skyflo user
60 | ENV PATH="/app/venv/bin:/usr/local/bin:/home/skyflo/.local/bin:${PATH}" \
61 | PYTHONPATH="/app/src"
62 |
63 | # Expose the API port
64 | EXPOSE 8080
65 |
66 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo
67 | LABEL org.opencontainers.image.description="Skyflo.ai Engine Service - Open Source AI Agent for Cloud Native"
68 |
69 | # Switch to non-root user
70 | USER skyflo
71 |
72 | # Use the entrypoint script
73 | ENTRYPOINT ["/app/entrypoint.sh"]
--------------------------------------------------------------------------------
/deployment/engine/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Wait for 45 seconds for the database to be ready
5 | sleep 45
6 |
7 | # Initialize Aerich if not already initialized
8 | if [ ! -d "migrations" ]; then
9 | echo "Initializing Aerich..."
10 | aerich init -t src.api.repositories.database.TORTOISE_ORM_CONFIG
11 | fi
12 |
13 | # Create initial migration if none exists
14 | if [ ! "$(ls -A migrations/models 2>/dev/null)" ]; then
15 | echo "Creating initial migration..."
16 | aerich init-db
17 | else
18 | # Apply any pending migrations
19 | echo "Applying pending migrations..."
20 | aerich upgrade
21 | fi
22 |
23 | # Start the API service with Uvicorn
24 | echo "Starting Engine service..."
25 | exec uvicorn api.asgi:app --host 0.0.0.0 --port 8080
--------------------------------------------------------------------------------
/deployment/kubernetes-controller/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM golang:1.24.1-alpine AS builder
3 |
4 | WORKDIR /workspace
5 |
6 | # Copy Go module files first for better layer caching
7 | COPY kubernetes-controller/go.mod kubernetes-controller/go.sum ./
8 | RUN go mod download
9 |
10 | # Copy the source code
11 | COPY kubernetes-controller/ ./
12 |
13 | # Build the controller binary
14 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager cmd/manager/main.go
15 |
16 | # Runtime stage
17 | FROM gcr.io/distroless/static:nonroot
18 |
19 | WORKDIR /
20 |
21 | # Copy the controller binary from builder stage
22 | COPY --from=builder /workspace/manager /manager
23 |
24 | # Use nonroot user for security
25 | USER 65532:65532
26 |
27 | # Set entrypoint to the manager binary
28 | ENTRYPOINT ["/manager"]
29 |
--------------------------------------------------------------------------------
/deployment/local.docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | name: skyflo_ai
3 |
4 | services:
5 | postgres:
6 | image: postgres:15-alpine
7 | container_name: skyflo_ai_postgres
8 | environment:
9 | - POSTGRES_USER=skyflo
10 | - POSTGRES_PASSWORD=skyflo
11 | - POSTGRES_DB=skyflo
12 | ports:
13 | - "5432:5432"
14 | volumes:
15 | - postgres_data:/var/lib/postgresql/data
16 | healthcheck:
17 | test: ["CMD-SHELL", "pg_isready -U skyflo"]
18 | interval: 10s
19 | timeout: 5s
20 | retries: 5
21 | start_period: 10s
22 | restart: unless-stopped
23 |
24 | redis:
25 | image: redis:7-alpine
26 | container_name: skyflo_ai_redis
27 | ports:
28 | - "6379:6379"
29 | volumes:
30 | - redis_data:/data
31 | healthcheck:
32 | test: ["CMD", "redis-cli", "ping"]
33 | interval: 10s
34 | timeout: 5s
35 | retries: 5
36 | start_period: 10s
37 | restart: unless-stopped
38 | command: redis-server --appendonly yes
39 |
40 | volumes:
41 | postgres_data:
42 | redis_data:
43 |
--------------------------------------------------------------------------------
/deployment/local.kind.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: kind.x-k8s.io/v1alpha4
2 | kind: Cluster
3 | name: skyflo-ai
4 | nodes:
5 | - role: control-plane
6 | image: kindest/node:v1.32.2
7 | extraPortMappings:
8 | - containerPort: 30080
9 | hostPort: 30080
10 | listenAddress: "0.0.0.0"
11 | protocol: TCP
12 | - containerPort: 30081
13 | hostPort: 30081
14 | listenAddress: "0.0.0.0"
15 | protocol: TCP
16 | - role: worker
17 | image: kindest/node:v1.32.2
18 |
--------------------------------------------------------------------------------
/deployment/local.test-deploy.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Namespace
4 | metadata:
5 | name: prod
6 | ---
7 | apiVersion: apps/v1
8 | kind: Deployment
9 | metadata:
10 | name: api-prod
11 | namespace: prod
12 | labels:
13 | app: api-prod
14 | spec:
15 | replicas: 2
16 | selector:
17 | matchLabels:
18 | app: api-prod
19 | template:
20 | metadata:
21 | labels:
22 | app: api-prod
23 | spec:
24 | containers:
25 | - name: api-prod
26 | image: nginx:1.345 # Incorrect image tag
27 | ports:
28 | - containerPort: 80
29 | ---
30 | apiVersion: v1
31 | kind: Service
32 | metadata:
33 | name: api-prod
34 | namespace: prod
35 | spec:
36 | selector:
37 | app: api-prod
38 | ports:
39 | - protocol: TCP
40 | port: 80
41 | targetPort: 80
42 | type: ClusterIP
43 |
--------------------------------------------------------------------------------
/deployment/mcp/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use official Python image
2 | FROM python:3.11-slim
3 |
4 | ARG TARGETARCH
5 |
6 | # Install system dependencies
7 | RUN apt-get update && apt-get install -y --no-install-recommends \
8 | git \
9 | build-essential \
10 | curl \
11 | ca-certificates \
12 | gnupg \
13 | netcat-openbsd \
14 | bash \
15 | && apt-get clean \
16 | && rm -rf /var/lib/apt/lists/*
17 |
18 | # Install kubectl
19 | RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/$TARGETARCH/kubectl" \
20 | && chmod +x kubectl \
21 | && mv kubectl /usr/local/bin/
22 |
23 | # Install kubectl argo rollouts plugin
24 | RUN curl -LO "https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-$TARGETARCH" \
25 | && chmod +x kubectl-argo-rollouts-linux-$TARGETARCH \
26 | && mv kubectl-argo-rollouts-linux-$TARGETARCH /usr/local/bin/kubectl-argo-rollouts \
27 | && ln -s /usr/local/bin/kubectl-argo-rollouts /usr/local/bin/kubectl-argo
28 |
29 | # Install Helm
30 | RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 \
31 | && chmod 700 get_helm.sh \
32 | && ./get_helm.sh \
33 | && rm get_helm.sh
34 |
35 | # Create app user and directory
36 | RUN groupadd -g 1002 skyflogroup \
37 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \
38 | && mkdir -p /app \
39 | && chown -R skyflo:skyflogroup /app
40 |
41 | # Set up application
42 | WORKDIR /app
43 |
44 | # Create and activate virtual environment early
45 | ENV VIRTUAL_ENV="/app/venv"
46 | RUN python -m venv $VIRTUAL_ENV
47 | ENV PATH="$VIRTUAL_ENV/bin:$PATH"
48 |
49 | # Create the source directory structure first
50 | RUN mkdir -p src/mcp_server
51 |
52 | # Copy dependency-related files and source code needed for installation
53 | COPY mcp/pyproject.toml mcp/.python-version mcp/uv.lock mcp/README.md ./
54 | COPY mcp/src/mcp_server/__about__.py ./src/mcp_server/
55 |
56 | # Install dependencies
57 | RUN pip install --upgrade pip && \
58 | pip install -e . && \
59 | pip install uvicorn[standard]
60 |
61 | # Now copy all remaining application files
62 | COPY mcp/src ./src
63 | COPY mcp/.env ./.env
64 |
65 | # Copy and set up entrypoint script
66 | COPY deployment/mcp/entrypoint.sh /app/entrypoint.sh
67 | RUN chmod +x /app/entrypoint.sh
68 |
69 | # Set final permissions
70 | RUN chown -R skyflo:skyflogroup /app && \
71 | chmod -R 755 /app
72 |
73 | # Update PATH and PYTHONPATH for skyflo user
74 | ENV PATH="/app/venv/bin:/usr/local/bin:/home/skyflo/.local/bin:${PATH}" \
75 | PYTHONPATH="/app/src"
76 |
77 | # Expose the API port
78 | EXPOSE 8081
79 |
80 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo
81 | LABEL org.opencontainers.image.description="Skyflo.ai MCP Server - Open Source AI Agent for Cloud Native"
82 |
83 | # Switch to non-root user
84 | USER skyflo
85 |
86 | # Use the entrypoint script
87 | ENTRYPOINT ["/app/entrypoint.sh"]
--------------------------------------------------------------------------------
/deployment/mcp/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Start the Engine service with Uvicorn
5 | echo "Starting MCP service..."
6 | exec uvicorn mcp_server.asgi:app --host 0.0.0.0 --port 8081
--------------------------------------------------------------------------------
/deployment/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM node:20-slim AS builder
3 |
4 | RUN apt-get update && apt-get install -y \
5 | bash \
6 | curl \
7 | && rm -rf /var/lib/apt/lists/*
8 |
9 | # Create app directory
10 | WORKDIR /app
11 |
12 | # Install dependencies first (caching)
13 | COPY ui/package.json ui/yarn.lock ./
14 | RUN yarn install --frozen-lockfile
15 |
16 | # Copy source files and environment file
17 | COPY ui/tsconfig.json ./
18 | COPY ui/next.config.mjs ./
19 | COPY ui/tailwind.config.ts ./
20 | COPY ui/postcss.config.mjs ./
21 | COPY ui/components.json ./
22 | COPY ui/.eslintrc.json ./
23 | COPY ui/src ./src
24 | COPY ui/public ./public
25 | COPY ui/.env ./.env.production
26 |
27 | # Build the application
28 | ENV NEXT_TELEMETRY_DISABLED=1
29 | RUN yarn build
30 |
31 | # Production stage
32 | FROM node:20-slim AS runner
33 |
34 | WORKDIR /app
35 |
36 | # Create app user
37 | RUN groupadd -g 1002 skyflogroup \
38 | && useradd -u 1002 -g skyflogroup -s /bin/bash -m skyflo \
39 | && chown -R skyflo:skyflogroup /app
40 |
41 | ENV NODE_ENV=production
42 | ENV NEXT_TELEMETRY_DISABLED=1
43 |
44 | # Copy necessary files from builder
45 | COPY --from=builder --chown=skyflo:skyflogroup /app/.next/standalone ./
46 | COPY --from=builder --chown=skyflo:skyflogroup /app/.next/static ./.next/static
47 | COPY --from=builder --chown=skyflo:skyflogroup /app/public ./public
48 | COPY --from=builder --chown=skyflo:skyflogroup /app/.env.production ./.env
49 |
50 | # Expose the UI port
51 | EXPOSE 3000
52 |
53 | LABEL org.opencontainers.image.source=https://github.com/skyflo-ai/skyflo
54 | LABEL org.opencontainers.image.description="UI for Skyflo.ai - Open Source AI Agent for Cloud Native"
55 |
56 | # Switch to non-root user
57 | USER skyflo
58 |
59 | # Start the application
60 | CMD ["node", "server.js"]
--------------------------------------------------------------------------------
/deployment/ui/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | server_name localhost;
5 |
6 | location /api/ws/ {
7 | proxy_pass http://skyflo-ai-engine:8080/;
8 | proxy_http_version 1.1;
9 | proxy_set_header Upgrade $http_upgrade;
10 | proxy_set_header Connection "Upgrade";
11 | proxy_set_header Host $http_host;
12 | proxy_set_header X-Forwarded-Host $http_host;
13 | proxy_set_header X-Real-IP $remote_addr;
14 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
15 | proxy_set_header X-Forwarded-Proto $scheme;
16 | }
17 |
18 | location / {
19 | proxy_pass http://skyflo-ai-ui.skyflo-ai.svc.cluster.local:3000;
20 | proxy_http_version 1.1;
21 | proxy_set_header Upgrade $http_upgrade;
22 | proxy_set_header Connection "Upgrade";
23 | proxy_set_header Host $http_host;
24 | proxy_set_header X-Forwarded-Host $http_host;
25 | proxy_set_header X-Real-IP $remote_addr;
26 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
27 | proxy_set_header X-Forwarded-Proto $scheme;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/deployment/ui/proxy.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.25-alpine
2 |
3 | # Copy nginx configuration
4 | COPY deployment/ui/nginx.conf /etc/nginx/conf.d/default.conf
5 |
6 | EXPOSE 80
7 |
8 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/engine/.env.example:
--------------------------------------------------------------------------------
1 | # Application Settings
2 | APP_NAME=Skyflo.ai - Engine
3 | APP_VERSION=0.1.0
4 | APP_DESCRIPTION=Core engine for Skyflo.ai
5 | DEBUG=false
6 | API_V1_STR=/api/v1
7 |
8 | # Server Settings
9 | LOG_LEVEL=INFO
10 |
11 | # Database Settings (using postgres:// for Tortoise ORM)
12 | POSTGRES_DATABASE_URL=postgres://skyflo:skyflo@skyflo-ai-postgres:5432/skyflo
13 |
14 | # Redis Settings
15 | REDIS_URL=redis://skyflo-ai-redis:6379/0
16 |
17 | # Rate Limiting
18 | RATE_LIMITING_ENABLED=true
19 | RATE_LIMIT_PER_MINUTE=100
20 |
21 | # JWT Settings
22 | JWT_SECRET=your-jwt-secret
23 | JWT_ALGORITHM=HS256
24 | JWT_ACCESS_TOKEN_EXPIRE_MINUTES=10080 # 1 week
25 | JWT_REFRESH_TOKEN_EXPIRE_DAYS=7 # 1 week
26 |
27 | # OpenAI Settings
28 | OPENAI_API_KEY=your-openai-api-key
29 | LLM_MODEL=openai/gpt-4o
30 | # GROQ_API_KEY= <>
31 | # LLM_MODEL=groq/meta-llama/llama-4-scout-17b-16e-instruct
32 | # LLM_MODEL=ollama/llama3.1:8b
33 | # LLM_HOST=0.0.0.0:11434
34 | MANAGER_OPENAI_TEMPERATURE=0.7
35 | OPENAI_PLANNER_TEMPERATURE=0.3
36 | OPENAI_EXECUTOR_TEMPERATURE=0.0
37 | OPENAI_VERIFIER_TEMPERATURE=0.2
38 |
39 | # MCP Server Settings
40 | MCP_SERVER_URL=http://skyflo-ai-mcp:8081
41 |
42 | # Workflow Settings
43 | WORKFLOW_EXECUTION_TIMEOUT=300
--------------------------------------------------------------------------------
/engine/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | env.bak/
136 | venv.bak/
137 |
138 | # Spyder project settings
139 | .spyderproject
140 | .spyproject
141 |
142 | # Rope project settings
143 | .ropeproject
144 |
145 | # mkdocs documentation
146 | /site
147 |
148 | # mypy
149 | .mypy_cache/
150 | .dmypy.json
151 | dmypy.json
152 |
153 | # Pyre type checker
154 | .pyre/
155 |
156 | # pytype static type analyzer
157 | .pytype/
158 |
159 | # Cython debug symbols
160 | cython_debug/
161 |
162 | # PyCharm
163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165 | # and can be added to the global gitignore or merged into this file. For a more nuclear
166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167 | #.idea/
168 |
169 | # Ruff stuff:
170 | .ruff_cache/
171 |
172 | # PyPI configuration file
173 | .pypirc
--------------------------------------------------------------------------------
/engine/.python-version:
--------------------------------------------------------------------------------
1 | 3.11
2 |
--------------------------------------------------------------------------------
/engine/aerich.ini:
--------------------------------------------------------------------------------
1 | [aerich]
2 | tortoise_orm = src.api.repositories.database.TORTOISE_ORM_CONFIG
3 | location = ./migrations
4 | src_folder = ./src/
--------------------------------------------------------------------------------
/engine/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = migrations
6 |
7 | # template used to generate migration files
8 | file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d%%(second).2d_%%(slug)s
9 |
10 | # timezone to use when rendering the date
11 | # within the migration file as well as the filename.
12 | # string value is passed to dateutil.tz.gettz()
13 | # leave blank for localtime
14 | timezone = UTC
15 |
16 | # max length of characters to apply to the
17 | # "slug" field
18 | truncate_slug_length = 40
19 |
20 | # set to 'true' to run the environment during
21 | # the 'revision' command, regardless of autogenerate
22 | revision_environment = false
23 |
24 | # set to 'true' to allow .pyc and .pyo files without
25 | # a source .py file to be detected as revisions in the
26 | # versions/ directory
27 | # sourceless = false
28 |
29 | # version location specification; this defaults
30 | # to migrations/versions. When using multiple version
31 | # directories, initial revisions must be specified with --version-path
32 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions
33 |
34 | # the output encoding used when revision files
35 | # are written from script.py.mako
36 | output_encoding = utf-8
37 |
38 | # Logging configuration
39 | [loggers]
40 | keys = root,sqlalchemy,alembic
41 |
42 | [handlers]
43 | keys = console
44 |
45 | [formatters]
46 | keys = generic
47 |
48 | [logger_root]
49 | level = WARN
50 | handlers = console
51 | qualname =
52 |
53 | [logger_sqlalchemy]
54 | level = WARN
55 | handlers =
56 | qualname = sqlalchemy.engine
57 |
58 | [logger_alembic]
59 | level = INFO
60 | handlers =
61 | qualname = alembic
62 |
63 | [handler_console]
64 | class = StreamHandler
65 | args = (sys.stderr,)
66 | level = NOTSET
67 | formatter = generic
68 |
69 | [formatter_generic]
70 | format = %(levelname)-5.5s [%(name)s] %(message)s
71 | datefmt = %H:%M:%S
--------------------------------------------------------------------------------
/engine/migrations/models/0_20250407154142_init.py:
--------------------------------------------------------------------------------
1 | from tortoise import BaseDBAsyncClient
2 |
3 |
4 | async def upgrade(db: BaseDBAsyncClient) -> str:
5 | return """
6 | CREATE TABLE IF NOT EXISTS "users" (
7 | "id" UUID NOT NULL PRIMARY KEY,
8 | "email" VARCHAR(255) NOT NULL UNIQUE,
9 | "hashed_password" VARCHAR(255) NOT NULL,
10 | "full_name" VARCHAR(255),
11 | "is_active" BOOL NOT NULL DEFAULT True,
12 | "is_superuser" BOOL NOT NULL DEFAULT False,
13 | "is_verified" BOOL NOT NULL DEFAULT False,
14 | "role" VARCHAR(20) NOT NULL DEFAULT 'member',
15 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 | "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
17 | );
18 | CREATE INDEX IF NOT EXISTS "idx_users_email_133a6f" ON "users" ("email");
19 | COMMENT ON TABLE "users" IS 'User model for authentication and profile information.';
20 | CREATE TABLE IF NOT EXISTS "conversations" (
21 | "id" UUID NOT NULL PRIMARY KEY,
22 | "title" VARCHAR(255),
23 | "is_active" BOOL NOT NULL DEFAULT True,
24 | "conversation_metadata" JSONB,
25 | "messages_json" JSONB,
26 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
27 | "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
28 | "user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
29 | );
30 | COMMENT ON TABLE "conversations" IS 'Conversation model for tracking chat sessions.';
31 | CREATE TABLE IF NOT EXISTS "messages" (
32 | "id" UUID NOT NULL PRIMARY KEY,
33 | "role" VARCHAR(50) NOT NULL,
34 | "content" TEXT NOT NULL,
35 | "sequence" INT NOT NULL,
36 | "message_metadata" JSONB,
37 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
38 | "conversation_id" UUID NOT NULL REFERENCES "conversations" ("id") ON DELETE CASCADE
39 | );
40 | COMMENT ON TABLE "messages" IS 'Message model for storing individual chat messages.';
41 | CREATE TABLE IF NOT EXISTS "aerich" (
42 | "id" SERIAL NOT NULL PRIMARY KEY,
43 | "version" VARCHAR(255) NOT NULL,
44 | "app" VARCHAR(100) NOT NULL,
45 | "content" JSONB NOT NULL
46 | );"""
47 |
48 |
49 | async def downgrade(db: BaseDBAsyncClient) -> str:
50 | return """
51 | """
52 |
--------------------------------------------------------------------------------
/engine/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "skyflo-engine"
7 | dynamic = ["version"]
8 | description = "Skyflo.ai Engine Service - Middleware for Cloud Native AI Agent"
9 | readme = "README.md"
10 | requires-python = ">=3.11"
11 | license = "Apache-2.0"
12 | keywords = ["ai", "agent", "cloud native", "engine", "open source"]
13 | dependencies = [
14 | "fastapi>=0.110.0",
15 | "uvicorn>=0.27.1",
16 | "pydantic>=2.10.6",
17 | "pydantic-settings>=2.2.1",
18 | "python-jose[cryptography]>=3.3.0",
19 | "passlib[bcrypt]>=1.7.4",
20 | "email-validator>=2.1.0.post1",
21 | "casbin>=1.41.0",
22 | "httpx>=0.27.0",
23 | "python-multipart>=0.0.9",
24 | "aiohttp>=3.11.14",
25 | "fastapi-websocket-pubsub>=0.3.9",
26 | "tortoise-orm>=0.24.2",
27 | "aerich>=0.8.2",
28 | "broadcaster>=0.3.1",
29 | "redis>=5.0.1",
30 | "fastapi-limiter>=0.1.6",
31 | "autogen-agentchat>=0.4.9.2",
32 | "autogen-core>=0.4.9.2",
33 | "autogen-ext[mcp]>=0.4.9.2",
34 | "langgraph>=0.3.18",
35 | "mcp>=1.5.0",
36 | "openai>=1.68.2",
37 | "typer>=0.15.2",
38 | "tenacity>=8.2.3",
39 | "tomlkit>=0.13.2",
40 | "tiktoken>=0.9.0",
41 | "fastapi-users-tortoise>=0.2.0",
42 | "python-socketio>=5.12.1",
43 | "python-decouple>=3.8",
44 | "litellm>=1.67.4",
45 | ]
46 |
47 | [[project.authors]]
48 | name = "Karan Jagtiani"
49 | email = "karan@skyflo.ai"
50 |
51 | [project.optional-dependencies]
52 | default = ["pytest>=8.0.2", "pytest-cov>=4.1.0", "pytest-asyncio>=0.23.5", "pytest-mock>=3.12.0", "pytest-env>=1.1.3", "mypy>=1.8.0", "black>=24.2.0", "ruff>=0.3.0"]
53 |
54 | [project.scripts]
55 | skyflo-engine = "engine.main:run"
56 |
57 | [project.urls]
58 | Documentation = "https://github.com/skyflo-ai/skyflo#readme"
59 | Issues = "https://github.com/skyflo-ai/skyflo/issues"
60 | Source = "https://github.com/skyflo-ai/skyflo"
61 |
62 | [tool.hatch.version]
63 | path = "src/api/__about__.py"
64 |
65 | [tool.hatch.build.targets.wheel]
66 | packages = ["src/api"]
67 |
68 | [tool.hatch.envs.default]
69 | dependencies = ["pytest>=8.0.2", "pytest-cov>=4.1.0", "pytest-asyncio>=0.23.5", "pytest-mock>=3.12.0", "pytest-env>=1.1.3", "mypy>=1.8.0", "black>=24.2.0", "ruff>=0.3.0"]
70 |
71 | [tool.hatch.envs.default.scripts]
72 | test = "pytest {args:tests}"
73 | test-cov = "pytest --cov {args:src/api}"
74 | format = "black {args:src/api tests}"
75 | lint = "ruff check {args:src/api tests}"
76 | type-check = "mypy --install-types --non-interactive {args:src/api tests}"
77 |
78 | [tool.coverage.run]
79 | source_pkgs = ["api", "tests"]
80 | branch = true
81 | parallel = true
82 | omit = ["src/api/__about__.py"]
83 |
84 | [tool.coverage.paths]
85 | api = ["src/api", "*/api/src/api"]
86 | tests = ["tests", "*/api/tests"]
87 |
88 | [tool.mypy]
89 | python_version = "3.11"
90 | warn_return_any = true
91 | warn_unused_configs = true
92 | disallow_untyped_defs = true
93 | disallow_incomplete_defs = true
94 |
95 | [tool.ruff]
96 | target-version = "py311"
97 | line-length = 100
98 | select = ["E", "F", "B", "I"]
99 |
100 | [tool.black]
101 | line-length = 100
102 | target-version = ["py311"]
103 |
104 | [tool.aerich]
105 | tortoise_orm = "src.api.repositories.database.TORTOISE_ORM_CONFIG"
106 | location = "./migrations"
107 | src_folder = "./."
108 |
--------------------------------------------------------------------------------
/engine/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/engine/src/__init__.py
--------------------------------------------------------------------------------
/engine/src/api/__about__.py:
--------------------------------------------------------------------------------
1 | """Version information."""
2 |
3 | __version__ = "0.1.0"
4 |
--------------------------------------------------------------------------------
/engine/src/api/__init__.py:
--------------------------------------------------------------------------------
1 | """Skyflo.ai API Service package."""
2 |
3 | from .__about__ import __version__
4 |
5 | __all__ = ["__version__"]
6 |
--------------------------------------------------------------------------------
/engine/src/api/asgi.py:
--------------------------------------------------------------------------------
1 | """ASGI entry point for Skyflo.ai API service."""
2 |
3 | import logging
4 | from contextlib import asynccontextmanager
5 | from fastapi import FastAPI
6 | from fastapi.middleware.cors import CORSMiddleware
7 |
8 | from .config import settings
9 | from .repositories import init_db, close_db_connection
10 | from .web.endpoints import api_router, socketio_app
11 | from .web.middleware import setup_middleware
12 | from .services.rbac import init_enforcer
13 | from .services.limiter import init_limiter, close_limiter
14 |
15 | # Configure logging
16 | logging.basicConfig(
17 | level=getattr(logging, settings.LOG_LEVEL),
18 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19 | )
20 |
21 | logger = logging.getLogger(__name__)
22 |
23 | @asynccontextmanager
24 | async def lifespan(app: FastAPI):
25 | """Lifespan context manager for startup and shutdown events."""
26 | # Startup
27 | logger.info(f"Starting {settings.APP_NAME} version {settings.APP_VERSION}")
28 | await init_db()
29 | await init_enforcer()
30 | await init_limiter()
31 | yield
32 | # Shutdown
33 | logger.info(f"Shutting down {settings.APP_NAME}")
34 | await close_db_connection()
35 | await close_limiter()
36 |
37 |
38 | def create_application() -> FastAPI:
39 | """Create the FastAPI application."""
40 | application = FastAPI(
41 | title=settings.APP_NAME,
42 | description=settings.APP_DESCRIPTION,
43 | version=settings.APP_VERSION,
44 | debug=settings.DEBUG,
45 | lifespan=lifespan,
46 | )
47 |
48 | # Add custom middleware
49 | setup_middleware(application)
50 |
51 | # Include API routes
52 | application.include_router(api_router, prefix=settings.API_V1_STR)
53 |
54 | # Mount the Socket.IO app at the root to handle Socket.IO connections
55 | application.mount("/socket.io", socketio_app)
56 |
57 | return application
58 |
59 |
60 | # Create and export the FastAPI application
61 | app = create_application()
62 |
--------------------------------------------------------------------------------
/engine/src/api/config/__init__.py:
--------------------------------------------------------------------------------
1 | """Config package."""
2 |
3 | from .settings import settings, get_settings
4 |
5 | __all__ = ["settings", "get_settings"]
6 |
--------------------------------------------------------------------------------
/engine/src/api/config/settings.py:
--------------------------------------------------------------------------------
1 | """Configuration settings for the API service."""
2 |
3 | from typing import Optional
4 | from pydantic import Field
5 | from pydantic_settings import BaseSettings
6 |
7 |
8 | class Settings(BaseSettings):
9 | """API service configuration settings.
10 |
11 | These settings can be configured using environment variables.
12 | """
13 |
14 | # Application Settings
15 | APP_NAME: str
16 | APP_VERSION: str
17 | APP_DESCRIPTION: str
18 | DEBUG: bool = False
19 | API_V1_STR: str = "/api/v1"
20 |
21 | # Server Settings
22 | LOG_LEVEL: str = "INFO"
23 |
24 | # Database Settings - using postgres:// format for Tortoise ORM
25 | POSTGRES_DATABASE_URL: str = Field(default="postgres://postgres:postgres@localhost:5432/skyflo")
26 |
27 | # Redis Settings for real-time features
28 | REDIS_URL: str = "redis://localhost:6379/0"
29 |
30 | # Rate Limiting
31 | RATE_LIMITING_ENABLED: bool = True
32 | RATE_LIMIT_PER_MINUTE: int = 100
33 |
34 | # JWT Settings
35 | JWT_SECRET: str = "CHANGE_ME_IN_PRODUCTION"
36 | JWT_ALGORITHM: str = "HS256"
37 | JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = 10080 # One week in minutes
38 | JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = 7
39 |
40 | # MCP Server Settings
41 | MCP_SERVER_URL: str = "http://127.0.0.1:8081"
42 |
43 | # Workflow Settings
44 | WORKFLOW_EXECUTION_TIMEOUT: int = 300
45 |
46 | LLM_MODEL: Optional[str] = Field(default="openai/gpt-4o", env="LLM_MODEL") # Format: provider/model_name
47 | LLM_HOST: Optional[str] = Field(default=None, env="LLM_HOST") # Generic host for any provider
48 |
49 | MANAGER_OPENAI_TEMPERATURE: float = 0.2
50 | OPENAI_PLANNER_TEMPERATURE: float = 0.3
51 | OPENAI_EXECUTOR_TEMPERATURE: float = 0.0
52 | OPENAI_VERIFIER_TEMPERATURE: float = 0.2
53 | MODEL_NAME: str = "gpt-4o"
54 | AGENT_TYPE: str = "assistant"
55 | TEMPERATURE: float = 0.2
56 |
57 | class Config:
58 | """Pydantic settings configuration."""
59 |
60 | env_file = ".env"
61 | env_file_encoding = "utf-8"
62 | case_sensitive = True
63 | extra = "ignore" # Allow extra fields from env vars
64 |
65 | def __init__(self, **kwargs):
66 | """Initialize settings and ensure POSTGRES_DATABASE_URL is in the right format."""
67 | super().__init__(**kwargs)
68 |
69 | # Convert SQLAlchemy URL format to Tortoise ORM format if needed
70 | if self.POSTGRES_DATABASE_URL and "postgresql+" in self.POSTGRES_DATABASE_URL:
71 | self.POSTGRES_DATABASE_URL = self.POSTGRES_DATABASE_URL.replace("postgresql+psycopg://", "postgres://")
72 |
73 |
74 | # Global settings instance to be imported by other modules
75 | settings = Settings()
76 |
77 |
78 | def get_settings() -> Settings:
79 | """Return the settings instance."""
80 | return settings
81 |
--------------------------------------------------------------------------------
/engine/src/api/domain/models/__init__.py:
--------------------------------------------------------------------------------
1 | """Domain models package."""
2 |
3 | # Tortoise ORM models
4 | from .user import User
5 | from .conversation import Conversation, Message
6 |
7 | # Pydantic schemas
8 | from .user import UserCreate, UserRead, UserUpdate, UserDB
9 | from .conversation import (
10 | ConversationCreate,
11 | ConversationRead,
12 | ConversationUpdate,
13 | MessageCreate,
14 | MessageRead,
15 | )
16 |
17 | __all__ = [
18 | # Tortoise ORM models
19 | "User",
20 | "Conversation",
21 | "Message",
22 | # Pydantic schemas
23 | "UserCreate",
24 | "UserRead",
25 | "UserUpdate",
26 | "UserDB",
27 | "ConversationCreate",
28 | "ConversationRead",
29 | "ConversationUpdate",
30 | "MessageCreate",
31 | "MessageRead",
32 | ]
33 |
--------------------------------------------------------------------------------
/engine/src/api/domain/models/conversation.py:
--------------------------------------------------------------------------------
1 | """Conversation and Message models for chat history."""
2 |
3 | import uuid
4 | from typing import List, Optional, Dict, Any
5 | from datetime import datetime
6 |
7 | from pydantic import BaseModel
8 | from tortoise import fields
9 | from tortoise.models import Model
10 |
11 |
12 | class Conversation(Model):
13 | """Conversation model for tracking chat sessions."""
14 |
15 | id = fields.UUIDField(pk=True, default=uuid.uuid4)
16 | title = fields.CharField(max_length=255, null=True)
17 | user = fields.ForeignKeyField("models.User", related_name="conversations")
18 | is_active = fields.BooleanField(default=True)
19 | conversation_metadata = fields.JSONField(null=True)
20 | messages_json = fields.JSONField(null=True) # JSONB field for storing messages
21 | created_at = fields.DatetimeField(auto_now_add=True)
22 | updated_at = fields.DatetimeField(auto_now=True)
23 |
24 | # Define relationships
25 | # One-to-many relationship with messages
26 | messages = fields.ReverseRelation["Message"]
27 |
28 | class Meta:
29 | """Tortoise ORM model configuration."""
30 |
31 | table = "conversations"
32 |
33 | def __str__(self) -> str:
34 | """String representation of the conversation."""
35 | return f""
36 |
37 |
38 | class Message(Model):
39 | """Message model for storing individual chat messages."""
40 |
41 | id = fields.UUIDField(pk=True, default=uuid.uuid4)
42 | conversation = fields.ForeignKeyField("models.Conversation", related_name="messages")
43 | role = fields.CharField(max_length=50) # "user", "assistant", "system"
44 | content = fields.TextField()
45 | sequence = fields.IntField() # Order in the conversation
46 | message_metadata = fields.JSONField(null=True) # Additional message metadata
47 | created_at = fields.DatetimeField(auto_now_add=True)
48 |
49 | class Meta:
50 | """Tortoise ORM model configuration."""
51 |
52 | table = "messages"
53 |
54 | def __str__(self) -> str:
55 | """String representation of the message."""
56 | return f""
57 |
58 |
59 | # Pydantic models for API
60 | class MessageCreate(BaseModel):
61 | """Schema for message creation."""
62 |
63 | role: str
64 | content: str
65 | sequence: int
66 | message_metadata: Optional[Dict[str, Any]] = None
67 |
68 |
69 | class MessageRead(BaseModel):
70 | """Schema for reading message data."""
71 |
72 | id: uuid.UUID
73 | role: str
74 | content: str
75 | sequence: int
76 | message_metadata: Optional[Dict[str, Any]] = None
77 | created_at: datetime
78 |
79 | class Config:
80 | """Pydantic model configuration."""
81 |
82 | from_attributes = True
83 |
84 |
85 | class ConversationCreate(BaseModel):
86 | """Schema for conversation creation."""
87 |
88 | title: Optional[str] = None
89 | conversation_metadata: Optional[Dict[str, Any]] = None
90 |
91 |
92 | class ConversationRead(BaseModel):
93 | """Schema for reading conversation data."""
94 |
95 | id: uuid.UUID
96 | title: Optional[str]
97 | user_id: uuid.UUID
98 | is_active: bool
99 | conversation_metadata: Optional[Dict[str, Any]] = None
100 | messages_json: Optional[List[Dict[str, Any]]] = None # Added messages_json field
101 | created_at: datetime
102 | updated_at: datetime
103 | messages: Optional[List[MessageRead]] = None
104 |
105 | class Config:
106 | """Pydantic model configuration."""
107 |
108 | from_attributes = True
109 |
110 |
111 | class ConversationUpdate(BaseModel):
112 | """Schema for updating conversation data."""
113 |
114 | title: Optional[str] = None
115 | is_active: Optional[bool] = None
116 | conversation_metadata: Optional[Dict[str, Any]] = None
117 | messages_json: Optional[List[Dict[str, Any]]] = None # Added messages_json field
118 |
--------------------------------------------------------------------------------
/engine/src/api/domain/models/user.py:
--------------------------------------------------------------------------------
1 | """User model definition."""
2 |
3 | import uuid
4 | from typing import Optional, Literal
5 | from datetime import datetime
6 |
7 | from fastapi_users.schemas import BaseUser, BaseUserCreate, BaseUserUpdate
8 | from tortoise import fields, models
9 |
10 |
11 | class User(models.Model):
12 | """User model for authentication and profile information."""
13 |
14 | id = fields.UUIDField(pk=True, default=uuid.uuid4)
15 | email = fields.CharField(max_length=255, unique=True, index=True)
16 | hashed_password = fields.CharField(max_length=255)
17 | full_name = fields.CharField(max_length=255, null=True)
18 | is_active = fields.BooleanField(default=True)
19 | is_superuser = fields.BooleanField(default=False)
20 | is_verified = fields.BooleanField(default=False)
21 | role = fields.CharField(max_length=20, default="member") # Options: admin, member
22 | created_at = fields.DatetimeField(auto_now_add=True)
23 | updated_at = fields.DatetimeField(auto_now=True)
24 |
25 | # Define relationships
26 | # One-to-many relationship with conversations
27 | conversations = fields.ReverseRelation["Conversation"]
28 |
29 | class Meta:
30 | """Tortoise ORM model configuration."""
31 |
32 | table = "users"
33 |
34 | def __str__(self) -> str:
35 | """String representation of the user."""
36 | return f""
37 |
38 |
39 | # Define Pydantic models for FastAPI Users
40 | class UserCreate(BaseUserCreate):
41 | """Schema for user creation."""
42 |
43 | full_name: Optional[str] = None
44 | role: Optional[str] = "member"
45 |
46 |
47 | class UserRead(BaseUser[uuid.UUID]):
48 | """Schema for reading user data."""
49 |
50 | full_name: Optional[str] = None
51 | role: str
52 | created_at: datetime
53 |
54 | class Config:
55 | from_attributes = True
56 |
57 |
58 | class UserUpdate(BaseUserUpdate):
59 | """Schema for updating user data."""
60 |
61 | full_name: Optional[str] = None
62 | role: Optional[str] = None
63 |
64 |
65 | # Custom model to replace the old BaseUserDB
66 | class UserDB(BaseUser[uuid.UUID]):
67 | """Schema for user in database with hashed password."""
68 |
69 | hashed_password: str
70 | full_name: Optional[str] = None
71 | role: str = "member"
72 | created_at: datetime
73 | updated_at: datetime
74 |
75 | class Config:
76 | from_attributes = True
77 |
--------------------------------------------------------------------------------
/engine/src/api/domain/schemas/team.py:
--------------------------------------------------------------------------------
1 | """Team related schema definitions."""
2 |
3 | from pydantic import BaseModel, EmailStr, Field
4 | from typing import Optional
5 | from datetime import datetime
6 |
7 |
8 | class TeamMemberCreate(BaseModel):
9 | """Schema for creating a team member invitation."""
10 |
11 | email: EmailStr
12 | role: str = Field(default="member", description="Role for the new user")
13 | password: str = Field(description="Initial password for the new user")
14 |
15 |
16 | class TeamMemberUpdate(BaseModel):
17 | """Schema for updating a team member."""
18 |
19 | role: str = Field(description="Updated role for the user")
20 |
21 |
22 | class TeamMemberRead(BaseModel):
23 | """Schema for reading team member information."""
24 |
25 | id: str
26 | email: str
27 | name: str
28 | role: str
29 | status: str # "active", "pending", "inactive"
30 | created_at: str
31 |
32 |
33 | class TeamInvitationRead(BaseModel):
34 | """Schema for reading invitation information."""
35 |
36 | id: str
37 | email: str
38 | role: str
39 | created_at: str
40 | expires_at: Optional[str] = None
41 |
--------------------------------------------------------------------------------
/engine/src/api/repositories/__init__.py:
--------------------------------------------------------------------------------
1 | """Database repositories package."""
2 |
3 | from .database import init_db, close_db_connection, generate_schemas, get_tortoise_config
4 |
5 | __all__ = ["init_db", "close_db_connection", "generate_schemas", "get_tortoise_config"]
6 |
--------------------------------------------------------------------------------
/engine/src/api/repositories/database.py:
--------------------------------------------------------------------------------
1 | """Database connection and initialization using Tortoise ORM."""
2 |
3 | import logging
4 | from typing import List, Dict, Any
5 |
6 | from tortoise import Tortoise
7 |
8 | from ..config import settings
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 | # Construct Tortoise ORM config with direct connection string
13 | TORTOISE_ORM_CONFIG = {
14 | "connections": {"default": str(settings.POSTGRES_DATABASE_URL)},
15 | "apps": {
16 | "models": {
17 | "models": [
18 | "api.domain.models.user",
19 | "api.domain.models.conversation",
20 | "aerich.models",
21 | ],
22 | "default_connection": "default",
23 | }
24 | },
25 | "use_tz": False,
26 | "timezone": "UTC",
27 | }
28 |
29 |
30 | async def init_db() -> None:
31 | """Initialize database connection with Tortoise ORM."""
32 | try:
33 | logger.info("Initializing database connection")
34 | logger.info(f"Using database URL: {settings.POSTGRES_DATABASE_URL}")
35 | await Tortoise.init(config=TORTOISE_ORM_CONFIG)
36 |
37 | logger.info("Database connection established")
38 | except Exception as e:
39 | logger.exception(f"Failed to initialize database: {str(e)}")
40 | raise
41 |
42 |
43 | async def generate_schemas() -> None:
44 | """Generate database schemas.
45 |
46 | This is mainly used for testing. In production, Aerich should handle migrations.
47 | """
48 | try:
49 | logger.info("Generating database schemas")
50 | await Tortoise.generate_schemas()
51 | logger.info("Database schemas generated")
52 | except Exception as e:
53 | logger.exception(f"Failed to generate schemas: {str(e)}")
54 | raise
55 |
56 |
57 | async def close_db_connection() -> None:
58 | """Close database connection."""
59 | try:
60 | logger.info("Closing database connection")
61 | await Tortoise.close_connections()
62 | logger.info("Database connection closed")
63 | except Exception as e:
64 | logger.exception(f"Error closing database connection: {str(e)}")
65 | raise
66 |
67 |
68 | def get_tortoise_config() -> Dict[str, Any]:
69 | """Get Tortoise ORM configuration."""
70 | return TORTOISE_ORM_CONFIG
71 |
--------------------------------------------------------------------------------
/engine/src/api/services/limiter.py:
--------------------------------------------------------------------------------
1 | """Rate limiting service using Redis."""
2 |
3 | import logging
4 | from typing import Optional
5 |
6 | import redis.asyncio as redis
7 | from fastapi_limiter import FastAPILimiter
8 |
9 | from ..config import settings
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | # Redis client instance
14 | _redis_client: Optional[redis.Redis] = None
15 |
16 |
17 | async def init_limiter() -> None:
18 | """Initialize the rate limiter with Redis."""
19 | global _redis_client
20 |
21 | if not settings.RATE_LIMITING_ENABLED:
22 | logger.info("Rate limiting is disabled")
23 | return
24 |
25 | try:
26 | # Create Redis client
27 | _redis_client = redis.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True)
28 |
29 | # Test connection
30 | await _redis_client.ping()
31 |
32 | # Initialize FastAPI limiter
33 | await FastAPILimiter.init(_redis_client)
34 |
35 | logger.info(f"Rate limiter initialized with Redis at {settings.REDIS_URL}")
36 | except Exception as e:
37 | logger.error(f"Failed to initialize rate limiter: {str(e)}")
38 | logger.warning("Rate limiting will be disabled")
39 |
40 |
41 | async def close_limiter() -> None:
42 | """Close the Redis connection for the rate limiter."""
43 | global _redis_client
44 |
45 | if _redis_client is not None:
46 | await _redis_client.close()
47 | _redis_client = None
48 | logger.info("Rate limiter connection closed")
49 |
50 |
51 | async def get_redis_client() -> Optional[redis.Redis]:
52 | """Get the Redis client instance."""
53 | global _redis_client
54 |
55 | if _redis_client is None and settings.RATE_LIMITING_ENABLED:
56 | await init_limiter()
57 |
58 | return _redis_client
59 |
--------------------------------------------------------------------------------
/engine/src/api/utils/__init__.py:
--------------------------------------------------------------------------------
1 | """Utility functions for the API."""
2 |
3 | from .helpers import (
4 | count_message_tokens,
5 | apply_sliding_window,
6 | clear_conversation_history,
7 | get_timestamp,
8 | )
9 |
10 | __all__ = [
11 | "count_message_tokens",
12 | "apply_sliding_window",
13 | "clear_conversation_history",
14 | "get_timestamp",
15 | ]
16 |
--------------------------------------------------------------------------------
/engine/src/api/web/dependencies/__init__.py:
--------------------------------------------------------------------------------
1 | """API dependencies package."""
2 |
3 | from .rate_limit import rate_limit_dependency
4 |
5 | __all__ = ["rate_limit_dependency"]
6 |
--------------------------------------------------------------------------------
/engine/src/api/web/dependencies/rate_limit.py:
--------------------------------------------------------------------------------
1 | """Rate limiting dependencies for API endpoints."""
2 |
3 | from fastapi import Depends, Request
4 | from fastapi_limiter.depends import RateLimiter
5 |
6 | from ...config import settings
7 |
8 |
9 | def get_client_ip(request: Request) -> str:
10 | """Get the client IP address from a request."""
11 | # Check X-Forwarded-For header (for proxy environments)
12 | forwarded = request.headers.get("X-Forwarded-For")
13 | if forwarded:
14 | # Get the first IP in the chain (client IP)
15 | return forwarded.split(",")[0].strip()
16 |
17 | # Fall back to direct client IP
18 | return request.client.host if request.client else "unknown"
19 |
20 |
21 | # Default rate limit dependency for API endpoints
22 | rate_limit_dependency = (
23 | Depends(RateLimiter(times=settings.RATE_LIMIT_PER_MINUTE, seconds=60))
24 | if settings.RATE_LIMITING_ENABLED
25 | else None
26 | )
27 |
--------------------------------------------------------------------------------
/engine/src/api/web/endpoints/__init__.py:
--------------------------------------------------------------------------------
1 | """API endpoints package."""
2 |
3 | from fastapi import APIRouter
4 |
5 | from .health import router as health_router
6 | from .chat import router as chat_router
7 | from .auth import router as auth_router
8 | from .team import router as team_router
9 | from .ws import router as ws_router, sio_app
10 |
11 | # Create main API router
12 | api_router = APIRouter()
13 |
14 | # Include all routers
15 | api_router.include_router(health_router, prefix="/health", tags=["health"])
16 | api_router.include_router(chat_router, prefix="/chat", tags=["chat"])
17 | api_router.include_router(auth_router, prefix="/auth", tags=["auth"])
18 | api_router.include_router(team_router, prefix="/team", tags=["team"])
19 | api_router.include_router(ws_router, prefix="/ws", tags=["websocket"])
20 |
21 | # Export socketio app
22 | socketio_app = sio_app
23 |
24 | __all__ = ["api_router", "socketio_app"]
25 |
--------------------------------------------------------------------------------
/engine/src/api/web/endpoints/health.py:
--------------------------------------------------------------------------------
1 | """Health check endpoints."""
2 |
3 | import logging
4 | from typing import Dict, Any
5 |
6 | from fastapi import APIRouter
7 | from tortoise import Tortoise
8 |
9 | from api.__about__ import __version__
10 |
11 | logger = logging.getLogger(__name__)
12 |
13 | router = APIRouter()
14 |
15 |
16 | @router.get("/", tags=["health"])
17 | async def health_check() -> Dict[str, Any]:
18 | """Check if the API service is healthy."""
19 | logger.debug("Health check endpoint called")
20 | return {
21 | "status": "ok",
22 | "version": __version__,
23 | }
24 |
25 |
26 | @router.get("/database", tags=["health"])
27 | async def database_health_check() -> Dict[str, Any]:
28 | """Check if the database connection is healthy."""
29 | try:
30 | # Check if Tortoise ORM is initialized
31 | conn = Tortoise.get_connection("default")
32 |
33 | # Execute a simple query to check database connection
34 | await conn.execute_query("SELECT 1")
35 |
36 | return {
37 | "status": "ok",
38 | "database": "connected",
39 | }
40 | except Exception as e:
41 | logger.exception("Database health check failed")
42 | return {
43 | "status": "error",
44 | "database": "disconnected",
45 | "error": str(e),
46 | }
47 |
--------------------------------------------------------------------------------
/engine/src/api/web/middleware/__init__.py:
--------------------------------------------------------------------------------
1 | """API middleware package."""
2 |
3 | from fastapi import FastAPI
4 | from .logging_middleware import LoggingMiddleware
5 |
6 |
7 | def setup_middleware(app: FastAPI) -> None:
8 | """Set up all middleware for the application."""
9 | # Add logging middleware
10 | app.add_middleware(LoggingMiddleware)
11 |
12 | # Note: Rate limiting is now handled directly by FastAPI-Limiter
13 | # via dependency injection rather than middleware
14 | # See services/limiter.py for configuration
15 |
16 |
17 | __all__ = ["setup_middleware"]
18 |
--------------------------------------------------------------------------------
/engine/src/api/web/middleware/logging_middleware.py:
--------------------------------------------------------------------------------
1 | """Logging middleware for request/response tracking."""
2 |
3 | import logging
4 | import time
5 | from typing import Callable
6 |
7 | from fastapi import Request, Response
8 | from starlette.middleware.base import BaseHTTPMiddleware
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | class LoggingMiddleware(BaseHTTPMiddleware):
14 | """Middleware to log request and response details."""
15 |
16 | async def dispatch(self, request: Request, call_next: Callable) -> Response:
17 | """Process the request/response and log details."""
18 | request_id = request.headers.get("X-Request-ID", "unknown")
19 | start_time = time.time()
20 |
21 | # Log the incoming request
22 | logger.debug(f"Request started [id={request_id}] {request.method} {request.url.path}")
23 |
24 | # Process the request
25 | try:
26 | response = await call_next(request)
27 |
28 | # Calculate request processing time
29 | process_time = time.time() - start_time
30 |
31 | # Log the response
32 | logger.debug(
33 | f"Request completed [id={request_id}] {request.method} {request.url.path} "
34 | f"status={response.status_code} duration={process_time:.4f}s"
35 | )
36 |
37 | # Add processing time header
38 | response.headers["X-Process-Time"] = str(process_time)
39 | return response
40 |
41 | except Exception as e:
42 | # Log any exceptions
43 | logger.exception(
44 | f"Request failed [id={request_id}] {request.method} {request.url.path}: {str(e)}"
45 | )
46 | raise
47 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/__init__.py:
--------------------------------------------------------------------------------
1 | """Workflow package for agent orchestration."""
2 |
3 | from .manager import WorkflowManager
4 |
5 | __all__ = ["WorkflowManager"]
6 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/__init__.py:
--------------------------------------------------------------------------------
1 | """Agent implementations for the workflow system."""
2 |
3 | from .base import BaseAgent, BaseAgentState, BaseAgentConfig
4 | from .planner import PlannerAgent, PlannerState, PlannerConfig
5 | from .executor import ExecutorAgent, ExecutorState, ExecutorConfig
6 | from .verifier import VerifierAgent, VerifierState, VerifierConfig
7 |
8 | __all__ = [
9 | "BaseAgent",
10 | "BaseAgentState",
11 | "BaseAgentConfig",
12 | "PlannerAgent",
13 | "PlannerState",
14 | "PlannerConfig",
15 | "ExecutorAgent",
16 | "ExecutorState",
17 | "ExecutorConfig",
18 | "VerifierAgent",
19 | "VerifierState",
20 | "VerifierConfig",
21 | ]
22 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/executor/__init__.py:
--------------------------------------------------------------------------------
1 | """Executor agent package."""
2 |
3 | from .main import ExecutorAgent
4 | from .types import ExecutorState, ExecutorConfig, ExecutionMetrics, ToolMetrics
5 |
6 | __all__ = [
7 | "ExecutorAgent",
8 | "ExecutorState",
9 | "ExecutorConfig",
10 | "ExecutionMetrics",
11 | "ToolMetrics",
12 | ]
13 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/executor/prompt_templates.py:
--------------------------------------------------------------------------------
1 | """Prompt templates for executor agent."""
2 |
3 | EXECUTOR_SYSTEM_PROMPT = """You are a cloud native expert.
4 |
5 | Your task is to execute the user's query on the Kubernetes cluster.
6 | """
7 |
8 | RESOURCE_RESOLUTION_SYSTEM_PROMPT = """You are a parameter resolution assistant for Kubernetes operations.
9 | Your task is to extract resource names from previous command outputs to resolve placeholder references.
10 |
11 | IMPORTANT INSTRUCTIONS:
12 | 1. Extract EXACT resource names from the previous step outputs based on the parameter name
13 | 2. Match resource names with the user query's intent
14 | 3. If multiple resources match, return them as a comma-separated list
15 | 4. If no matches are found, return a reasonable fallback based on the user query
16 | 5. NEVER return placeholder text like {{EXTRACTED_FROM_STEP_X}}
17 | 6. Return plain values only - no placeholders or template variables
18 |
19 | Your response format:
20 | {
21 | "parameter_name": "resolved_value",
22 | "parameter_name2": "resolved_value2"
23 | }"""
24 |
25 | POD_SELECTION_SYSTEM_PROMPT = """You are an AI assistant specialized in Kubernetes pod selection. Your task is to analyze pod information and select the specific pod names that match the user's intent.
26 |
27 | Key Objectives:
28 | 1. Return ONLY the pod names that are relevant to the user's query
29 | 2. Each pod name should be on its own line
30 | 3. Do not include any explanations, just the pod names
31 | 4. If there are no relevant pods, return an empty response
32 |
33 | Response Format:
34 | pod-name-1
35 | pod-name-2
36 | pod-name-3"""
37 |
38 | SUMMARIZATION_SYSTEM_PROMPT = """You are an AI specialized in summarizing Kubernetes execution data.
39 | Your task is to analyze execution state and create a concise summary of the most relevant information so far.
40 |
41 | Guidelines:
42 | 1. Focus on information relevant to continuing the execution of further steps
43 | 2. Preserve exact resource names, namespaces, and identifiers needed for future steps
44 | 3. Extract key facts and outputs from previous steps that might be referenced later
45 | 4. Maintain a list of discovered resources by type (pods, services, deployments, etc.)
46 | 5. Keep error messages and important command outputs
47 | 6. Summarize in a structured format that maintains data relationships
48 | 7. Return only a JSON structure with the summarized state"""
49 |
50 | # User prompt templates
51 | SUMMARIZATION_USER_PROMPT = """Original user query: {user_query}
52 |
53 | Current execution state to summarize (will be cleared after summary):
54 | {execution_state}
55 |
56 | Create a concise summary of the execution state so far, focusing on information needed for future steps.
57 | Return ONLY a JSON object with the summarized state."""
58 |
59 | RESOURCE_RESOLUTION_USER_PROMPT = """
60 | Parameters to resolve: {placeholder_info}
61 |
62 | User query: {user_query}
63 |
64 | Available resource data from previous steps:
65 | {resource_context}
66 |
67 | Current execution context:
68 | {context}
69 |
70 | Resolve each placeholder parameter to actual resource names based on the available data.
71 | Return ONLY a JSON object containing parameter names and their resolved values."""
72 |
73 | POD_SELECTION_USER_PROMPT = """
74 | Workflow Analysis:
75 |
76 | 1. Original User Query:
77 | {user_query}
78 |
79 | 2. Workflow Context:
80 | - Step Count: {step_count}
81 | - Last Tool: {last_tool}
82 | - Target Namespace: {target_namespace}
83 | - Step Details: {step_details}
84 |
85 | 3. Available Pod Information:
86 | {pod_output}
87 |
88 | Your Task:
89 | Based on the user query "{user_query}", extract and return ONLY the pod names from the available pod information that are relevant to this query.
90 | Return each pod name on a separate line with no additional text."""
91 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/executor/types.py:
--------------------------------------------------------------------------------
1 | """Type definitions for executor agent."""
2 |
3 | from typing import Dict, Any, Optional, List
4 | from datetime import datetime
5 | from pydantic import Field, BaseModel
6 |
7 | from ..base import BaseAgentState, BaseAgentConfig
8 |
9 |
10 | class ExecutionMetrics(BaseModel):
11 | """Metrics for tool execution."""
12 |
13 | total_executions: int = 0
14 | successful_executions: int = 0
15 | failed_executions: int = 0
16 | total_retries: int = 0
17 | average_execution_time: float = 0.0
18 | last_execution_time: Optional[datetime] = None
19 |
20 |
21 | class ToolMetrics(BaseModel):
22 | """Metrics for individual tools."""
23 |
24 | success_rate: float = 0.0
25 | average_latency: float = 0.0
26 | error_frequency: Dict[str, int] = Field(default_factory=dict)
27 | last_success: Optional[datetime] = None
28 | last_failure: Optional[datetime] = None
29 |
30 |
31 | class ParameterItem(BaseModel):
32 | """Represents a single parameter with a fixed name and value."""
33 |
34 | name: str = Field(description="The name of the parameter")
35 | value: Any = Field(description="The value of the parameter")
36 |
37 |
38 | class ExecutorState(BaseAgentState):
39 | """State for executor agent."""
40 |
41 | type: str = Field(default="ExecutorState")
42 | execution_metrics: ExecutionMetrics = Field(default_factory=ExecutionMetrics)
43 | tool_metrics: Dict[str, ToolMetrics] = Field(default_factory=dict)
44 | tool_info_cache: Dict[str, Dict[str, Any]] = Field(default_factory=dict)
45 | token_count: int = Field(default=0)
46 | summarization_history: List[Dict[str, Any]] = Field(default_factory=list)
47 | max_token_limit: int = Field(default=20000)
48 |
49 |
50 | class ExecutorConfig(BaseAgentConfig):
51 | """Configuration for executor agent."""
52 |
53 | name: str = Field(default="executor")
54 | system_message: str = Field(
55 | default="You are an executor agent responsible for implementing planned operations."
56 | )
57 | max_retries: int = Field(default=3)
58 | retry_delay: int = Field(default=1)
59 | timeout: int = Field(default=300) # 5 minutes
60 | tool_info_cache_ttl: int = Field(default=300) # 5 minutes
61 | max_token_limit: int = Field(default=20000) # Maximum token limit before summarization
62 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/planner/__init__.py:
--------------------------------------------------------------------------------
1 | """Planner agent package for workflow execution."""
2 |
3 | from .main import PlannerAgent
4 | from .types import PlannerState, PlannerConfig, ToolDependency
5 | from .prompt_templates import PLANNER_SYSTEM_MESSAGE, ANALYZE_QUERY_PROMPT
6 |
7 | __all__ = [
8 | "PlannerAgent",
9 | "PlannerState",
10 | "PlannerConfig",
11 | "ToolDependency",
12 | "PLANNER_SYSTEM_MESSAGE",
13 | "ANALYZE_QUERY_PROMPT",
14 | ]
15 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/planner/types.py:
--------------------------------------------------------------------------------
1 | """Pydantic models and types for planner agent."""
2 |
3 | from typing import Dict, Any, Optional
4 | from pydantic import BaseModel, Field
5 | from datetime import datetime
6 |
7 |
8 | class ToolDependency:
9 | """Represents a tool and its dependencies."""
10 |
11 | def __init__(self, tool, provides=None, weight=1):
12 | """Initialize a tool dependency.
13 |
14 | Args:
15 | tool: The tool identifier
16 | provides: The capabilities this tool provides
17 | weight: The complexity weight of this tool
18 | """
19 | self.tool = tool
20 | self.provides = provides or []
21 | self.weight = weight
22 | self.required_by = []
23 |
24 |
25 | class ExecutionMetrics(BaseModel):
26 | """Execution metrics for planner."""
27 |
28 | total_plans: int = 0
29 | successful_plans: int = 0
30 | failed_plans: int = 0
31 | last_plan_time: Optional[datetime] = None
32 |
33 |
34 | class PlannerState(BaseModel):
35 | """State for planner agent."""
36 |
37 | inner_state: Dict[str, Any] = Field(default_factory=dict)
38 | execution_metrics: ExecutionMetrics = Field(default_factory=ExecutionMetrics)
39 | tool_graph: Dict[str, Any] = Field(default_factory=dict)
40 | cached_plans: Dict[str, Any] = Field(default_factory=dict)
41 |
42 |
43 | class PlannerConfig(BaseModel):
44 | """Configuration for planner agent."""
45 |
46 | llm_temperature: float = 0.6
47 | max_plan_cache_size: int = 100
48 | min_wait_after_write: int = 5
49 | max_wait_after_write: int = 30
50 | verify_resources_exist: bool = True
51 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/verifier/__init__.py:
--------------------------------------------------------------------------------
1 | """Verifier agent module."""
2 |
3 | from .main import VerifierAgent
4 | from .types import ValidationResult, VerificationMetrics, VerifierState, VerifierConfig
5 |
6 | __all__ = [
7 | "VerifierAgent",
8 | "ValidationResult",
9 | "VerificationMetrics",
10 | "VerifierState",
11 | "VerifierConfig",
12 | ]
13 |
--------------------------------------------------------------------------------
/engine/src/api/workflow/agents/verifier/types.py:
--------------------------------------------------------------------------------
1 | """Type definitions for verifier agent."""
2 |
3 | from typing import Dict, Any, List, Optional
4 | from datetime import datetime
5 | from pydantic import Field, BaseModel
6 |
7 | from ..base import BaseAgentState, BaseAgentConfig
8 |
9 |
10 | class ValidationResult(BaseModel):
11 | """Model for validation results."""
12 |
13 | matches_intent: bool
14 | issues: List[str] = []
15 | confidence: float
16 | reasoning: str
17 | recommendations: List[str] = []
18 | metrics: Dict[str, Any] = {}
19 |
20 |
21 | class VerificationMetrics(BaseModel):
22 | """Metrics for verification process."""
23 |
24 | total_verifications: int = 0
25 | successful_verifications: int = 0
26 | failed_verifications: int = 0
27 | average_confidence: float = 0.0
28 | common_issues: Dict[str, int] = Field(default_factory=dict)
29 | last_verification_time: Optional[datetime] = None
30 |
31 |
32 | class VerifierState(BaseAgentState):
33 | """State for verifier agent."""
34 |
35 | type: str = Field(default="VerifierState")
36 | verification_metrics: VerificationMetrics = Field(default_factory=VerificationMetrics)
37 | validation_history: List[ValidationResult] = Field(default_factory=list)
38 |
39 |
40 | class VerifierConfig(BaseAgentConfig):
41 | """Configuration for verifier agent."""
42 |
43 | name: str = Field(default="verifier")
44 | system_message: str = Field(
45 | default="You are a verifier agent responsible for validating execution results."
46 | )
47 | max_retries: int = Field(default=3)
48 | retry_delay: int = Field(default=1)
49 | timeout: int = Field(default=300) # 5 minutes
50 |
51 | # New configuration options
52 | batch_verification: bool = Field(
53 | default=True,
54 | description="Whether to verify multiple criteria in a single LLM call when possible",
55 | )
56 | generate_summary: bool = Field(
57 | default=True, description="Whether to generate a summary of all verification results"
58 | )
59 | summary_confidence_threshold: float = Field(
60 | default=0.7, description="Confidence threshold for accepting summary results"
61 | )
62 | use_detailed_context: bool = Field(
63 | default=True,
64 | description="Whether to include detailed execution context in verification prompts",
65 | )
66 |
--------------------------------------------------------------------------------
/kubernetes-controller/.gitignore:
--------------------------------------------------------------------------------
1 | # If you prefer the allow list template instead of the deny list, see community template:
2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3 | #
4 | # Binaries for programs and plugins
5 | *.exe
6 | *.exe~
7 | *.dll
8 | *.so
9 | *.dylib
10 |
11 | # Test binary, built with `go test -c`
12 | *.test
13 |
14 | # Output of the go coverage tool, specifically when used with LiteIDE
15 | *.out
16 |
17 | # Dependency directories (remove the comment below to include it)
18 | # vendor/
19 |
20 | # Go workspace file
21 | go.work
22 | go.work.sum
23 |
24 | # env file
25 | .env
--------------------------------------------------------------------------------
/kubernetes-controller/cmd/manager/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "os"
6 |
7 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
8 | _ "k8s.io/client-go/plugin/pkg/client/auth"
9 |
10 | "k8s.io/apimachinery/pkg/runtime"
11 | utilruntime "k8s.io/apimachinery/pkg/util/runtime"
12 | clientgoscheme "k8s.io/client-go/kubernetes/scheme"
13 | ctrl "sigs.k8s.io/controller-runtime"
14 | "sigs.k8s.io/controller-runtime/pkg/healthz"
15 | "sigs.k8s.io/controller-runtime/pkg/log/zap"
16 | )
17 |
18 | var (
19 | scheme = runtime.NewScheme()
20 | setupLog = ctrl.Log.WithName("setup")
21 | )
22 |
23 | func init() {
24 | utilruntime.Must(clientgoscheme.AddToScheme(scheme))
25 | // +kubebuilder:scaffold:scheme
26 | }
27 |
28 | func main() {
29 | var metricsAddr string
30 | var enableLeaderElection bool
31 | var probeAddr string
32 |
33 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
34 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
35 | flag.BoolVar(&enableLeaderElection, "leader-elect", false,
36 | "Enable leader election for controller manager. "+
37 | "Enabling this will ensure there is only one active controller manager.")
38 |
39 | opts := zap.Options{
40 | Development: true,
41 | }
42 | opts.BindFlags(flag.CommandLine)
43 | flag.Parse()
44 |
45 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
46 |
47 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
48 | Scheme: scheme,
49 | HealthProbeBindAddress: probeAddr,
50 | LeaderElection: enableLeaderElection,
51 | LeaderElectionID: "skyflo-controller.skyflo.ai",
52 | WebhookServer: nil, // We'll configure webhooks later when needed
53 | })
54 | if err != nil {
55 | setupLog.Error(err, "unable to start manager")
56 | os.Exit(1)
57 | }
58 |
59 | // TODO: Register controllers here once we create them
60 |
61 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
62 | setupLog.Error(err, "unable to set up health check")
63 | os.Exit(1)
64 | }
65 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
66 | setupLog.Error(err, "unable to set up ready check")
67 | os.Exit(1)
68 | }
69 |
70 | setupLog.Info("starting manager")
71 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
72 | setupLog.Error(err, "problem running manager")
73 | os.Exit(1)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/kubernetes-controller/config/rbac/role.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: manager-role
6 | rules:
7 | - apiGroups:
8 | - apps
9 | resources:
10 | - deployments
11 | verbs:
12 | - create
13 | - delete
14 | - get
15 | - list
16 | - patch
17 | - update
18 | - watch
19 | - apiGroups:
20 | - ""
21 | resources:
22 | - secrets
23 | verbs:
24 | - get
25 | - list
26 | - watch
27 | - apiGroups:
28 | - ""
29 | resources:
30 | - services
31 | verbs:
32 | - create
33 | - delete
34 | - get
35 | - list
36 | - patch
37 | - update
38 | - watch
39 | - apiGroups:
40 | - skyflo.ai
41 | resources:
42 | - skyfloais
43 | verbs:
44 | - create
45 | - delete
46 | - get
47 | - list
48 | - patch
49 | - update
50 | - watch
51 | - apiGroups:
52 | - skyflo.ai
53 | resources:
54 | - skyfloais/finalizers
55 | verbs:
56 | - update
57 | - apiGroups:
58 | - skyflo.ai
59 | resources:
60 | - skyfloais/status
61 | verbs:
62 | - get
63 | - patch
64 | - update
65 |
--------------------------------------------------------------------------------
/kubernetes-controller/engine/v1/groupversion_info.go:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 Skyflo.ai.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | // Package v1 contains API Schema definitions for the skyflo v1 API group
18 | // +kubebuilder:object:generate=true
19 | // +groupName=skyflo.ai
20 | package v1
21 |
22 | import (
23 | "k8s.io/apimachinery/pkg/runtime/schema"
24 | "sigs.k8s.io/controller-runtime/pkg/scheme"
25 | )
26 |
27 | var (
28 | // GroupVersion is group version used to register these objects
29 | GroupVersion = schema.GroupVersion{Group: "skyflo.ai", Version: "v1"}
30 |
31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme
32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
33 |
34 | // AddToScheme adds the types in this group-version to the given scheme.
35 | AddToScheme = SchemeBuilder.AddToScheme
36 | )
37 |
--------------------------------------------------------------------------------
/kubernetes-controller/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/skyflo-ai/skyflo/kubernetes-controller
2 |
3 | go 1.24.1
4 |
5 | require (
6 | k8s.io/api v0.29.2
7 | k8s.io/apimachinery v0.29.2
8 | k8s.io/client-go v0.29.2
9 | sigs.k8s.io/controller-runtime v0.17.2
10 | )
11 |
12 | require (
13 | github.com/beorn7/perks v1.0.1 // indirect
14 | github.com/cespare/xxhash/v2 v2.2.0 // indirect
15 | github.com/davecgh/go-spew v1.1.1 // indirect
16 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect
17 | github.com/evanphx/json-patch/v5 v5.8.0 // indirect
18 | github.com/fsnotify/fsnotify v1.7.0 // indirect
19 | github.com/go-logr/logr v1.4.1 // indirect
20 | github.com/go-logr/zapr v1.3.0 // indirect
21 | github.com/go-openapi/jsonpointer v0.19.6 // indirect
22 | github.com/go-openapi/jsonreference v0.20.2 // indirect
23 | github.com/go-openapi/swag v0.22.3 // indirect
24 | github.com/gogo/protobuf v1.3.2 // indirect
25 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
26 | github.com/golang/protobuf v1.5.3 // indirect
27 | github.com/google/gnostic-models v0.6.8 // indirect
28 | github.com/google/go-cmp v0.6.0 // indirect
29 | github.com/google/gofuzz v1.2.0 // indirect
30 | github.com/google/uuid v1.3.0 // indirect
31 | github.com/imdario/mergo v0.3.6 // indirect
32 | github.com/josharian/intern v1.0.0 // indirect
33 | github.com/json-iterator/go v1.1.12 // indirect
34 | github.com/mailru/easyjson v0.7.7 // indirect
35 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
36 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
37 | github.com/modern-go/reflect2 v1.0.2 // indirect
38 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
39 | github.com/pkg/errors v0.9.1 // indirect
40 | github.com/prometheus/client_golang v1.18.0 // indirect
41 | github.com/prometheus/client_model v0.5.0 // indirect
42 | github.com/prometheus/common v0.45.0 // indirect
43 | github.com/prometheus/procfs v0.12.0 // indirect
44 | github.com/spf13/pflag v1.0.5 // indirect
45 | go.uber.org/multierr v1.11.0 // indirect
46 | go.uber.org/zap v1.26.0 // indirect
47 | golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
48 | golang.org/x/net v0.19.0 // indirect
49 | golang.org/x/oauth2 v0.12.0 // indirect
50 | golang.org/x/sys v0.16.0 // indirect
51 | golang.org/x/term v0.15.0 // indirect
52 | golang.org/x/text v0.14.0 // indirect
53 | golang.org/x/time v0.3.0 // indirect
54 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
55 | google.golang.org/appengine v1.6.7 // indirect
56 | google.golang.org/protobuf v1.31.0 // indirect
57 | gopkg.in/inf.v0 v0.9.1 // indirect
58 | gopkg.in/yaml.v2 v2.4.0 // indirect
59 | gopkg.in/yaml.v3 v3.0.1 // indirect
60 | k8s.io/apiextensions-apiserver v0.29.0 // indirect
61 | k8s.io/component-base v0.29.0 // indirect
62 | k8s.io/klog/v2 v2.110.1 // indirect
63 | k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
64 | k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
65 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
66 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
67 | sigs.k8s.io/yaml v1.4.0 // indirect
68 | )
69 |
--------------------------------------------------------------------------------
/kubernetes-controller/hack/boilerplate.go.txt:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright 2024 Skyflo.ai.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
--------------------------------------------------------------------------------
/mcp/.env.example:
--------------------------------------------------------------------------------
1 | # MCP Server Settings
2 | APP_NAME=Skyflo.ai - MCP Server
3 | APP_VERSION=0.1.0
4 | APP_DESCRIPTION=MCP Server for Skyflo.ai
5 | DEBUG=false
6 |
7 | # Retry Configuration
8 | MAX_RETRY_ATTEMPTS=3
9 | RETRY_BASE_DELAY=60
10 | RETRY_MAX_DELAY=300
11 | RETRY_EXPONENTIAL_BASE=2.0
12 |
13 | # Server Settings
14 | LOG_LEVEL=INFO
--------------------------------------------------------------------------------
/mcp/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # UV
98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | #uv.lock
102 |
103 | # poetry
104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | # This is especially recommended for binary packages to ensure reproducibility, and is more
106 | # commonly ignored for libraries.
107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 |
110 | # pdm
111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | # in version control.
115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 |
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 |
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 |
127 | # SageMath parsed files
128 | *.sage.py
129 |
130 | # Environments
131 | .env
132 | .env.*.local
133 | .venv
134 | env/
135 | venv/
136 | env.bak/
137 | venv.bak/
138 |
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 |
143 | # Rope project settings
144 | .ropeproject
145 |
146 | # mkdocs documentation
147 | /site
148 |
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 |
154 | # Pyre type checker
155 | .pyre/
156 |
157 | # pytype static type analyzer
158 | .pytype/
159 |
160 | # Cython debug symbols
161 | cython_debug/
162 |
163 | # PyCharm
164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | # and can be added to the global gitignore or merged into this file. For a more nuclear
167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 |
170 | # Ruff stuff:
171 | .ruff_cache/
172 |
173 | # PyPI configuration file
174 | .pypirc
175 |
176 |
--------------------------------------------------------------------------------
/mcp/.python-version:
--------------------------------------------------------------------------------
1 | 3.11
2 |
--------------------------------------------------------------------------------
/mcp/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "skyflo-mcp"
7 | dynamic = ["version"]
8 | description = 'Skyflo.ai MCP Server - Open Source AI Agent for Cloud Native'
9 | readme = "README.md"
10 | requires-python = ">=3.11"
11 | license = "Apache-2.0"
12 | keywords = ["ai", "agent", "cloud native", "open source"]
13 | authors = [
14 | { name = "Karan Jagtiani", email = "karan@skyflo.ai" },
15 | ]
16 |
17 | dependencies = [
18 | "autogen-agentchat>=0.4.9.2",
19 | "autogen-core>=0.4.9.2",
20 | "autogen-ext[mcp,openai]>=0.4.9.2",
21 | "autogenstudio>=0.4.2",
22 | "langgraph>=0.3.18",
23 | "mcp>=1.5.0",
24 | "pydantic>=2.10.6",
25 | "python-decouple>=3.8",
26 | "uvicorn>=0.27.1",
27 | "typer>=0.9.0",
28 | "tiktoken>=0.6.0",
29 | "fastapi>=0.115.11",
30 | "fastapi-users[tortoise-orm]>=14.0.1",
31 | "casbin>=1.41.0",
32 | "tortoise-orm>=0.24.2",
33 | "aerich>=0.8.2",
34 | "broadcaster>=0.3.1",
35 | "fastapi-limiter>=0.1.6",
36 | "fastapi-websocket-pubsub>=0.3.9",
37 | ]
38 |
39 | [project.optional-dependencies]
40 | default = [
41 | "pytest>=8.0.2",
42 | "pytest-cov>=4.1.0",
43 | "pytest-asyncio>=0.23.5",
44 | "mypy>=1.8.0",
45 | "pyyaml>=6.0.1",
46 | ]
47 |
48 | [project.scripts]
49 | skyflo-mcp = "mcp.main:run"
50 |
51 | [project.urls]
52 | Documentation = "https://github.com/skyflo-ai/skyflo#readme"
53 | Issues = "https://github.com/skyflo-ai/skyflo/issues"
54 | Source = "https://github.com/skyflo-ai/skyflo"
55 |
56 | [tool.hatch.version]
57 | path = "src/mcp_server/__about__.py"
58 |
59 | [tool.hatch.build.targets.wheel]
60 | packages = ["src/mcp_server"]
61 |
62 | [tool.hatch.envs.default]
63 | dependencies = [
64 | "pytest>=8.0.2",
65 | "pytest-cov>=4.1.0",
66 | "pytest-asyncio>=0.23.5",
67 | "mypy>=1.8.0",
68 | "pyyaml>=6.0.1",
69 | ]
70 |
71 | [tool.hatch.envs.default.scripts]
72 | test = "pytest {args:tests}"
73 | test-cov = "pytest --cov {args:src/mcp_server}"
74 | type-check = "mypy --install-types --non-interactive {args:src/mcp_server tests}"
75 |
76 | [tool.coverage.run]
77 | source_pkgs = ["mcp", "tests"]
78 | branch = true
79 | parallel = true
80 | omit = [
81 | "src/mcp_server/__about__.py",
82 | ]
83 |
84 | [tool.coverage.paths]
85 | mcp = ["src/mcp_server", "*/mcp/src/mcp_server"]
86 | tests = ["tests", "*/mcp/tests"]
87 |
88 | [tool.coverage.report]
89 | exclude_lines = [
90 | "no cov",
91 | "if __name__ == .__main__.:",
92 | "if TYPE_CHECKING:",
93 | ]
94 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/__about__.py:
--------------------------------------------------------------------------------
1 | """Version information."""
2 |
3 | __version__ = "0.1.0"
4 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Skyflo.ai MCP Server - Open Source AI Agent for Cloud Native.
3 | """
4 |
5 | from importlib import metadata as importlib_metadata
6 |
7 |
8 | def get_version() -> str:
9 | try:
10 | return importlib_metadata.version(__name__)
11 | except importlib_metadata.PackageNotFoundError: # pragma: no cover
12 | return "unknown"
13 |
14 |
15 | __version__ = get_version()
16 |
17 | __all__ = ["run"]
18 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/api/__init__.py:
--------------------------------------------------------------------------------
1 | """FastAPI integration for Skyflo.ai MCP Server."""
2 |
3 | from fastapi import FastAPI
4 | from fastapi.middleware.cors import CORSMiddleware
5 |
6 | app = FastAPI(
7 | title="Skyflo.ai MCP Server API",
8 | description="API for Skyflo.ai MCP Server - Cloud Native operations through natural language",
9 | version="1.0.0",
10 | )
11 |
12 | # Configure CORS
13 | app.add_middleware(
14 | CORSMiddleware,
15 | allow_origins=["*"], # In production, replace with specific origins
16 | allow_credentials=True,
17 | allow_methods=["*"],
18 | allow_headers=["*"],
19 | )
20 |
21 | # Import and include routers
22 | from .v1 import router as v1_router
23 |
24 | app.include_router(v1_router, prefix="/mcp/v1")
25 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/api/v1/__init__.py:
--------------------------------------------------------------------------------
1 | """V1 API router for Skyflo.ai MCP Server."""
2 |
3 | from fastapi import APIRouter
4 | from .tools import router as tools_router
5 | from .health import router as health_router
6 |
7 | router = APIRouter()
8 | router.include_router(tools_router, prefix="/tools", tags=["tools"])
9 | router.include_router(health_router, prefix="/health", tags=["health"])
10 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/api/v1/health.py:
--------------------------------------------------------------------------------
1 | """Health check endpoints for Kubernetes probes."""
2 |
3 | from fastapi import APIRouter
4 |
5 | router = APIRouter()
6 |
7 | @router.get("")
8 | async def health_check():
9 | """Basic health check endpoint for Kubernetes liveness and readiness probes."""
10 | return {"status": "healthy"}
--------------------------------------------------------------------------------
/mcp/src/mcp_server/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/mcp/src/mcp_server/config/__init__.py
--------------------------------------------------------------------------------
/mcp/src/mcp_server/config/settings.py:
--------------------------------------------------------------------------------
1 | """Configuration settings for Skyflo.ai MCP Server."""
2 |
3 | from typing import List
4 | from pydantic import validator
5 | from pydantic_settings import BaseSettings
6 |
7 |
8 | class Settings(BaseSettings):
9 | """MCP Server configuration settings.
10 |
11 | These settings can be configured using environment variables.
12 | """
13 |
14 | # Application Configuration
15 | APP_NAME: str
16 | APP_VERSION: str
17 | APP_DESCRIPTION: str
18 | DEBUG: bool = False
19 |
20 | # Server Settings
21 | LOG_LEVEL: str = "INFO"
22 |
23 | # Retry Configuration
24 | MAX_RETRY_ATTEMPTS: int = 3
25 | RETRY_BASE_DELAY: float = 60 # Base delay in seconds
26 | RETRY_MAX_DELAY: float = 60 * 5 # Maximum delay in seconds
27 | RETRY_EXPONENTIAL_BASE: float = 2.0 # Base for exponential backoff
28 |
29 | class Config:
30 | """Pydantic settings configuration."""
31 | env_file = ".env"
32 | env_file_encoding = "utf-8"
33 | case_sensitive = True
34 |
35 |
36 | # Global settings instance to be imported by other modules
37 | settings = Settings()
38 |
39 |
40 | def get_settings() -> Settings:
41 | """Return the settings instance."""
42 | return settings
43 |
44 |
45 | # Legacy function to support existing code
46 | def get_config() -> Settings:
47 | """Legacy function that returns the settings instance."""
48 | return settings
49 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/__init__.py:
--------------------------------------------------------------------------------
1 | """Skyflo.ai MCP Server tools package."""
2 |
3 | from . import k8s, argo, helm
4 |
5 | __all__ = ["k8s", "argo", "helm"]
6 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/argo/__init__.py:
--------------------------------------------------------------------------------
1 | """Argo tools package."""
2 |
3 | from ._argo_rollouts import (
4 | get_rollouts,
5 | promote_rollout,
6 | pause_rollout,
7 | set_rollout_image,
8 | rollout_restart,
9 | )
10 |
11 | __all__ = [
12 | "get_rollouts",
13 | "promote_rollout",
14 | "pause_rollout",
15 | "set_rollout_image",
16 | "rollout_restart",
17 | ]
18 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/argo/_argo_rollouts.py:
--------------------------------------------------------------------------------
1 | """Argo Rollouts tools implementation."""
2 |
3 | from typing import Annotated, Optional
4 | from autogen_core.tools import FunctionTool
5 |
6 | from .._utils import create_typed_fn_tool, run_command
7 |
8 |
9 | async def _run_argo_command(command: str) -> str:
10 | """Run an argo rollouts command and return its output."""
11 | # Split the command and remove empty strings
12 | cmd_parts = [part for part in command.split(" ") if part]
13 | return await run_command("kubectl", ["argo", "rollouts"] + cmd_parts)
14 |
15 |
16 | async def _get_rollouts(
17 | namespace: Annotated[Optional[str], "The namespace of the rollout"],
18 | ) -> str:
19 | """Get all Argo rollouts using kubectl get command since 'get rollouts' is not supported."""
20 | # Use kubectl get directly since 'argo rollouts get rollouts' is not supported
21 | return await run_command(
22 | "kubectl",
23 | ["get", "rollouts.argoproj.io", "-o", "wide"]
24 | + (["-n", namespace] if namespace else []),
25 | )
26 |
27 |
28 | async def _promote_rollout(
29 | name: Annotated[str, "The name of the rollout to promote"],
30 | namespace: Annotated[Optional[str], "The namespace of the rollout"],
31 | full: Annotated[
32 | Optional[bool],
33 | "Whether to do a full promotion (skip analysis, pauses, and steps)",
34 | ] = False,
35 | ) -> str:
36 | """Promote an Argo rollout."""
37 | return await _run_argo_command(
38 | f"promote {name} {f'-n {namespace}' if namespace else ''} {'--full' if full else ''}"
39 | )
40 |
41 |
42 | async def _pause_rollout(
43 | name: Annotated[str, "The name of the rollout to pause"],
44 | namespace: Annotated[Optional[str], "The namespace of the rollout"],
45 | ) -> str:
46 | """Pause an Argo rollout."""
47 | return await _run_argo_command(
48 | f"pause {name} {f'-n {namespace}' if namespace else ''}"
49 | )
50 |
51 |
52 | async def _set_rollout_image(
53 | name: Annotated[str, "The name of the rollout"],
54 | container_image: Annotated[
55 | str, "The container image to set (format: container=image)"
56 | ],
57 | namespace: Annotated[Optional[str], "The namespace of the rollout"],
58 | ) -> str:
59 | """Set the image for a container in an Argo rollout."""
60 | return await _run_argo_command(
61 | f"set image {name} {container_image} {f'-n {namespace}' if namespace else ''}"
62 | )
63 |
64 |
65 | async def _rollout_restart(
66 | name: Annotated[str, "The name of the rollout"],
67 | namespace: Annotated[Optional[str], "The namespace of the rollout"],
68 | ) -> str:
69 | """Restart an Argo rollout."""
70 | return await _run_argo_command(
71 | f"restart {name} {f'-n {namespace}' if namespace else ''}"
72 | )
73 |
74 |
75 | # Create function tools
76 | get_rollouts = FunctionTool(
77 | _get_rollouts,
78 | description="List all Argo rollouts in a namespace using kubectl get command.",
79 | name="get_rollouts",
80 | )
81 |
82 | promote_rollout = FunctionTool(
83 | _promote_rollout,
84 | description="Promote an Argo rollout.",
85 | name="promote_rollout",
86 | )
87 |
88 | pause_rollout = FunctionTool(
89 | _pause_rollout,
90 | description="Pause an Argo rollout.",
91 | name="pause_rollout",
92 | )
93 |
94 | set_rollout_image = FunctionTool(
95 | _set_rollout_image,
96 | description="Set the image for a container in an Argo rollout.",
97 | name="set_rollout_image",
98 | )
99 |
100 | rollout_restart = FunctionTool(
101 | _rollout_restart,
102 | description="Restart an Argo rollout.",
103 | name="rollout_restart",
104 | )
105 |
106 | # Create typed tools
107 | GetRollouts, GetRolloutsConfig = create_typed_fn_tool(
108 | get_rollouts, "engine.tools.argo.GetRollouts", "GetRollouts"
109 | )
110 |
111 | PromoteRollout, PromoteRolloutConfig = create_typed_fn_tool(
112 | promote_rollout, "engine.tools.argo.PromoteRollout", "PromoteRollout"
113 | )
114 |
115 | PauseRollout, PauseRolloutConfig = create_typed_fn_tool(
116 | pause_rollout, "engine.tools.argo.PauseRollout", "PauseRollout"
117 | )
118 |
119 | SetRolloutImage, SetRolloutImageConfig = create_typed_fn_tool(
120 | set_rollout_image, "engine.tools.argo.SetRolloutImage", "SetRolloutImage"
121 | )
122 |
123 | RolloutRestart, RolloutRestartConfig = create_typed_fn_tool(
124 | rollout_restart, "engine.tools.argo.RolloutRestart", "RolloutRestart"
125 | )
126 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/common/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/mcp/src/mcp_server/tools/common/__init__.py
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/common/mcp.py:
--------------------------------------------------------------------------------
1 | """MCP integration for tool execution."""
2 |
3 | from typing import Dict, Any, Optional
4 | from mcp.tools.utils.helpers import parse_command_args
5 |
6 |
7 | class MCPExecutor:
8 | """MCP executor for tool commands."""
9 |
10 | async def execute(
11 | self,
12 | tool: str,
13 | command: str,
14 | args: Dict[str, Any],
15 | input_data: Optional[Any] = None,
16 | context: Optional[Dict[str, Any]] = None,
17 | ) -> Any:
18 | """Execute a command through MCP.
19 |
20 | Args:
21 | tool: Tool name (kubectl, helm, argo)
22 | command: Command to execute
23 | args: Command arguments
24 | input_data: Optional input data
25 | context: Additional context
26 |
27 | Returns:
28 | Command execution result
29 | """
30 | # Format command arguments
31 | cmd_args = parse_command_args(args)
32 |
33 | # Build full command
34 | full_command = f"{command} {cmd_args}"
35 |
36 | # Prepare MCP context
37 | mcp_context = {"tool": tool, "command": full_command, **(context or {})}
38 |
39 | if input_data:
40 | mcp_context["input_data"] = input_data
41 |
42 | # Execute through MCP
43 | try:
44 | result = await self.agent.execute_tool(
45 | tool_name=tool, command=full_command, context=mcp_context
46 | )
47 | return result
48 |
49 | except Exception as e:
50 | raise RuntimeError(f"MCP execution failed: {str(e)}")
51 |
52 | async def validate_permissions(
53 | self, tool: str, command: str, context: Optional[Dict[str, Any]] = None
54 | ) -> bool:
55 | """Validate permissions for a tool command.
56 |
57 | Args:
58 | tool: Tool name
59 | command: Command to validate
60 | context: Additional context
61 |
62 | Returns:
63 | True if permitted, False otherwise
64 | """
65 | try:
66 | # This will be implemented with proper RBAC checks
67 | return True
68 | except Exception:
69 | return False
70 |
71 | def get_timestamp(self) -> float:
72 | """Get current timestamp in seconds.
73 |
74 | Returns:
75 | Current time in seconds since epoch
76 | """
77 | import time
78 |
79 | return time.time()
80 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/common/models.py:
--------------------------------------------------------------------------------
1 | """Common models for tool responses and configurations."""
2 |
3 | from typing import Dict, Any, Optional, List, Type
4 | from pydantic import BaseModel, Field, create_model
5 | from datetime import datetime
6 |
7 |
8 | class ToolResponse(BaseModel):
9 | """Base model for tool responses."""
10 |
11 | tool: str = Field(..., description="Name of the tool used")
12 | command: str = Field(..., description="Command that was executed")
13 | status: str = Field(..., description="Status of the execution (success/error)")
14 | result: Optional[Any] = Field(None, description="Result of the command execution")
15 | error: Optional[str] = Field(None, description="Error message if execution failed")
16 | timestamp: datetime = Field(
17 | default_factory=datetime.now, description="Execution timestamp"
18 | )
19 | metadata: Dict[str, Any] = Field(
20 | default_factory=dict, description="Additional metadata"
21 | )
22 |
23 |
24 | class ParameterSchema(BaseModel):
25 | """Schema for tool parameters."""
26 |
27 | name: str = Field(..., description="Parameter name")
28 | type: str = Field(
29 | ..., description="Parameter type (string, integer, boolean, etc.)"
30 | )
31 | description: str = Field(..., description="Parameter description")
32 | required: bool = Field(
33 | default=True, description="Whether the parameter is required"
34 | )
35 | default: Optional[Any] = Field(None, description="Default value if not provided")
36 | validation_rules: Dict[str, Any] = Field(
37 | default_factory=dict, description="Validation rules for the parameter"
38 | )
39 | aliases: List[str] = Field(
40 | default_factory=list, description="Alternative names for the parameter"
41 | )
42 |
43 |
44 | class ToolConfig(BaseModel):
45 | """Base model for tool configuration."""
46 |
47 | name: str = Field(..., description="Name of the tool")
48 | description: str = Field(..., description="Description of the tool")
49 | commands: List[str] = Field(..., description="List of available commands")
50 | permissions: List[str] = Field(default=["read"], description="Required permissions")
51 | parameters: List[ParameterSchema] = Field(
52 | default_factory=list, description="Tool parameter schemas"
53 | )
54 | context: Dict[str, Any] = Field(
55 | default_factory=dict, description="Additional context"
56 | )
57 |
58 | def get_parameter_schema(self) -> Type[BaseModel]:
59 | """Get a Pydantic model for parameter validation."""
60 | fields = {}
61 | for param in self.parameters:
62 | field_type = self._get_field_type(param.type)
63 | field = Field(
64 | ... if param.required else Optional[field_type],
65 | description=param.description,
66 | default=param.default,
67 | )
68 | fields[param.name] = (field_type, field)
69 |
70 | # Add aliases as additional fields
71 | for alias in param.aliases:
72 | fields[alias] = (field_type, field)
73 |
74 | return create_model(f"{self.name}Parameters", **fields)
75 |
76 | def _get_field_type(self, type_str: str) -> Type:
77 | """Convert string type to Python type."""
78 | type_map = {
79 | "string": str,
80 | "integer": int,
81 | "boolean": bool,
82 | "float": float,
83 | "list": List,
84 | "dict": Dict,
85 | "any": Any,
86 | }
87 | return type_map.get(type_str, Any)
88 |
89 |
90 | class ResourceIdentifier(BaseModel):
91 | """Model for identifying Kubernetes resources."""
92 |
93 | name: str = Field(..., description="Name of the resource")
94 | namespace: Optional[str] = Field(None, description="Namespace of the resource")
95 | kind: str = Field(..., description="Kind of resource (pod, deployment, etc.)")
96 | api_version: str = Field(default="v1", description="API version of the resource")
97 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/helm/__init__.py:
--------------------------------------------------------------------------------
1 | """Helm tools package."""
2 |
3 | from ._helm import (
4 | helm_list_releases,
5 | helm_repo_add,
6 | helm_repo_update,
7 | helm_repo_remove,
8 | helm_install_with_values,
9 | generate_helm_values,
10 | )
11 |
12 | __all__ = [
13 | "helm_list_releases",
14 | "helm_repo_add",
15 | "helm_repo_update",
16 | "helm_repo_remove",
17 | "helm_install_with_values",
18 | "generate_helm_values",
19 | ]
20 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/k8s/__init__.py:
--------------------------------------------------------------------------------
1 | """Kubernetes tools package."""
2 |
3 | from ._kubectl import (
4 | get_pod_logs,
5 | get_resources,
6 | describe_resource,
7 | create_manifest,
8 | apply_manifest,
9 | update_resource_container_images,
10 | patch_resource,
11 | rollout_restart_deployment,
12 | scale,
13 | delete_resource,
14 | wait_for_x_seconds,
15 | rollout_status,
16 | get_cluster_info,
17 | cordon_node,
18 | uncordon_node,
19 | drain_node,
20 | run_pod,
21 | port_forward,
22 | )
23 |
24 | __all__ = [
25 | "get_pod_logs",
26 | "get_resources",
27 | "describe_resource",
28 | "create_manifest",
29 | "apply_manifest",
30 | "update_resource_container_images",
31 | "patch_resource",
32 | "rollout_restart_deployment",
33 | "scale",
34 | "delete_resource",
35 | "wait_for_x_seconds",
36 | "rollout_status",
37 | "get_cluster_info",
38 | "cordon_node",
39 | "uncordon_node",
40 | "drain_node",
41 | "run_pod",
42 | "port_forward",
43 | ]
44 |
--------------------------------------------------------------------------------
/mcp/src/mcp_server/tools/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/mcp/src/mcp_server/tools/utils/__init__.py
--------------------------------------------------------------------------------
/mcp/src/mcp_server/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/mcp/src/mcp_server/utils/__init__.py
--------------------------------------------------------------------------------
/mcp/src/mcp_server/utils/retry.py:
--------------------------------------------------------------------------------
1 | """Retry utility with exponential backoff for handling request timeouts."""
2 |
3 | import asyncio
4 | import logging
5 | from functools import wraps
6 | from typing import Callable, TypeVar, Any
7 | from ..config.settings import settings
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 | T = TypeVar("T")
12 |
13 |
14 | def with_retry(func: Callable[..., T]) -> Callable[..., T]:
15 | """
16 | Decorator that implements retry logic with exponential backoff.
17 |
18 | Args:
19 | func: The function to be retried
20 |
21 | Returns:
22 | A wrapped function that implements retry logic
23 | """
24 |
25 | @wraps(func)
26 | async def wrapper(*args: Any, **kwargs: Any) -> T:
27 | last_exception = None
28 |
29 | for attempt in range(settings.MAX_RETRY_ATTEMPTS):
30 | try:
31 | return await func(*args, **kwargs)
32 | except asyncio.TimeoutError as e:
33 | last_exception = e
34 | if attempt < settings.MAX_RETRY_ATTEMPTS - 1:
35 | delay = min(
36 | settings.RETRY_BASE_DELAY
37 | * (settings.RETRY_EXPONENTIAL_BASE**attempt),
38 | settings.RETRY_MAX_DELAY,
39 | )
40 | logger.warning(
41 | f"Request timed out (attempt {attempt + 1}/{settings.MAX_RETRY_ATTEMPTS}). "
42 | f"Retrying in {delay:.2f} seconds..."
43 | )
44 | await asyncio.sleep(delay)
45 | else:
46 | logger.error(
47 | f"Request failed after {settings.MAX_RETRY_ATTEMPTS} attempts. "
48 | f"Last error: {str(e)}"
49 | )
50 |
51 | raise last_exception
52 |
53 | return wrapper
54 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "zustand": "^5.0.3"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/ui/.env.example:
--------------------------------------------------------------------------------
1 | # UI Settings
2 | NEXT_PUBLIC_APP_NAME=Skyflo.ai
3 | NEXT_PUBLIC_APP_VERSION=0.1.0
4 | NODE_ENV=production
5 |
6 | # API Connection
7 | API_URL=http://skyflo-ai-engine:8080/api/v1
8 | NEXT_PUBLIC_API_WS_URL=http://skyflo-ai-ui/api/ws
9 | NEXT_PUBLIC_API_WS_URL=ws://localhost:30081
10 |
11 | # Feature Flags
12 | NEXT_PUBLIC_ENABLE_ANALYTICS=false
13 | NEXT_PUBLIC_ENABLE_FEEDBACK=false
--------------------------------------------------------------------------------
/ui/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "next/core-web-vitals",
4 | "plugin:@typescript-eslint/recommended"
5 | ],
6 | "parser": "@typescript-eslint/parser",
7 | "plugins": ["@typescript-eslint"],
8 | "rules": {
9 | // Add any custom rules here
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # IDE
39 | .idea/
40 | .vscode/
41 | .cursor/
--------------------------------------------------------------------------------
/ui/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "gray",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | }
20 | }
--------------------------------------------------------------------------------
/ui/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | eslint: {
4 | ignoreDuringBuilds: true,
5 | },
6 | output: "standalone",
7 | };
8 |
9 | export default nextConfig;
10 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stocksgenie-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@dagrejs/dagre": "^1.1.4",
13 | "@radix-ui/react-icons": "^1.3.0",
14 | "@radix-ui/react-label": "^2.1.0",
15 | "@radix-ui/react-scroll-area": "^1.2.0",
16 | "@radix-ui/react-slot": "^1.1.0",
17 | "@radix-ui/react-switch": "^1.1.1",
18 | "@radix-ui/react-tabs": "^1.1.0",
19 | "@radix-ui/react-tooltip": "^1.1.4",
20 | "@tsparticles/engine": "^3.0.2",
21 | "@tsparticles/react": "^3.0.0",
22 | "@tsparticles/slim": "^3.8.1",
23 | "@types/react-beautiful-dnd": "^13.1.8",
24 | "@xyflow/react": "^12.3.2",
25 | "aws-icons": "^2.1.0",
26 | "canvas-confetti": "^1.9.3",
27 | "class-variance-authority": "^0.7.0",
28 | "clsx": "^2.1.1",
29 | "date-fns": "^4.1.0",
30 | "framer-motion": "^12.6.2",
31 | "html-to-image": "^1.11.11",
32 | "http-proxy-middleware": "^3.0.5",
33 | "lucide-react": "^0.441.0",
34 | "newrelic": "^12.8.0",
35 | "next": "14.2.11",
36 | "react": "^18",
37 | "react-beautiful-dnd": "^13.1.1",
38 | "react-code-blocks": "^0.1.6",
39 | "react-dnd": "^16.0.1",
40 | "react-dnd-html5-backend": "^16.0.1",
41 | "react-dom": "^18",
42 | "react-icons": "^5.3.0",
43 | "react-markdown": "^9.0.1",
44 | "react-svg": "^16.1.34",
45 | "react-toastify": "^11.0.5",
46 | "react-tsparticles": "^2.12.2",
47 | "recharts": "^2.12.7",
48 | "remark-breaks": "^4.0.0",
49 | "remark-gfm": "^4.0.0",
50 | "sharp": "^0.33.5",
51 | "socket.io-client": "^4.8.1",
52 | "tailwind-merge": "^3.2.0",
53 | "tailwindcss-animate": "^1.0.7",
54 | "tsparticles": "^3.8.1"
55 | },
56 | "devDependencies": {
57 | "@types/node": "^20",
58 | "@types/react": "^18",
59 | "@types/react-dom": "^18",
60 | "eslint": "^8",
61 | "eslint-config-next": "14.2.11",
62 | "postcss": "^8",
63 | "tailwindcss": "^3.4.1",
64 | "typescript": "^5"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/ui/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/ui/public/logo_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/ui/public/logo_transparent.png
--------------------------------------------------------------------------------
/ui/public/logo_vector_transparent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/ui/public/logo_vector_transparent.png
--------------------------------------------------------------------------------
/ui/src/app/api/auth/admin-check/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getAuthHeaders } from "@/lib/api";
3 |
4 | export async function GET() {
5 | try {
6 | // Get authentication headers including Bearer token if available
7 | const headers = await getAuthHeaders();
8 |
9 | // Make the API call to check admin status with explicit IPv4
10 | const apiUrl = process.env.API_URL;
11 | const response = await fetch(`${apiUrl}/auth/is_admin_user`, {
12 | method: "GET",
13 | headers: {
14 | "Content-Type": "application/json",
15 | ...headers,
16 | },
17 | cache: "no-store",
18 | });
19 |
20 | if (!response.ok) {
21 | const errorText = await response.text();
22 | console.error(
23 | "[Admin Check API] Error checking admin status:",
24 | response.status,
25 | errorText
26 | );
27 | return NextResponse.json(
28 | { status: "error", error: "Failed to check admin status" },
29 | { status: response.status }
30 | );
31 | }
32 |
33 | const data = await response.json();
34 | return NextResponse.json(data);
35 | } catch (error) {
36 | console.error("[Admin Check API] Error in admin check route:", error);
37 | return NextResponse.json(
38 | { status: "error", error: "Failed to check admin status" },
39 | { status: 500 }
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/ui/src/app/api/auth/me/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getAuthHeaders } from "@/lib/api";
3 |
4 | export async function GET() {
5 | try {
6 | // Get authentication headers including Bearer token if available
7 | const headers = await getAuthHeaders();
8 |
9 | // Make the API call to fetch user data
10 | const response = await fetch(`${process.env.API_URL}/auth/me`, {
11 | method: "GET",
12 | headers,
13 | cache: "no-store",
14 | });
15 |
16 | if (!response.ok) {
17 | const errorText = await response.text();
18 | console.error(
19 | "[Me API] Error fetching user data:",
20 | response.status,
21 | errorText
22 | );
23 | return NextResponse.json(
24 | { status: "error", error: "Failed to fetch user data" },
25 | { status: response.status }
26 | );
27 | }
28 |
29 | const data = await response.json();
30 | return NextResponse.json(data);
31 | } catch (error) {
32 | console.error("[Me API] Error in me route:", error);
33 | return NextResponse.json(
34 | { status: "error", error: "Failed to fetch user data" },
35 | { status: 500 }
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ui/src/app/api/conversation/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getAuthHeaders } from "@/lib/api";
3 |
4 | export async function GET(
5 | request: Request,
6 | { params }: { params: { id: string } }
7 | ) {
8 | try {
9 | // Get authentication headers including Bearer token if available
10 | const headers = await getAuthHeaders();
11 |
12 | // Make the API call to fetch conversation details
13 | const response = await fetch(
14 | `${process.env.API_URL}/chat/conversations/${params.id}`,
15 | {
16 | method: "GET",
17 | headers,
18 | cache: "no-store",
19 | }
20 | );
21 |
22 | if (!response.ok) {
23 | const errorText = await response.text();
24 | console.error(
25 | "[Conversation API] Error fetching conversation:",
26 | response.status,
27 | errorText
28 | );
29 | return NextResponse.json(
30 | { status: "error", error: "Failed to fetch conversation details" },
31 | { status: response.status }
32 | );
33 | }
34 |
35 | const data = await response.json();
36 |
37 | return NextResponse.json(data);
38 | } catch (error) {
39 | console.error("[Conversation API] Error in conversation route:", error);
40 | return NextResponse.json(
41 | { status: "error", error: "Failed to fetch conversation details" },
42 | { status: 500 }
43 | );
44 | }
45 | }
46 |
47 | export async function PATCH(
48 | request: Request,
49 | { params }: { params: { id: string } }
50 | ) {
51 | try {
52 | // Get authentication headers
53 | const headers = await getAuthHeaders();
54 |
55 | // Get the request body
56 | const body = await request.json();
57 | // Make the API call to update conversation messages
58 | const response = await fetch(
59 | `${process.env.API_URL}/chat/conversations/${params.id}/messages`,
60 | {
61 | method: "PATCH",
62 | headers: {
63 | ...headers,
64 | "Content-Type": "application/json",
65 | },
66 | body: JSON.stringify(body),
67 | }
68 | );
69 |
70 | if (!response.ok) {
71 | const errorText = await response.text();
72 | console.error(
73 | "[Conversation API] Error updating conversation messages:",
74 | response.status,
75 | errorText
76 | );
77 | return NextResponse.json(
78 | { status: "error", error: "Failed to update conversation messages" },
79 | { status: response.status }
80 | );
81 | }
82 |
83 | const data = await response.json();
84 |
85 | return NextResponse.json(data);
86 | } catch (error) {
87 | console.error(
88 | "[Conversation API] Error updating conversation messages:",
89 | error
90 | );
91 | return NextResponse.json(
92 | { status: "error", error: "Failed to update conversation messages" },
93 | { status: 500 }
94 | );
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/ui/src/app/api/history/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { getAuthHeaders } from "@/lib/api";
3 |
4 | export async function GET() {
5 | try {
6 | // Get authentication headers including Bearer token if available
7 | const headers = await getAuthHeaders();
8 |
9 | // Make the API call to fetch conversations
10 | const response = await fetch(`${process.env.API_URL}/chat/conversations`, {
11 | method: "GET",
12 | headers,
13 | cache: "no-store",
14 | });
15 |
16 | if (!response.ok) {
17 | const errorText = await response.text();
18 | console.error(
19 | "[History API] Error fetching conversations:",
20 | response.status,
21 | errorText
22 | );
23 | return NextResponse.json(
24 | { status: "error", error: "Failed to fetch conversation history" },
25 | { status: response.status }
26 | );
27 | }
28 |
29 | const data = await response.json();
30 |
31 | return NextResponse.json(data);
32 | } catch (error) {
33 | console.error("[History API] Error in history route:", error);
34 | return NextResponse.json(
35 | { status: "error", error: "Failed to fetch conversation history" },
36 | { status: 500 }
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ui/src/app/api/profile/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { updateUserProfile, changePassword } from "@/lib/auth";
3 |
4 | export async function PATCH(request: NextRequest) {
5 | try {
6 | const data = await request.json();
7 |
8 | const result = await updateUserProfile(data);
9 |
10 | if (result.success) {
11 | return NextResponse.json(result.user, { status: 200 });
12 | } else {
13 | console.error("[API Route] Profile update failed:", result.error);
14 | return NextResponse.json({ error: result.error }, { status: 400 });
15 | }
16 | } catch (error) {
17 | console.error("[API Route] Error in profile update route:", error);
18 | return NextResponse.json(
19 | { error: "Failed to update profile" },
20 | { status: 500 }
21 | );
22 | }
23 | }
24 |
25 | export async function POST(request: NextRequest) {
26 | try {
27 | const data = await request.json();
28 |
29 | // Validate new password
30 | if (data.new_password && data.new_password.length < 8) {
31 | console.warn("[API Route] Password too short");
32 | return NextResponse.json(
33 | { error: "Password must be at least 8 characters long" },
34 | { status: 400 }
35 | );
36 | }
37 |
38 | // Check password confirmation
39 | if (data.new_password !== data.confirm_password) {
40 | console.warn("[API Route] Passwords do not match");
41 | return NextResponse.json(
42 | { error: "Passwords do not match" },
43 | { status: 400 }
44 | );
45 | }
46 |
47 | const result = await changePassword({
48 | current_password: data.current_password,
49 | new_password: data.new_password,
50 | });
51 |
52 | if (result.success) {
53 | return NextResponse.json(
54 | { message: "Password updated successfully" },
55 | { status: 200 }
56 | );
57 | } else {
58 | console.error("[API Route] Password change failed:", result.error);
59 | return NextResponse.json({ error: result.error }, { status: 400 });
60 | }
61 | } catch (error) {
62 | console.error("[API Route] Error in password change route:", error);
63 | return NextResponse.json(
64 | { error: "Failed to change password" },
65 | { status: 500 }
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ui/src/app/chat/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ChatInterface } from "@/components/ChatInterface";
4 | import Navbar from "@/components/navbar/Navbar";
5 |
6 | export default function ChatPage({
7 | params,
8 | searchParams,
9 | }: {
10 | params: { id: string };
11 | searchParams: { message?: string };
12 | }) {
13 | return (
14 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/ui/src/app/favicon.ico
--------------------------------------------------------------------------------
/ui/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 224 71.4% 4.1%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 224 71.4% 4.1%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 224 71.4% 4.1%;
13 | --primary: 220.9 39.3% 11%;
14 | --primary-foreground: 210 20% 98%;
15 | --secondary: 220 14.3% 95.9%;
16 | --secondary-foreground: 220.9 39.3% 11%;
17 | --muted: 220 14.3% 95.9%;
18 | --muted-foreground: 220 8.9% 46.1%;
19 | --accent: 220 14.3% 95.9%;
20 | --accent-foreground: 220.9 39.3% 11%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 20% 98%;
23 | --border: 220 13% 91%;
24 | --input: 220 13% 91%;
25 | --ring: 224 71.4% 4.1%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 224 71.4% 4.1%;
31 | --foreground: 210 20% 98%;
32 | --card: 224 71.4% 4.1%;
33 | --card-foreground: 210 20% 98%;
34 | --popover: 224 71.4% 4.1%;
35 | --popover-foreground: 210 20% 98%;
36 | --primary: 210 20% 98%;
37 | --primary-foreground: 220.9 39.3% 11%;
38 | --secondary: 215 27.9% 16.9%;
39 | --secondary-foreground: 210 20% 98%;
40 | --muted: 215 27.9% 16.9%;
41 | --muted-foreground: 217.9 10.6% 64.9%;
42 | --accent: 215 27.9% 16.9%;
43 | --accent-foreground: 210 20% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 210 20% 98%;
46 | --border: 215 27.9% 16.9%;
47 | --input: 215 27.9% 16.9%;
48 | --ring: 216 12.2% 83.9%;
49 | }
50 | }
51 |
52 | @layer base {
53 | body {
54 | @apply font-sans;
55 | font-feature-settings: "rlig" 1, "calt" 1;
56 | }
57 | }
58 |
59 | @layer utilities {
60 | .text-balance {
61 | text-wrap: balance;
62 | }
63 | }
64 |
65 | @font-face {
66 | font-family: 'Inter';
67 | src: url('/fonts/Inter-Regular.woff2') format('woff2'),
68 | url('/fonts/Inter-Regular.woff') format('woff');
69 | font-weight: 400;
70 | font-style: normal;
71 | }
72 | @font-face {
73 | font-family: 'Inter';
74 | src: url('/fonts/Inter-Bold.woff2') format('woff2'),
75 | url('/fonts/Inter-Bold.woff') format('woff');
76 | font-weight: 700;
77 | font-style: normal;
78 | }
79 |
80 | input:-webkit-autofill,
81 | input:-webkit-autofill:hover,
82 | input:-webkit-autofill:focus,
83 | input:-webkit-autofill:active {
84 | -webkit-box-shadow: 0 0 0 30px #374151 inset !important;
85 | -webkit-text-fill-color: white !important;
86 | }
87 |
88 | /* Customize the scrollbar for WebKit-based browsers (Chrome, Safari) */
89 | ::-webkit-scrollbar {
90 | width: 12px; /* Width of the scrollbar */
91 | }
92 |
93 | ::-webkit-scrollbar-track {
94 | background: #1E1E1E; /* Dark background for the scrollbar track */
95 | border-radius: 10px;
96 | }
97 |
98 | ::-webkit-scrollbar-thumb {
99 | background: #4B4B4B; /* Darker shade for the scrollbar thumb */
100 | border-radius: 10px;
101 | border: 2px solid #1E1E1E; /* Matches the track */
102 | }
103 |
104 | ::-webkit-scrollbar-thumb:hover {
105 | background: #646464; /* Lighter on hover for better visibility */
106 | }
107 |
108 | /* For Firefox (supports scrollbar-width and scrollbar-color) */
109 | * {
110 | scrollbar-width: thin;
111 | scrollbar-color: #4B4B4B #1E1E1E;
112 | }
113 |
114 | .react-flow__edge .react-flow__edge-path {
115 | stroke: #ffffff;
116 | stroke-width: 1;
117 | stroke-opacity: 0.50;
118 | }
119 |
120 | @keyframes dots {
121 | 0%, 20% {
122 | content: '';
123 | }
124 | 40% {
125 | content: '.';
126 | }
127 | 60% {
128 | content: '..';
129 | }
130 | 80%, 100% {
131 | content: '...';
132 | }
133 | }
134 |
135 | @keyframes gradient-move {
136 | 0% {
137 | background-position: 0% 50%;
138 | }
139 | 50% {
140 | background-position: 100% 50%;
141 | }
142 | 100% {
143 | background-position: 0% 50%;
144 | }
145 | }
146 |
147 | .markdown > * {
148 | all: revert;
149 | }
150 |
151 | .dots::after {
152 | content: '';
153 | animation: dots 1.5s steps(1) infinite;
154 | }
155 |
156 | .animate-gradient {
157 | background-size: 300% 300%;
158 | animation: gradient-move 3s ease infinite;
159 | }
160 |
161 |
--------------------------------------------------------------------------------
/ui/src/app/history/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import { useAuth } from "@/components/auth/AuthProvider";
5 | import Navbar from "@/components/navbar/Navbar";
6 | import { useAuthStore } from "@/store/useAuthStore";
7 | import History from "@/components/History";
8 |
9 | export default function HistoryPage() {
10 | const { user } = useAuth();
11 | const { user: storeUser } = useAuthStore();
12 |
13 | return (
14 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/app/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skyflo-ai/skyflo/3ddaa5a13a5c4a8d0f556abd610ebe36b641bf1b/ui/src/app/icon.ico
--------------------------------------------------------------------------------
/ui/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Inter } from "next/font/google";
4 | import "./globals.css";
5 | import dynamic from "next/dynamic";
6 | import { WebSocketProvider } from "@/components/WebSocketProvider";
7 | import ToastContainer from "@/components/ui/ToastContainer";
8 |
9 | const inter = Inter({ subsets: ["latin"] });
10 |
11 | const AuthProvider = dynamic(
12 | () =>
13 | import("@/components/auth/AuthProvider").then((mod) => mod.AuthProvider),
14 | { ssr: false }
15 | );
16 |
17 | export default function RootLayout({
18 | children,
19 | }: {
20 | children: React.ReactNode;
21 | }) {
22 | return (
23 |
24 |
25 | Skyflo.ai | AI Agent for Cloud Native
26 |
27 |
28 |
29 | {children}
30 |
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/ui/src/app/login/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Login } from "@/components/auth/Login";
4 | import { useEffect, useState } from "react";
5 | import Image from "next/image";
6 | import { MdLockPerson } from "react-icons/md";
7 |
8 | export default function LoginPage() {
9 | const [loading, setLoading] = useState(true);
10 |
11 | useEffect(() => {
12 | setLoading(false);
13 | }, []);
14 |
15 | if (loading) {
16 | return (
17 |
20 | );
21 | }
22 |
23 | return (
24 |
25 | {/* Large Background Logo */}
26 |
27 |

32 |
33 |
34 | {/* Grid Background with 3D Gradient */}
35 |
41 |
42 | {/* Gradient Orbs */}
43 |
44 |
45 |
46 |
47 | {/* Welcome Header */}
48 |
49 |
50 |
51 | skyflo
52 | .ai
53 |
54 |
55 |
Sign in to your account
56 |
57 |
58 | {/* Login Card */}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
Sign In
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/ui/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Navbar from "@/components/navbar/Navbar";
5 | import Welcome from "@/components/chat/components/Welcome";
6 |
7 | import { MdElectricBolt, MdSearch, MdDelete, MdRefresh } from "react-icons/md";
8 |
9 | const INITIAL_SUGGESTIONS = [
10 | {
11 | text: "Get all pods in the ... namespace",
12 | icon: MdSearch,
13 | category: "Query",
14 | },
15 | {
16 | text: "Create a new deployment with nginx:latest in the ... namespace",
17 | icon: MdElectricBolt,
18 | category: "Create Deployment",
19 | },
20 | {
21 | text: "Restart the api deployment in the ... namespace",
22 | icon: MdRefresh,
23 | category: "Restart Deployment",
24 | },
25 | {
26 | text: "Delete the frontend deployment in the ... namespace",
27 | icon: MdDelete,
28 | category: "Delete Resource",
29 | },
30 | ];
31 |
32 | export default function HomePage() {
33 | return (
34 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/ui/src/app/settings/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState } from "react";
4 | import { useAuth } from "@/components/auth/AuthProvider";
5 | import Navbar from "@/components/navbar/Navbar";
6 | import { useAuthStore } from "@/store/useAuthStore";
7 | import ProfileSettings from "@/components/settings/Settings";
8 |
9 | export default function SettingsPage() {
10 | const { user } = useAuth();
11 | const { user: storeUser } = useAuthStore();
12 |
13 | return (
14 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/app/welcome/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Register } from "@/components/auth/Register";
4 | import { MdAdminPanelSettings } from "react-icons/md";
5 |
6 | export default function Welcome() {
7 | return (
8 |
9 | {/* Large Background Logo */}
10 |
11 |

16 |
17 |
18 | {/* Grid Background with 3D Gradient */}
19 |
25 |
26 | {/* Gradient Orbs */}
27 |
28 |
29 |
30 |
31 | {/* Welcome Header */}
32 |
33 |
34 |
35 | skyflo
36 | .ai
37 |
38 |
39 |
40 | Let's get started by creating your admin account
41 |
42 |
43 |
44 | {/* Admin Registration Card */}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Create Admin Account
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | {/* Footer Note */}
62 |
63 | After creating your admin account, you'll be able to invite team
64 | members from within the app.
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/ui/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import { Button } from '@/components/ui/button'
3 |
4 | export default function Header() {
5 | return (
6 |
21 | )
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/ui/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import Header from "./Header";
3 | import ToastContainer from "./ui/ToastContainer";
4 |
5 | interface LayoutProps {
6 | children: ReactNode;
7 | }
8 |
9 | export default function Layout({ children }: LayoutProps) {
10 | return (
11 |
12 |
13 | {children}
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/components/auth/AuthInput.tsx:
--------------------------------------------------------------------------------
1 | import { Input } from "@/components/ui/input";
2 | import { Label } from "@/components/ui/label";
3 | import { LucideIcon } from "lucide-react";
4 |
5 | interface AuthInputProps {
6 | id: string;
7 | type: string;
8 | name: string;
9 | placeholder: string;
10 | icon: LucideIcon;
11 | }
12 |
13 | export const AuthInput: React.FC = ({
14 | id,
15 | type,
16 | name,
17 | placeholder,
18 | icon: Icon,
19 | }) => (
20 |
21 |
24 |
37 |
38 | );
39 |
--------------------------------------------------------------------------------
/ui/src/components/auth/Login.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { Button } from "@/components/ui/button";
6 | import { AuthInput } from "./AuthInput";
7 | import { Lock, Mail } from "lucide-react";
8 | import { useAuthStore } from "@/store/useAuthStore";
9 | import { handleLogin } from "@/lib/auth";
10 | import { setCookie } from "@/lib/utils";
11 |
12 | export const Login = () => {
13 | const { login } = useAuthStore();
14 | const [loading, setLoading] = useState(false);
15 | const [error, setError] = useState(null);
16 | const [isMounted, setIsMounted] = useState(false);
17 | const router = useRouter();
18 |
19 | useEffect(() => {
20 | setIsMounted(true);
21 | }, []);
22 |
23 | const handleSubmit = async (e: React.FormEvent) => {
24 | e.preventDefault();
25 | if (!isMounted) return;
26 |
27 | setLoading(true);
28 | setError(null);
29 |
30 | const formData = new FormData(e.currentTarget);
31 | const result = await handleLogin(formData);
32 |
33 | if (result && result.success) {
34 | localStorage.setItem("auth_token", result.token);
35 | setCookie("auth_token", result.token, 7);
36 | login(result.user, result.token);
37 | router.push("/");
38 | } else {
39 | setError(result?.error || "Authentication failed");
40 | }
41 |
42 | setLoading(false);
43 | };
44 |
45 | if (!isMounted) {
46 | return null;
47 | }
48 |
49 | return (
50 |
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/ui/src/components/auth/Register.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 | import { useRouter } from "next/navigation";
5 | import { Button } from "@/components/ui/button";
6 | import { AuthInput } from "./AuthInput";
7 | import { Lock, Mail, User } from "lucide-react";
8 | import { useAuthStore } from "@/store/useAuthStore";
9 | import { handleRegistration } from "@/lib/auth";
10 | import { setCookie } from "@/lib/utils";
11 |
12 | export const Register = () => {
13 | const { login } = useAuthStore();
14 | const [loading, setLoading] = useState(false);
15 | const [error, setError] = useState(null);
16 | const [isMounted, setIsMounted] = useState(false);
17 | const router = useRouter();
18 |
19 | useEffect(() => {
20 | setIsMounted(true);
21 | }, []);
22 |
23 | const handleSubmit = async (e: React.FormEvent) => {
24 | e.preventDefault();
25 | if (!isMounted) return;
26 |
27 | setLoading(true);
28 | setError(null);
29 |
30 | const formData = new FormData(e.currentTarget);
31 | const result = await handleRegistration(formData);
32 |
33 | if (result && result.success) {
34 | localStorage.setItem("auth_token", result.token);
35 | setCookie("auth_token", result.token, 7);
36 | login(result.user, result.token);
37 | router.push("/login");
38 | } else {
39 | setError(result?.error || "Registration failed");
40 | }
41 |
42 | setLoading(false);
43 | };
44 |
45 | if (!isMounted) {
46 | return null;
47 | }
48 |
49 | return (
50 |
92 | );
93 | };
94 |
--------------------------------------------------------------------------------
/ui/src/components/chat/components/ApprovalButtons.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { MdThumbUp, MdThumbDown } from "react-icons/md";
3 | import { Button } from "@/components/ui/button";
4 |
5 | interface ApprovalButtonsProps {
6 | onApprove: (stepId: string) => void;
7 | onReject: (stepId: string) => void;
8 | parameters: any;
9 | tool: string;
10 | action: string;
11 | step: { id: string };
12 | setExpandedSteps: React.Dispatch<
13 | React.SetStateAction>
14 | >;
15 | }
16 |
17 | const ApprovalButtons: React.FC = ({
18 | onApprove,
19 | onReject,
20 | parameters,
21 | tool,
22 | action,
23 | step,
24 | setExpandedSteps,
25 | }) => {
26 | return (
27 |
33 |
34 |
35 |
36 |
37 | Approval Required
38 |
39 |
40 |
41 |
This operation requires your approval before execution.
42 |
43 |
44 |
45 |
67 |
68 |
69 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | export default ApprovalButtons;
100 |
--------------------------------------------------------------------------------
/ui/src/components/chat/components/ChatHeader.tsx:
--------------------------------------------------------------------------------
1 | import { HiMiniSparkles } from "react-icons/hi2";
2 |
3 | const ChatHeader = () => {
4 | return (
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 | Hi! I'm{" "}
16 |
17 | Sky
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Your AI-Powered Cloud Native Co-Pilot
26 |
27 |
28 |
29 | );
30 | };
31 |
32 | export default ChatHeader;
33 |
--------------------------------------------------------------------------------
/ui/src/components/chat/components/ChatSuggestions.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import {
3 | Card,
4 | CardHeader,
5 | CardTitle,
6 | CardDescription,
7 | } from "@/components/ui/card";
8 | import { HiArrowRight } from "react-icons/hi2";
9 |
10 | import { ChatSuggestionsProps, Suggestion } from "../types";
11 |
12 | const cardVariants = {
13 | hidden: { opacity: 0, y: 20 },
14 | visible: (index: number) => ({
15 | opacity: 1,
16 | y: 0,
17 | transition: {
18 | delay: index * 0.1,
19 | duration: 0.4,
20 | type: "spring",
21 | stiffness: 100,
22 | damping: 20,
23 | },
24 | }),
25 | hover: {
26 | y: -6,
27 | boxShadow: "0 10px 25px -5px rgba(59, 130, 246, 0.15)",
28 | borderColor: "rgba(59, 130, 246, 0.4)",
29 | transition: {
30 | duration: 0.3,
31 | },
32 | },
33 | };
34 |
35 | const ChatSuggestions = ({
36 | suggestions,
37 | onSuggestionClick,
38 | }: ChatSuggestionsProps) => {
39 | return (
40 |
41 | {suggestions.map((suggestion, index) => (
42 |
onSuggestionClick(suggestion.text)}
51 | onKeyDown={(e) => {
52 | if (e.key === "Enter" || e.key === " ") {
53 | onSuggestionClick(suggestion.text);
54 | }
55 | }}
56 | tabIndex={0}
57 | role="button"
58 | aria-label={`Select suggestion: ${suggestion.text}`}
59 | >
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {suggestion.category}
71 |
72 |
73 | {suggestion.text}
74 |
75 |
76 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | ))}
88 |
89 | );
90 | };
91 |
92 | export default ChatSuggestions;
93 |
--------------------------------------------------------------------------------
/ui/src/components/chat/components/VerificationResults.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | FaCheckCircle,
4 | FaTimesCircle,
5 | FaExclamationTriangle,
6 | } from "react-icons/fa";
7 |
8 | interface ValidationResult {
9 | criterion: string;
10 | status: "success" | "failure" | "warning";
11 | details?: string;
12 | }
13 |
14 | interface VerificationResultsProps {
15 | results: ValidationResult[];
16 | overallStatus: "success" | "failure" | "partial" | "pending";
17 | title?: string;
18 | }
19 |
20 | const VerificationResults: React.FC = ({
21 | results,
22 | overallStatus,
23 | title = "Verification Results",
24 | }) => {
25 | const getStatusBadge = () => {
26 | switch (overallStatus) {
27 | case "success":
28 | return (
29 |
30 | All Checks Passed
31 |
32 | );
33 | case "failure":
34 | return (
35 |
36 | Verification Failed
37 |
38 | );
39 | case "partial":
40 | return (
41 |
42 | Partially Verified
43 |
44 | );
45 | default:
46 | return (
47 |
48 | Verification In Progress
49 |
50 | );
51 | }
52 | };
53 |
54 | const getStatusIcon = (status: string) => {
55 | switch (status) {
56 | case "success":
57 | return ;
58 | case "failure":
59 | return ;
60 | default:
61 | return (
62 |
63 | );
64 | }
65 | };
66 |
67 | return (
68 |
69 |
70 |
{title}
71 | {getStatusBadge()}
72 |
73 |
74 |
75 | {results.map((result, index) => (
76 |
77 |
78 |
{getStatusIcon(result.status)}
79 |
80 |
{result.criterion}
81 | {result.details && (
82 |
83 | {result.details}
84 |
85 | )}
86 |
87 |
88 |
89 | ))}
90 |
91 | {results.length === 0 && (
92 |
93 | No verification criteria to display
94 |
95 | )}
96 |
97 |
98 | );
99 | };
100 |
101 | export default VerificationResults;
102 |
--------------------------------------------------------------------------------
/ui/src/components/chat/components/Welcome.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/navigation";
2 | import ChatHeader from "./ChatHeader";
3 | import ChatSuggestions from "./ChatSuggestions";
4 | import { Suggestion } from "../types";
5 | import ChatInput from "./ChatInput";
6 | import { useState } from "react";
7 | import Loader from "@/components/ui/Loader";
8 | import { createConversation } from "@/lib/api";
9 | import { useWebSocket } from "@/components/WebSocketProvider";
10 | import { queryAgent } from "@/lib/api";
11 |
12 | interface WelcomeProps {
13 | initialSuggestions?: Suggestion[];
14 | }
15 |
16 | export default function Welcome({ initialSuggestions }: WelcomeProps) {
17 | const router = useRouter();
18 | const { joinConversation } = useWebSocket();
19 |
20 | const [inputValue, setInputValue] = useState("");
21 | const [isAgentResponding, setIsAgentResponding] = useState(false);
22 | const [error, setError] = useState(null);
23 |
24 | const initializeConversation = async (message: string) => {
25 | try {
26 | setError(null);
27 | setIsAgentResponding(true);
28 |
29 | // Create a new conversation ID
30 | const conversationId = crypto.randomUUID();
31 |
32 | // Create the conversation first
33 | const response = await createConversation(conversationId);
34 |
35 | // Join the WebSocket conversation
36 | joinConversation(conversationId);
37 |
38 | // Send the initial query
39 | const chatHistory = [
40 | {
41 | role: "user",
42 | content: message,
43 | timestamp: Date.now(),
44 | contextMarker: "latest-query",
45 | latest: true,
46 | },
47 | ];
48 | const chatHistoryString = JSON.stringify(chatHistory);
49 | await queryAgent(chatHistoryString, "kubernetes", conversationId);
50 |
51 | // Route to the chat interface with the new conversation ID
52 | router.push(
53 | `/chat/${conversationId}?message=${encodeURIComponent(message)}`
54 | );
55 | } catch (err) {
56 | console.error("[Welcome] Failed to create conversation:", err);
57 | setError("Failed to start conversation. Please try again.");
58 | setIsAgentResponding(false);
59 | }
60 | };
61 |
62 | const handleSuggestionClick = (suggestionText: string) => {
63 | setInputValue(suggestionText);
64 | // Focus the textarea after setting the input value
65 | setTimeout(() => {
66 | const textArea = document.querySelector("textarea");
67 | if (textArea) {
68 | textArea.focus();
69 | }
70 | }, 100);
71 | };
72 |
73 | const handleSubmit = () => {
74 | if (!inputValue.trim()) return;
75 | initializeConversation(inputValue);
76 | setInputValue("");
77 | };
78 |
79 | return (
80 |
81 |
82 | {isAgentResponding ? (
83 |
84 | ) : (
85 | <>
86 | {/* Header */}
87 |
88 |
89 | {/* Error Message */}
90 | {error && (
91 |
{error}
92 | )}
93 |
94 | {/* Suggestions */}
95 |
99 | >
100 | )}
101 |
102 |
103 | {/* Chat Input - Fixed at bottom */}
104 |
105 | {}}
110 | isAgentResponding={isAgentResponding}
111 | />
112 |
113 |
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/ui/src/components/chat/services/WebSocketService.ts:
--------------------------------------------------------------------------------
1 | import { io, Socket } from "socket.io-client";
2 | import {
3 | AgentUpdate,
4 | MessageData,
5 | Plan,
6 | StepUpdate,
7 | VerificationCriteria,
8 | VerificationResult,
9 | } from "../types";
10 |
11 | export class WebSocketService {
12 | private socket: Socket | null = null;
13 | private handlers: {
14 | onAgentUpdate?: (data: AgentUpdate) => void;
15 | onMessage?: (data: MessageData) => void;
16 | onError?: (error: Error) => void;
17 | onConnect?: () => void;
18 | onDisconnect?: () => void;
19 | onPlan?: (data: Plan) => void;
20 | onStepUpdate?: (data: StepUpdate) => void;
21 | onVerificationCriteriaList?: (data: VerificationCriteria[]) => void;
22 | onVerificationResult?: (data: VerificationResult) => void;
23 | } = {};
24 |
25 | constructor(private baseUrl: string) {}
26 |
27 | connect(conversationId: string): Promise {
28 | return new Promise((resolve, reject) => {
29 | try {
30 | this.socket = io(this.baseUrl, {
31 | path: "/socket.io",
32 | query: { conversation_id: conversationId },
33 | transports: ["websocket"],
34 | reconnection: true,
35 | reconnectionAttempts: 5,
36 | reconnectionDelay: 1000,
37 | });
38 |
39 | this.setupEventListeners();
40 | resolve(this.socket);
41 | } catch (error) {
42 | reject(error);
43 | }
44 | });
45 | }
46 |
47 | private setupEventListeners() {
48 | if (!this.socket) return;
49 |
50 | this.socket.on("connect", () => {
51 | this.handlers.onConnect?.();
52 | });
53 |
54 | this.socket.on("disconnect", () => {
55 | this.handlers.onDisconnect?.();
56 | });
57 |
58 | this.socket.on("agent_update", (data: AgentUpdate) => {
59 | this.handlers.onAgentUpdate?.(data);
60 | });
61 |
62 | this.socket.on("message", (data: MessageData) => {
63 | this.handlers.onMessage?.(data);
64 | });
65 |
66 | this.socket.on("error", (error: Error) => {
67 | console.error("WebSocket error:", error);
68 | this.handlers.onError?.(error);
69 | });
70 |
71 | this.socket.on("plan", (data: any) => {
72 | if (data.data?.plan) {
73 | this.handlers.onPlan?.(data.data.plan);
74 | }
75 | });
76 |
77 | this.socket.on("plan_generated", (data: any) => {
78 | if (data.details?.plan) {
79 | this.handlers.onPlan?.(data.details.plan);
80 | }
81 | });
82 |
83 | this.socket.on("step_update", (data: any) => {
84 | if (data.data) {
85 | this.handlers.onStepUpdate?.(data.data);
86 | }
87 | });
88 |
89 | this.socket.on("verification_criteria_list", (data: any) => {
90 | if (data.data?.details?.criteria) {
91 | this.handlers.onVerificationCriteriaList?.(data.data.details.criteria);
92 | }
93 | });
94 |
95 | this.socket.on("criterion_result", (data: any) => {
96 | if (data.data?.details) {
97 | this.handlers.onVerificationResult?.(data.data.details);
98 | }
99 | });
100 | }
101 |
102 | setHandlers(handlers: typeof this.handlers) {
103 | this.handlers = handlers;
104 | }
105 |
106 | disconnect() {
107 | if (this.socket) {
108 | this.socket.disconnect();
109 | this.socket = null;
110 | }
111 | }
112 |
113 | isConnected(): boolean {
114 | return this.socket?.connected || false;
115 | }
116 |
117 | getSocket(): Socket | null {
118 | return this.socket;
119 | }
120 |
121 | async reconnect(conversationId: string): Promise {
122 | this.disconnect();
123 | await this.connect(conversationId);
124 | }
125 |
126 | emit(event: string, data: any) {
127 | if (this.socket && this.isConnected()) {
128 | this.socket.emit(event, data);
129 | } else {
130 | console.error("Cannot emit event: socket is not connected");
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/ui/src/components/navbar/Navbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import Image from "next/image";
5 | import { Home, History, LogOut, Settings } from "lucide-react";
6 |
7 | import { useAuth } from "@/components/auth/AuthProvider";
8 | import { useAuthStore } from "@/store/useAuthStore";
9 | import { handleLogout } from "@/lib/auth";
10 | import { useRouter, usePathname } from "next/navigation";
11 | import {
12 | Tooltip,
13 | TooltipContent,
14 | TooltipTrigger,
15 | TooltipProvider,
16 | } from "@/components/ui/tooltip";
17 |
18 | export default function Navbar() {
19 | const router = useRouter();
20 | const pathname = usePathname();
21 | const { logout } = useAuth();
22 | const { logout: storeLogout } = useAuthStore();
23 |
24 | const handleLogoutOnClick = async () => {
25 | await handleLogout();
26 | storeLogout();
27 | logout();
28 | router.push("/login");
29 | };
30 |
31 | return (
32 |
75 | );
76 | }
77 |
78 | function NavIcon({
79 | icon,
80 | tooltip,
81 | onClick,
82 | isActive,
83 | }: {
84 | icon: React.ReactNode;
85 | tooltip: string;
86 | onClick?: () => void;
87 | isActive?: boolean;
88 | }) {
89 | return (
90 |
91 |
92 |
93 |
104 |
105 |
106 | {tooltip}
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/ui/src/components/settings/Settings.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState, useEffect } from "react";
4 |
5 | import TeamSettings from "./TeamSettings";
6 | import ProfileSettings from "./ProfileSettings";
7 | import { User } from "@/lib/types/auth";
8 | import { motion } from "framer-motion";
9 | import { MdError } from "react-icons/md";
10 | import { IoSettingsOutline } from "react-icons/io5";
11 |
12 | // Animation variants
13 | const fadeInVariants = {
14 | hidden: { opacity: 0 },
15 | visible: { opacity: 1, transition: { duration: 0.4 } },
16 | };
17 |
18 | interface SettingsProps {
19 | user: User | null;
20 | }
21 |
22 | export default function Settings({ user }: SettingsProps) {
23 | if (!user) {
24 | return (
25 |
30 |
31 |
32 |
Authentication Required
33 |
34 |
35 | You need to be logged in to access profile settings.
36 |
37 |
38 | );
39 | }
40 |
41 | return (
42 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | Settings
56 |
57 |
58 | Manage your personal profile and team member access in a
59 | centralized dashboard.
60 |
61 |
{" "}
62 |
63 |
64 |
69 |
70 |
71 | {/* Two Column Layout */}
72 |
73 | {/* Left Column - Profile Settings */}
74 |
77 |
78 | {/* Right Column - Team Management */}
79 |
80 |
81 |
82 |
83 |
84 | {/* Custom Scrollbar Styles */}
85 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/ui/src/components/ui/Loader.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | export default function Loader() {
4 | return (
5 |
6 | {/* Animated spinner with logo */}
7 |
8 | {/* Outer spinning ring */}
9 |
10 |
14 |
15 | {/* Middle ring with reverse animation */}
16 |
17 |
21 |
22 | {/* Inner pulsing core with logo */}
23 |
27 |
28 |
35 |
36 |
37 |
38 | {/* Cycling text messages */}
39 |
40 |
41 | -
42 | Scanning environment
43 |
44 | -
45 | Initializing neural pathways
46 |
47 | -
48 | Connecting to the cluster
49 |
50 | -
51 | Loading cloud interface
52 |
53 | -
54 | Ready to assist
55 |
56 |
57 |
58 |
59 | {/* Add CSS for cycling text animation */}
60 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/ui/src/components/ui/ToastContainer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ToastContainer as ReactToastifyContainer } from "react-toastify";
3 | import "react-toastify/dist/ReactToastify.css";
4 |
5 | export const ToastContainer: React.FC = () => {
6 | return (
7 |
20 | "relative flex p-4 rounded-md justify-between overflow-hidden cursor-pointer my-3"
21 | }
22 | />
23 | );
24 | };
25 |
26 | export default ToastContainer;
27 |
--------------------------------------------------------------------------------
/ui/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/ui/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ));
42 | CardTitle.displayName = "CardTitle";
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ));
54 | CardDescription.displayName = "CardDescription";
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ));
62 | CardContent.displayName = "CardContent";
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ));
74 | CardFooter.displayName = "CardFooter";
75 |
76 | export {
77 | Card,
78 | CardHeader,
79 | CardFooter,
80 | CardTitle,
81 | CardDescription,
82 | CardContent,
83 | };
84 |
--------------------------------------------------------------------------------
/ui/src/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Check, Copy } from "lucide-react";
3 | import { cn } from "@/lib/utils";
4 | import { ScrollArea } from "@/components/ui/scroll-area";
5 |
6 | interface CodeBlockProps {
7 | code: string;
8 | language?: string;
9 | showLineNumbers?: boolean;
10 | className?: string;
11 | }
12 |
13 | export function CodeBlock({
14 | code,
15 | language = "bash",
16 | showLineNumbers = false,
17 | className,
18 | }: CodeBlockProps) {
19 | const [copied, setCopied] = useState(false);
20 |
21 | const handleCopy = async () => {
22 | await navigator.clipboard.writeText(code);
23 | setCopied(true);
24 | setTimeout(() => setCopied(false), 2000);
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
38 | {language && (
39 |
{language}
40 | )}
41 |
53 |
54 |
55 |
56 |
62 | {showLineNumbers && (
63 |
64 | {code.split("\n").map((_, i) => (
65 |
66 | {i + 1}
67 |
68 | ))}
69 |
70 | )}
71 | {code}
72 |
73 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/ui/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/ui/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/ui/src/components/ui/markdown-components.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { CodeBlock } from "./code-block";
3 |
4 | interface MarkdownProps {
5 | node?: unknown;
6 | children?: ReactNode;
7 | }
8 |
9 | interface CodeProps extends MarkdownProps {
10 | inline?: boolean;
11 | className?: string;
12 | }
13 |
14 | // The sub-list should be indented and have a different bullet style
15 | export const markdownComponents = {
16 | h1: ({ node, ...props }: MarkdownProps) => (
17 |
21 | ),
22 | h2: ({ node, ...props }: MarkdownProps) => (
23 |
27 | ),
28 | h3: ({ node, ...props }: MarkdownProps) => (
29 |
33 | ),
34 | p: ({ node, ...props }: MarkdownProps) => (
35 |
36 | ),
37 | code: ({ node, inline, className, children, ...props }: CodeProps) => {
38 | const match = /language-(\w+)/.exec(className || "");
39 | const language = match ? match[1] : undefined;
40 |
41 | // Single backtick code is inline and has no language class
42 | if (
43 | inline ||
44 | (!language && typeof children === "string" && !children.includes("\n"))
45 | ) {
46 | return (
47 |
51 | {children}
52 |
53 | );
54 | }
55 |
56 | return (
57 |
63 | );
64 | },
65 | ul: ({ node, ...props }: MarkdownProps & { depth?: number }) => {
66 | const depth = (node as any)?.depth || 0;
67 | return (
68 |
74 | );
75 | },
76 | li: ({ node, ...props }: MarkdownProps) => {
77 | const depth = (node as any)?.depth || 0;
78 | return (
79 |
80 | );
81 | },
82 | };
83 |
--------------------------------------------------------------------------------
/ui/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/ui/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface SkeletonProps {
4 | className?: string;
5 | }
6 |
7 | export function Skeleton({ className }: SkeletonProps) {
8 | return (
9 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/ui/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/ui/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { createContext, useState, useContext, ReactNode } from "react";
4 |
5 | interface TooltipContextType {
6 | open: boolean;
7 | setOpen: (open: boolean) => void;
8 | }
9 |
10 | const TooltipContext = createContext(undefined);
11 |
12 | export function TooltipProvider({ children }: { children: ReactNode }) {
13 | const [open, setOpen] = useState(false);
14 | return (
15 |
16 | {children}
17 |
18 | );
19 | }
20 |
21 | export function Tooltip({ children }: { children: ReactNode }) {
22 | return {children}
;
23 | }
24 |
25 | export function TooltipTrigger({
26 | children,
27 | asChild = false,
28 | }: {
29 | children: ReactNode;
30 | asChild?: boolean;
31 | }) {
32 | const context = useContext(TooltipContext);
33 | if (!context)
34 | throw new Error("TooltipTrigger must be used within a TooltipProvider");
35 |
36 | const { setOpen } = context;
37 |
38 | const trigger = asChild ? (
39 | React.Children.only(children)
40 | ) : (
41 |
42 | );
43 |
44 | // Preserve original event handlers
45 | const child = trigger as React.ReactElement;
46 | const originalProps = child.props || {};
47 | const { onClick: originalOnClick } = originalProps;
48 |
49 | return React.cloneElement(child, {
50 | onMouseEnter: (e: React.MouseEvent) => {
51 | setOpen(true);
52 | if (originalProps.onMouseEnter) originalProps.onMouseEnter(e);
53 | },
54 | onMouseLeave: (e: React.MouseEvent) => {
55 | setOpen(false);
56 | if (originalProps.onMouseLeave) originalProps.onMouseLeave(e);
57 | },
58 | onFocus: (e: React.FocusEvent) => {
59 | setOpen(true);
60 | if (originalProps.onFocus) originalProps.onFocus(e);
61 | },
62 | onBlur: (e: React.FocusEvent) => {
63 | setOpen(false);
64 | if (originalProps.onBlur) originalProps.onBlur(e);
65 | },
66 | onClick: (e: React.MouseEvent) => {
67 | // Ensure click events are preserved and properly forwarded
68 | if (originalOnClick) {
69 | originalOnClick(e);
70 | }
71 | },
72 | });
73 | }
74 |
75 | export function TooltipContent({
76 | children,
77 | side = "top",
78 | }: {
79 | children: ReactNode;
80 | side?: "top" | "right" | "bottom" | "left";
81 | }) {
82 | const context = useContext(TooltipContext);
83 | if (!context)
84 | throw new Error("TooltipContent must be used within a TooltipProvider");
85 |
86 | const { open } = context;
87 |
88 | if (!open) return null;
89 |
90 | const sideClasses = {
91 | top: "bottom-full left-1/2 -translate-x-1/2 mb-3 whitespace-nowrap",
92 | right: "left-full top-1/2 -translate-y-1/2 ml-2 whitespace-nowrap",
93 | bottom: "top-full left-1/2 -translate-x-1/2 mt-2 whitespace-nowrap",
94 | left: "right-full top-1/2 -translate-y-1/2 mr-2 whitespace-nowrap",
95 | };
96 |
97 | return (
98 |
101 | {children}
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/ui/src/lib/toast.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import { toast, ToastOptions, TypeOptions } from "react-toastify";
3 | import "react-toastify/dist/ReactToastify.css";
4 | import { HiMiniSparkles } from "react-icons/hi2";
5 |
6 | interface ToastContentProps {
7 | message: string;
8 | type: TypeOptions;
9 | heading?: string;
10 | }
11 |
12 | const ToastContent: FC = ({ message, type, heading }) => (
13 |
14 | {type === "info" && (
15 |
16 |
17 |
18 | )}
19 |
20 |
21 | {heading || type.charAt(0).toUpperCase() + type.slice(1)}
22 |
23 |
{message}
24 |
25 |
26 | );
27 |
28 | // Default toast configuration to match Skyflo.ai design system
29 | const defaultOptions: ToastOptions = {
30 | position: "top-right",
31 | autoClose: 3500,
32 | hideProgressBar: true,
33 | closeOnClick: true,
34 | pauseOnHover: true,
35 | draggable: true,
36 | progress: undefined,
37 | className: "!bg-transparent !p-0 !shadow-none",
38 | };
39 |
40 | const approvalRequiredOptions: ToastOptions = {
41 | position: "top-right",
42 | autoClose: 5000,
43 | hideProgressBar: true,
44 | closeOnClick: true,
45 | pauseOnHover: true,
46 | draggable: true,
47 | progress: undefined,
48 | className: "!bg-transparent !p-0 !shadow-none",
49 | };
50 |
51 | // Custom toast styles by type
52 | const toastStyles = {
53 | success: {
54 | style: {
55 | background: "transparent",
56 | padding: 0,
57 | margin: 0,
58 | },
59 | icon: true,
60 | },
61 | error: {
62 | style: {
63 | background: "transparent",
64 | padding: 0,
65 | margin: 0,
66 | },
67 | icon: true,
68 | },
69 | info: {
70 | style: {
71 | background: "transparent",
72 | padding: 0,
73 | margin: 0,
74 | },
75 | icon: true,
76 | },
77 | warning: {
78 | style: {
79 | background: "transparent",
80 | padding: 0,
81 | margin: 0,
82 | },
83 | icon: true,
84 | },
85 | };
86 |
87 | // Toast API
88 | export const showToast = (
89 | message: string,
90 | type: TypeOptions = "default",
91 | options?: ToastOptions
92 | ) => {
93 | const toastStyle = toastStyles[type as keyof typeof toastStyles];
94 |
95 | return toast(, {
96 | ...defaultOptions,
97 | type,
98 | style: {
99 | ...toastStyle?.style,
100 | },
101 | icon: false,
102 | ...options,
103 | });
104 | };
105 |
106 | const approvalRequiredToast = (
107 | heading: string,
108 | message: string,
109 | options?: ToastOptions
110 | ) => {
111 | const toastStyle = toastStyles["info" as keyof typeof toastStyles];
112 |
113 | return toast(
114 | ,
115 | {
116 | ...approvalRequiredOptions,
117 | type: "info",
118 | style: {
119 | ...toastStyle?.style,
120 | },
121 | icon: false,
122 | ...options,
123 | }
124 | );
125 | };
126 |
127 | // Helper functions for specific toast types
128 | export const showSuccess = (message: string, options?: ToastOptions) =>
129 | showToast(message, "success", options);
130 |
131 | export const showError = (message: string, options?: ToastOptions) =>
132 | showToast(message, "error", options);
133 |
134 | export const showInfo = (message: string, options?: ToastOptions) =>
135 | showToast(message, "info", options);
136 |
137 | export const showWarning = (message: string, options?: ToastOptions) =>
138 | showToast(message, "warning", options);
139 |
140 | export const showApprovalRequired = (
141 | heading: string,
142 | message: string,
143 | options?: ToastOptions
144 | ) => approvalRequiredToast(heading, message, options);
145 |
--------------------------------------------------------------------------------
/ui/src/lib/types/auth.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | id: string;
3 | email: string;
4 | full_name: string;
5 | role: string;
6 | is_active: boolean;
7 | is_superuser: boolean;
8 | is_verified: boolean;
9 | created_at: string;
10 | }
11 |
12 | export interface TeamMember {
13 | id: string;
14 | email: string;
15 | name: string;
16 | role: string;
17 | status: string; // "active", "pending", etc.
18 | created_at: string;
19 | }
20 |
21 | export interface AuthToken {
22 | access_token: string;
23 | token_type: string;
24 | }
25 |
26 | export interface AuthState {
27 | user: User | null;
28 | token: string | null;
29 | isLoading: boolean;
30 | isAuthenticated: boolean;
31 | }
32 |
--------------------------------------------------------------------------------
/ui/src/lib/types/chat.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Types for the chat interface components and socket events
3 | */
4 |
5 | export enum AgentState {
6 | IDLE = "idle",
7 | PLANNING = "planning",
8 | EXECUTING = "executing",
9 | VERIFYING = "verifying",
10 | COMPLETED = "completed",
11 | ERROR = "error",
12 | }
13 |
14 | export interface PlanStep {
15 | step_id: string;
16 | tool: string;
17 | action: string;
18 | parameters: any;
19 | description: string;
20 | required: boolean;
21 | recursive: boolean;
22 | discovery_step: boolean;
23 | error_handling: {
24 | retry_count: number;
25 | retry_delay: number;
26 | fallback_action: string;
27 | };
28 | status?: "pending" | "executing" | "completed" | "failed";
29 | timestamp?: number; // For event ordering
30 | }
31 |
32 | export interface Plan {
33 | id: string; // Unique identifier for the plan
34 | query: string;
35 | intent: string;
36 | steps: PlanStep[];
37 | validation_criteria: string[];
38 | context: {
39 | requires_verification: boolean;
40 | additional_context: string;
41 | target_namespace?: string;
42 | resource_type?: string;
43 | discovery_context?: {
44 | resource_type: string;
45 | filters: string;
46 | };
47 | };
48 | created_at: number; // Timestamp when plan was created
49 | updated_at: number; // Timestamp of last update
50 | }
51 |
52 | export interface ChatMessage {
53 | id: string;
54 | from: "user" | "sky";
55 | message?: string;
56 | plan?: Plan;
57 | currentStep?: string;
58 | stepOutputs?: Record;
59 | timestamp: number;
60 | error?: string;
61 | }
62 |
63 | export interface SocketEvent {
64 | type: string;
65 | data?: T;
66 | timestamp: number;
67 | sequence_number?: number; // For ordering events
68 | }
69 |
70 | export interface PlannerStartEvent {
71 | conversation_id: string;
72 | query: string;
73 | }
74 |
75 | export interface PlannerCompleteEvent {
76 | conversation_id: string;
77 | plan: Plan;
78 | }
79 |
80 | export interface ExecutorProgressEvent {
81 | conversation_id: string;
82 | step_id: string;
83 | progress: number;
84 | status: string;
85 | }
86 |
87 | export interface StepOutputEvent {
88 | conversation_id: string;
89 | step_id: string;
90 | output: string;
91 | }
92 |
93 | export interface StepCompleteEvent {
94 | conversation_id: string;
95 | step_id: string;
96 | output?: string;
97 | }
98 |
99 | export interface StepFailedEvent {
100 | conversation_id: string;
101 | step_id: string;
102 | error: string;
103 | retry_count?: number;
104 | }
105 |
106 | export interface PlanFailedEvent {
107 | conversation_id: string;
108 | message: string;
109 | error_code?: string;
110 | }
111 |
112 | export interface VerifierStartEvent {
113 | conversation_id: string;
114 | }
115 |
116 | export interface VerifierCompleteEvent {
117 | conversation_id: string;
118 | validation_results: {
119 | success: boolean;
120 | message: string;
121 | details?: any;
122 | };
123 | }
124 |
125 | // State management types
126 | export interface ChatState {
127 | messages: ChatMessage[];
128 | currentPlan: Plan | null;
129 | agentState: AgentState;
130 | currentPhase: string;
131 | progress: number;
132 | error: string | null;
133 | isConnected: boolean;
134 | lastEventTimestamp: number;
135 | }
136 |
137 | export interface ChatStateUpdate {
138 | type: string;
139 | payload: Partial;
140 | timestamp: number;
141 | }
142 |
--------------------------------------------------------------------------------
/ui/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
8 | // Cookie management functions for client-side
9 | export function getCookie(name: string): string | null {
10 | if (typeof document === "undefined") return null;
11 |
12 | const cookies = document.cookie.split(";");
13 | for (let i = 0; i < cookies.length; i++) {
14 | const cookie = cookies[i].trim();
15 | if (cookie.startsWith(name + "=")) {
16 | return cookie.substring(name.length + 1);
17 | }
18 | }
19 | return null;
20 | }
21 |
22 | export function setCookie(name: string, value: string, days?: number): void {
23 | if (typeof document === "undefined") return;
24 |
25 | let expires = "";
26 | if (days) {
27 | const date = new Date();
28 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
29 | expires = "; expires=" + date.toUTCString();
30 | }
31 |
32 | document.cookie = name + "=" + value + expires + "; path=/";
33 | }
34 |
35 | export function removeCookie(name: string): void {
36 | if (typeof document === "undefined") return;
37 |
38 | setCookie(name, "", -1);
39 | }
40 |
--------------------------------------------------------------------------------
/ui/src/store/useAuthStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist, createJSONStorage } from "zustand/middleware";
3 | import { User, AuthState } from "@/lib/types/auth";
4 |
5 | interface AuthStore extends AuthState {
6 | login: (user: User, token: string) => void;
7 | logout: () => void;
8 | setLoading: (isLoading: boolean) => void;
9 | }
10 |
11 | export const useAuthStore = create()(
12 | persist(
13 | (set) => ({
14 | user: null,
15 | token: null,
16 | isLoading: false,
17 | isAuthenticated: false,
18 |
19 | login: (user: User, token: string) =>
20 | set({
21 | user,
22 | token,
23 | isAuthenticated: true,
24 | isLoading: false,
25 | }),
26 |
27 | logout: () =>
28 | set({
29 | user: null,
30 | token: null,
31 | isAuthenticated: false,
32 | isLoading: false,
33 | }),
34 |
35 | setLoading: (isLoading: boolean) => set({ isLoading }),
36 | }),
37 | {
38 | name: "skyflo-auth-storage",
39 | storage: createJSONStorage(() => localStorage),
40 | partialize: (state) => ({
41 | user: state.user,
42 | token: state.token,
43 | isAuthenticated: state.isAuthenticated,
44 | }),
45 | }
46 | )
47 | );
48 |
--------------------------------------------------------------------------------
/ui/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | colors: {
12 | dark: {
13 | DEFAULT: "#070708",
14 | secondary: "#1c1e24",
15 | navbar: "#070708",
16 | hover: "#16161a",
17 | active: "#1B1B1C",
18 | red: "#f54257",
19 | },
20 | border: {
21 | DEFAULT: "#1c1c1c",
22 | focus: "#545457",
23 | menu: "#4E4E50",
24 | },
25 | button: {
26 | primary: "#2e87e6", // 2e87e6, purple: 8e30d1
27 | hover: "#1a6fc9",
28 | },
29 | },
30 | borderRadius: {
31 | lg: "var(--radius)",
32 | md: "calc(var(--radius) - 2px)",
33 | sm: "calc(var(--radius) - 4px)",
34 | },
35 | fontFamily: {
36 | sans: ["Inter"],
37 | },
38 | },
39 | },
40 | plugins: [],
41 | };
42 |
43 | export default config;
44 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": false,
7 | "noImplicitAny": false,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./src/*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | zustand@^5.0.3:
6 | version "5.0.3"
7 | resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.3.tgz#b323435b73d06b2512e93c77239634374b0e407f"
8 | integrity sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==
9 |
--------------------------------------------------------------------------------