├── .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 | Skyflo.ai 5 |

6 | 7 |
8 | 9 | [![Website](https://img.shields.io/badge/Website-Visit-blue.svg)](https://skyflo.ai) 10 | [![Discord](https://img.shields.io/badge/Discord-Join-blue.svg)](https://discord.gg/kCFNavMund) 11 | [![Twitter/X Follow](https://img.shields.io/twitter/follow/skyflo_ai?style=social)](https://x.com/skyflo_ai) 12 | [![YouTube Channel](https://img.shields.io/badge/YouTube-Subscribe-red.svg)](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 |
15 | 16 |
17 | 21 |
22 |
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 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
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 |
18 |
19 |
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 |
35 | 36 |
37 | 38 |
39 |
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 |
15 | 16 |
17 |
18 | 19 |
20 |
21 |
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 |
7 | 20 |
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 |
25 |
26 | 27 |
28 | 36 |
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 |
51 | 58 | 65 | {error && ( 66 |
67 |

{error}

68 |
69 | )} 70 | 84 | 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 |
51 | 58 | 65 | 72 | {error && ( 73 |
74 |

{error}

75 |
76 | )} 77 | 91 | 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 |
65 |
66 | 67 |
68 |
69 |
70 | 71 | {/* Two Column Layout */} 72 |
73 | {/* Left Column - Profile Settings */} 74 |
75 | 76 |
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 | Skyflo.ai 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 |
34 |
35 |
36 |
37 |
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 | --------------------------------------------------------------------------------