``` ├── .cursor/ ├── rules/ ├── summary.mdc ├── .env.example ├── .eslintrc.cjs ├── .github/ ├── FUNDING.yml ├── workflows/ ├── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend/ ├── Dockerfile ├── app/ ├── __init__.py ├── core/ ├── limiter.py ├── main.py ├── prompts.py ├── routers/ ├── generate.py ├── modify.py ├── services/ ├── claude_service.py ├── github_service.py ├── o1_mini_openai_service.py ├── o3_mini_openai_service.py ├── o3_mini_openrouter_service.py ├── o4_mini_openai_service.py ├── utils/ ├── format_message.py ├── deploy.sh ├── entrypoint.sh ├── nginx/ ├── api.conf ├── setup_nginx.sh ├── requirements.txt ├── components.json ├── docker-compose.yml ├── docs/ ├── readme_img.png ├── drizzle.config.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ``` ## /.cursor/rules/summary.mdc ```mdc path="/.cursor/rules/summary.mdc" --- description: summary of project globs: alwaysApply: true --- # GitDiagram Project Summary ## Project Overview This is GitDiagram, a web application that converts any GitHub repository structure into an interactive system design/architecture diagram for visualization. It allows users to quickly understand the architecture of any repository by generating visual diagrams, and provides interactivity by letting users click on components to navigate directly to source files and relevant directories. ## Key Features - Instant conversion of GitHub repositories into system design diagrams - Interactive components that link to source files and directories - Support for both public and private repositories (with GitHub token) - Customizable diagrams through user instructions - URL shortcut: replace `hub` with `diagram` in any GitHub URL to access its diagram ## Tech Stack - **Frontend**: Next.js 15, TypeScript, Tailwind CSS, ShadCN UI components - **Backend**: FastAPI (Python), Server Actions - **Database**: PostgreSQL with Drizzle ORM, Neon Database for serverless PostgreSQL - **AI**: Claude 3.5 Sonnet (previously) / OpenAI o3-mini (currently) for diagram generation - **Deployment**: Vercel (Frontend), EC2 (Backend) - **CI/CD**: GitHub Actions - **Analytics**: PostHog, Api-Analytics ## Architecture The project follows a modern full-stack architecture: 1. **Frontend (Next.js)**: - Organized using the App Router pattern - Uses server components and server actions - Implements Mermaid.js for rendering diagrams - Provides UI for repository input and diagram customization 2. **Backend (FastAPI)**: - Handles repository data extraction - Implements complex prompt engineering through a pipeline: - First prompt analyzes the repository and creates an explanation - Second prompt maps relevant directories and files to diagram components - Third prompt generates the final Mermaid.js code - Manages API rate limiting and authentication 3. **Database (PostgreSQL)**: - Stores user data, repository information, and generated diagrams - Uses Drizzle ORM for type-safe database operations 4. **AI Integration**: - Uses LLMs to analyze repository structure - Generates detailed diagrams based on file trees and README content - Implements sophisticated prompt engineering to extract accurate information ## Project Structure - `/src`: Frontend source code (Next.js) and server actions for db calls with drizzle - `/backend`: Python FastAPI backend - `/public`: Static assets - `/docs`: Documentation and images ## Development Setup The project supports both local development and self-hosting: - Dependencies managed with pnpm - Docker Compose for containerization - Environment configuration via .env files - Database initialization scripts ## Future Development - Implementation of font-awesome icons in diagrams - Embedded feature for progressive diagram updates as commits are made - Expanded API access for third-party integration ``` ## /.env.example ```example path="/.env.example" POSTGRES_URL="postgresql://postgres:password@localhost:5432/gitdiagram" NEXT_PUBLIC_API_DEV_URL=http://localhost:8000 OPENAI_API_KEY= # OPTIONAL: providing your own GitHub PAT increases rate limits from 60/hr to 5000/hr to the GitHub API GITHUB_PAT= # old implementation # OPENROUTER_API_KEY= # ANTHROPIC_API_KEY= ``` ## /.eslintrc.cjs ```cjs path="/.eslintrc.cjs" /** @type {import("eslint").Linter.Config} */ const config = { "parser": "@typescript-eslint/parser", "parserOptions": { "project": true }, "plugins": [ "@typescript-eslint", "drizzle" ], "extends": [ "next/core-web-vitals", "plugin:@typescript-eslint/recommended-type-checked", "plugin:@typescript-eslint/stylistic-type-checked" ], "rules": { "@typescript-eslint/array-type": "off", "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/consistent-type-imports": [ "warn", { "prefer": "type-imports", "fixStyle": "inline-type-imports" } ], "@typescript-eslint/no-unused-vars": [ "warn", { "argsIgnorePattern": "^_" } ], "@typescript-eslint/require-await": "off", "@typescript-eslint/no-misused-promises": [ "error", { "checksVoidReturn": { "attributes": false } } ], "drizzle/enforce-delete-with-where": [ "error", { "drizzleObjectName": [ "db", "ctx.db" ] } ], "drizzle/enforce-update-with-where": [ "error", { "drizzleObjectName": [ "db", "ctx.db" ] } ] } } module.exports = config; ``` ## /.github/FUNDING.yml ```yml path="/.github/FUNDING.yml" # These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: ahmedkhaleel2004 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username thanks_dev: # Replace with a single thanks.dev username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ``` ## /.github/workflows/deploy.yml ```yml path="/.github/workflows/deploy.yml" name: Deploy to EC2 on: push: branches: [main] paths: - "backend/**" # Only trigger on backend changes - ".github/workflows/**" jobs: deploy: runs-on: ubuntu-latest # Add concurrency to prevent multiple deployments running at once concurrency: group: production cancel-in-progress: true steps: - uses: actions/checkout@v4 - name: Deploy to EC2 uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST }} username: ubuntu key: ${{ secrets.EC2_SSH_KEY }} script: | cd ~/gitdiagram git fetch origin main git reset --hard origin/main # Force local to match remote main sudo chmod +x ./backend/nginx/setup_nginx.sh sudo ./backend/nginx/setup_nginx.sh chmod +x ./backend/deploy.sh ./backend/deploy.sh ``` ## /.gitignore ```gitignore path="/.gitignore" # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # database /prisma/db.sqlite /prisma/db.sqlite-journal db.sqlite # next.js /.next/ /out/ next-env.d.ts # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables .env .env*.local .env-e # vercel .vercel # typescript *.tsbuildinfo # idea files .idea __pycache__/ venv # vscode .vscode/ ``` ## /LICENSE ``` path="/LICENSE" MIT License Copyright (c) 2024 Ahmed Khaleel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## /README.md [![Image](./docs/readme_img.png "GitDiagram Front Page")](https://gitdiagram.com/) ![License](https://img.shields.io/badge/license-MIT-blue.svg) [![Kofi](https://img.shields.io/badge/Kofi-F16061.svg?logo=ko-fi&logoColor=white)](https://ko-fi.com/ahmedkhaleel2004) # GitDiagram Turn any GitHub repository into an interactive diagram for visualization in seconds. You can also replace `hub` with `diagram` in any Github URL to access its diagram. ## 🚀 Features - 👀 **Instant Visualization**: Convert any GitHub repository structure into a system design / architecture diagram - 🎨 **Interactivity**: Click on components to navigate directly to source files and relevant directories - ⚡ **Fast Generation**: Powered by OpenAI o4-mini for quick and accurate diagrams - 🔄 **Customization**: Modify and regenerate diagrams with custom instructions - 🌐 **API Access**: Public API available for integration (WIP) ## ⚙️ Tech Stack - **Frontend**: Next.js, TypeScript, Tailwind CSS, ShadCN - **Backend**: FastAPI, Python, Server Actions - **Database**: PostgreSQL (with Drizzle ORM) - **AI**: OpenAI o4-mini - **Deployment**: Vercel (Frontend), EC2 (Backend) - **CI/CD**: GitHub Actions - **Analytics**: PostHog, Api-Analytics ## 🤔 About I created this because I wanted to contribute to open-source projects but quickly realized their codebases are too massive for me to dig through manually, so this helps me get started - but it's definitely got many more use cases! Given any public (or private!) GitHub repository it generates diagrams in Mermaid.js with OpenAI's o4-mini! (Previously Claude 3.5 Sonnet) I extract information from the file tree and README for details and interactivity (you can click components to be taken to relevant files and directories) Most of what you might call the "processing" of this app is done with prompt engineering - see `/backend/app/prompts.py`. This basically extracts and pipelines data and analysis for a larger action workflow, ending in the diagram code. ## 🔒 How to diagram private repositories You can simply click on "Private Repos" in the header and follow the instructions by providing a GitHub personal access token with the `repo` scope. You can also self-host this app locally (backend separated as well!) with the steps below. ## 🛠️ Self-hosting / Local Development 1. Clone the repository ```bash git clone https://github.com/ahmedkhaleel2004/gitdiagram.git cd gitdiagram ``` 2. Install dependencies ```bash pnpm i ``` 3. Set up environment variables (create .env) ```bash cp .env.example .env ``` Then edit the `.env` file with your Anthropic API key and optional GitHub personal access token. 4. Run backend ```bash docker-compose up --build -d ``` Logs available at `docker-compose logs -f` The FastAPI server will be available at `localhost:8000` 5. Start local database ```bash chmod +x start-database.sh ./start-database.sh ``` When prompted to generate a random password, input yes. The Postgres database will start in a container at `localhost:5432` 6. Initialize the database schema ```bash pnpm db:push ``` You can view and interact with the database using `pnpm db:studio` 7. Run Frontend ```bash pnpm dev ``` You can now access the website at `localhost:3000` and edit the rate limits defined in `backend/app/routers/generate.py` in the generate function decorator. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## Acknowledgements Shoutout to [Romain Courtois](https://github.com/cyclotruc)'s [Gitingest](https://gitingest.com/) for inspiration and styling ## 📈 Rate Limits I am currently hosting it for free with no rate limits though this is somewhat likely to change in the future. ## 🤔 Future Steps - Implement font-awesome icons in diagram - Implement an embedded feature like star-history.com but for diagrams. The diagram could also be updated progressively as commits are made. ## /backend/Dockerfile ``` path="/backend/Dockerfile" # Use Python 3.12 slim image for smaller size FROM python:3.12-slim # Set working directory WORKDIR /app # Copy requirements first to leverage Docker cache COPY requirements.txt . # Install dependencies RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . # Create and set permissions for entrypoint script COPY entrypoint.sh /app/ RUN chmod +x /app/entrypoint.sh && \ # Ensure the script uses Unix line endings sed -i 's/\r$//' /app/entrypoint.sh && \ # Double check permissions ls -la /app/entrypoint.sh # Expose port EXPOSE 8000 # Use entrypoint script CMD ["/bin/bash", "/app/entrypoint.sh"] ``` ## /backend/app/__init__.py ```py path="/backend/app/__init__.py" ``` ## /backend/app/core/limiter.py ```py path="/backend/app/core/limiter.py" from slowapi import Limiter from slowapi.util import get_remote_address limiter = Limiter(key_func=get_remote_address) ``` ## /backend/app/main.py ```py path="/backend/app/main.py" from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from app.routers import generate, modify from app.core.limiter import limiter from typing import cast from starlette.exceptions import ExceptionMiddleware from api_analytics.fastapi import Analytics import os app = FastAPI() origins = ["http://localhost:3000", "https://gitdiagram.com"] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["GET", "POST"], allow_headers=["*"], ) API_ANALYTICS_KEY = os.getenv("API_ANALYTICS_KEY") if API_ANALYTICS_KEY: app.add_middleware(Analytics, api_key=API_ANALYTICS_KEY) app.state.limiter = limiter app.add_exception_handler( RateLimitExceeded, cast(ExceptionMiddleware, _rate_limit_exceeded_handler) ) app.include_router(generate.router) app.include_router(modify.router) @app.get("/") # @limiter.limit("100/day") async def root(request: Request): return {"message": "Hello from GitDiagram API!"} ``` ## /backend/app/prompts.py ```py path="/backend/app/prompts.py" # This is our processing. This is where GitDiagram makes the magic happen # There is a lot of DETAIL we need to extract from the repository to produce detailed and accurate diagrams # I will immediately put out there that I'm trying to reduce costs. Theoretically, I could, for like 5x better accuracy, include most file content as well which would make for perfect diagrams, but thats too many tokens for my wallet, and would probably greatly increase generation time. (maybe a paid feature?) # THE PROCESS: # imagine it like this: # def prompt1(file_tree, readme) -> explanation of diagram # def prompt2(explanation, file_tree) -> maps relevant directories and files to parts of diagram for interactivity # def prompt3(explanation, map) -> Mermaid.js code # Note: Originally prompt1 and prompt2 were combined - but I tested it, and turns out mapping relevant dirs and files in one prompt along with generating detailed and accurate diagrams was difficult for Claude 3.5 Sonnet. It lost detail in the explanation and dedicated more "effort" to the mappings, so this is now its own prompt. # This is my first take at prompt engineering so if you have any ideas on optimizations please make an issue on the GitHub! SYSTEM_FIRST_PROMPT = """ You are tasked with explaining to a principal software engineer how to draw the best and most accurate system design diagram / architecture of a given project. This explanation should be tailored to the specific project's purpose and structure. To accomplish this, you will be provided with two key pieces of information: 1. The complete and entire file tree of the project including all directory and file names, which will be enclosed in tags in the users message. 2. The README file of the project, which will be enclosed in tags in the users message. Analyze these components carefully, as they will provide crucial information about the project's structure and purpose. Follow these steps to create an explanation for the principal software engineer: 1. Identify the project type and purpose: - Examine the file structure and README to determine if the project is a full-stack application, an open-source tool, a compiler, or another type of software imaginable. - Look for key indicators in the README, such as project description, features, or use cases. 2. Analyze the file structure: - Pay attention to top-level directories and their names (e.g., "frontend", "backend", "src", "lib", "tests"). - Identify patterns in the directory structure that might indicate architectural choices (e.g., MVC pattern, microservices). - Note any configuration files, build scripts, or deployment-related files. 3. Examine the README for additional insights: - Look for sections describing the architecture, dependencies, or technical stack. - Check for any diagrams or explanations of the system's components. 4. Based on your analysis, explain how to create a system design diagram that accurately represents the project's architecture. Include the following points: a. Identify the main components of the system (e.g., frontend, backend, database, building, external services). b. Determine the relationships and interactions between these components. c. Highlight any important architectural patterns or design principles used in the project. d. Include relevant technologies, frameworks, or libraries that play a significant role in the system's architecture. 5. Provide guidelines for tailoring the diagram to the specific project type: - For a full-stack application, emphasize the separation between frontend and backend, database interactions, and any API layers. - For an open-source tool, focus on the core functionality, extensibility points, and how it integrates with other systems. - For a compiler or language-related project, highlight the different stages of compilation or interpretation, and any intermediate representations. 6. Instruct the principal software engineer to include the following elements in the diagram: - Clear labels for each component - Directional arrows to show data flow or dependencies - Color coding or shapes to distinguish between different types of components 7. NOTE: Emphasize the importance of being very detailed and capturing the essential architectural elements. Don't overthink it too much, simply separating the project into as many components as possible is best. Present your explanation and instructions within tags, ensuring that you tailor your advice to the specific project based on the provided file tree and README content. """ # - A legend explaining any symbols or abbreviations used # ^ removed since it was making the diagrams very long # just adding some clear separation between the prompts # ************************************************************ # ************************************************************ SYSTEM_SECOND_PROMPT = """ You are tasked with mapping key components of a system design to their corresponding files and directories in a project's file structure. You will be provided with a detailed explanation of the system design/architecture and a file tree of the project. First, carefully read the system design explanation which will be enclosed in tags in the users message. Then, examine the file tree of the project which will be enclosed in tags in the users message. Your task is to analyze the system design explanation and identify key components, modules, or services mentioned. Then, try your best to map these components to what you believe could be their corresponding directories and files in the provided file tree. Guidelines: 1. Focus on major components described in the system design. 2. Look for directories and files that clearly correspond to these components. 3. Include both directories and specific files when relevant. 4. If a component doesn't have a clear corresponding file or directory, simply dont include it in the map. Now, provide your final answer in the following format: 1. [Component Name]: [File/Directory Path] 2. [Component Name]: [File/Directory Path] [Continue for all identified components] Remember to be as specific as possible in your mappings, only use what is given to you from the file tree, and to strictly follow the components mentioned in the explanation. """ # ❌ BELOW IS A REMOVED SECTION FROM THE ABOVE PROMPT USED FOR CLAUDE 3.5 SONNET # Before providing your final answer, use the to think through your process: # 1. List the key components identified in the system design. # 2. For each component, brainstorm potential corresponding directories or files. # 3. Verify your mappings by double-checking the file tree. # # [Your thought process here] # # just adding some clear separation between the prompts # ************************************************************ # ************************************************************ SYSTEM_THIRD_PROMPT = """ You are a principal software engineer tasked with creating a system design diagram using Mermaid.js based on a detailed explanation. Your goal is to accurately represent the architecture and design of the project as described in the explanation. The detailed explanation of the design will be enclosed in tags in the users message. Also, sourced from the explanation, as a bonus, a few of the identified components have been mapped to their paths in the project file tree, whether it is a directory or file which will be enclosed in tags in the users message. To create the Mermaid.js diagram: 1. Carefully read and analyze the provided design explanation. 2. Identify the main components, services, and their relationships within the system. 3. Determine the appropriate Mermaid.js diagram type to use (e.g., flowchart, sequence diagram, class diagram, architecture, etc.) based on the nature of the system described. 4. Create the Mermaid.js code to represent the design, ensuring that: a. All major components are included b. Relationships between components are clearly shown c. The diagram accurately reflects the architecture described in the explanation d. The layout is logical and easy to understand Guidelines for diagram components and relationships: - Use appropriate shapes for different types of components (e.g., rectangles for services, cylinders for databases, etc.) - Use clear and concise labels for each component - Show the direction of data flow or dependencies using arrows - Group related components together if applicable - Include any important notes or annotations mentioned in the explanation - Just follow the explanation. It will have everything you need. IMPORTANT!!: Please orient and draw the diagram as vertically as possible. You must avoid long horizontal lists of nodes and sections! You must include click events for components of the diagram that have been specified in the provided : - Do not try to include the full url. This will be processed by another program afterwards. All you need to do is include the path. - For example: - This is a correct click event: `click Example "app/example.js"` - This is an incorrect click event: `click Example "https://github.com/username/repo/blob/main/app/example.js"` - Do this for as many components as specified in the component mapping, include directories and files. - If you believe the component contains files and is a directory, include the directory path. - If you believe the component references a specific file, include the file path. - Make sure to include the full path to the directory or file exactly as specified in the component mapping. - It is very important that you do this for as many files as possible. The more the better. - IMPORTANT: THESE PATHS ARE FOR CLICK EVENTS ONLY, these paths should not be included in the diagram's node's names. Only for the click events. Paths should not be seen by the user. Your output should be valid Mermaid.js code that can be rendered into a diagram. Do not include an init declaration such as `%%{init: {'key':'etc'}}%%`. This is handled externally. Just return the diagram code. Your response must strictly be just the Mermaid.js code, without any additional text or explanations. No code fence or markdown ticks needed, simply return the Mermaid.js code. Ensure that your diagram adheres strictly to the given explanation, without adding or omitting any significant components or relationships. For general direction, the provided example below is how you should structure your code: \`\`\`mermaid flowchart TD %% or graph TD, your choice %% Global entities A("Entity A"):::external %% more... %% Subgraphs and modules subgraph "Layer A" A1("Module A"):::example %% more modules... %% inner subgraphs if needed... end %% more subgraphs, modules, etc... %% Connections A -->|"relationship"| B %% and a lot more... %% Click Events click A1 "example/example.js" %% and a lot more... %% Styles classDef frontend %%... %% and a lot more... \`\`\` EXTREMELY Important notes on syntax!!! (PAY ATTENTION TO THIS): - Make sure to add colour to the diagram!!! This is extremely critical. - In Mermaid.js syntax, we cannot include special characters for nodes without being inside quotes! For example: `EX[/api/process (Backend)]:::api` and `API -->|calls Process()| Backend` are two examples of syntax errors. They should be `EX["/api/process (Backend)"]:::api` and `API -->|"calls Process()"| Backend` respectively. Notice the quotes. This is extremely important. Make sure to include quotes for any string that contains special characters. - In Mermaid.js syntax, you cannot apply a class style directly within a subgraph declaration. For example: `subgraph "Frontend Layer":::frontend` is a syntax error. However, you can apply them to nodes within the subgraph. For example: `Example["Example Node"]:::frontend` is valid, and `class Example1,Example2 frontend` is valid. - In Mermaid.js syntax, there cannot be spaces in the relationship label names. For example: `A -->| "example relationship" | B` is a syntax error. It should be `A -->|"example relationship"| B` - In Mermaid.js syntax, you cannot give subgraphs an alias like nodes. For example: `subgraph A "Layer A"` is a syntax error. It should be `subgraph "Layer A"` """ # ^^^ note: ive generated a few diagrams now and claude still writes incorrect mermaid code sometimes. in the future, refer to those generated diagrams and add important instructions to the prompt above to avoid those mistakes. examples are best. # e. A legend is included # ^ removed since it was making the diagrams very long ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT = """ IMPORTANT: the user will provide custom additional instructions enclosed in tags. Please take these into account and give priority to them. However, if these instructions are unrelated to the task, unclear, or not possible to follow, ignore them by simply responding with: "BAD_INSTRUCTIONS" """ SYSTEM_MODIFY_PROMPT = """ You are tasked with modifying the code of a Mermaid.js diagram based on the provided instructions. The diagram will be enclosed in tags in the users message. Also, to help you modify it and simply for additional context, you will also be provided with the original explanation of the diagram enclosed in tags in the users message. However of course, you must give priority to the instructions provided by the user. The instructions will be enclosed in tags in the users message. If these instructions are unrelated to the task, unclear, or not possible to follow, ignore them by simply responding with: "BAD_INSTRUCTIONS" Your response must strictly be just the Mermaid.js code, without any additional text or explanations. Keep as many of the existing click events as possible. No code fence or markdown ticks needed, simply return the Mermaid.js code. """ ``` ## /backend/app/routers/generate.py ```py path="/backend/app/routers/generate.py" from fastapi import APIRouter, Request, HTTPException from fastapi.responses import StreamingResponse from dotenv import load_dotenv from app.services.github_service import GitHubService from app.services.o4_mini_openai_service import OpenAIo4Service from app.prompts import ( SYSTEM_FIRST_PROMPT, SYSTEM_SECOND_PROMPT, SYSTEM_THIRD_PROMPT, ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT, ) from anthropic._exceptions import RateLimitError from pydantic import BaseModel from functools import lru_cache import re import json import asyncio # from app.services.claude_service import ClaudeService # from app.core.limiter import limiter load_dotenv() router = APIRouter(prefix="/generate", tags=["OpenAI o4-mini"]) # Initialize services # claude_service = ClaudeService() o4_service = OpenAIo4Service() # cache github data to avoid double API calls from cost and generate @lru_cache(maxsize=100) def get_cached_github_data(username: str, repo: str, github_pat: str | None = None): # Create a new service instance for each call with the appropriate PAT current_github_service = GitHubService(pat=github_pat) default_branch = current_github_service.get_default_branch(username, repo) if not default_branch: default_branch = "main" # fallback value file_tree = current_github_service.get_github_file_paths_as_list(username, repo) readme = current_github_service.get_github_readme(username, repo) return {"default_branch": default_branch, "file_tree": file_tree, "readme": readme} class ApiRequest(BaseModel): username: str repo: str instructions: str = "" api_key: str | None = None github_pat: str | None = None @router.post("/cost") # @limiter.limit("5/minute") # TEMP: disable rate limit for growth?? async def get_generation_cost(request: Request, body: ApiRequest): try: # Get file tree and README content github_data = get_cached_github_data(body.username, body.repo, body.github_pat) file_tree = github_data["file_tree"] readme = github_data["readme"] # Calculate combined token count # file_tree_tokens = claude_service.count_tokens(file_tree) # readme_tokens = claude_service.count_tokens(readme) file_tree_tokens = o4_service.count_tokens(file_tree) readme_tokens = o4_service.count_tokens(readme) # CLAUDE: Calculate approximate cost # Input cost: $3 per 1M tokens ($0.000003 per token) # Output cost: $15 per 1M tokens ($0.000015 per token) # input_cost = ((file_tree_tokens * 2 + readme_tokens) + 3000) * 0.000003 # output_cost = 3500 * 0.000015 # estimated_cost = input_cost + output_cost # Input cost: $1.1 per 1M tokens ($0.0000011 per token) # Output cost: $4.4 per 1M tokens ($0.0000044 per token) input_cost = ((file_tree_tokens * 2 + readme_tokens) + 3000) * 0.0000011 output_cost = ( 8000 * 0.0000044 ) # 8k just based on what I've seen (reasoning is expensive) estimated_cost = input_cost + output_cost # Format as currency string cost_string = f"${estimated_cost:.2f} USD" return {"cost": cost_string} except Exception as e: return {"error": str(e)} def process_click_events(diagram: str, username: str, repo: str, branch: str) -> str: """ Process click events in Mermaid diagram to include full GitHub URLs. Detects if path is file or directory and uses appropriate URL format. """ def replace_path(match): # Extract the path from the click event path = match.group(2).strip("\"'") # Determine if path is likely a file (has extension) or directory is_file = "." in path.split("/")[-1] # Construct GitHub URL base_url = f"https://github.com/{username}/{repo}" path_type = "blob" if is_file else "tree" full_url = f"{base_url}/{path_type}/{branch}/{path}" # Return the full click event with the new URL return f'click {match.group(1)} "{full_url}"' # Match click events: click ComponentName "path/to/something" click_pattern = r'click ([^\s"]+)\s+"([^"]+)"' return re.sub(click_pattern, replace_path, diagram) @router.post("/stream") async def generate_stream(request: Request, body: ApiRequest): try: # Initial validation checks if len(body.instructions) > 1000: return {"error": "Instructions exceed maximum length of 1000 characters"} if body.repo in [ "fastapi", "streamlit", "flask", "api-analytics", "monkeytype", ]: return {"error": "Example repos cannot be regenerated"} async def event_generator(): try: # Get cached github data github_data = get_cached_github_data( body.username, body.repo, body.github_pat ) default_branch = github_data["default_branch"] file_tree = github_data["file_tree"] readme = github_data["readme"] # Send initial status yield f"data: {json.dumps({'status': 'started', 'message': 'Starting generation process...'})}\n\n" await asyncio.sleep(0.1) # Token count check combined_content = f"{file_tree}\n{readme}" token_count = o4_service.count_tokens(combined_content) if 50000 < token_count < 195000 and not body.api_key: yield f"data: {json.dumps({'error': f'File tree and README combined exceeds token limit (50,000). Current size: {token_count} tokens. This GitHub repository is too large for my wallet, but you can continue by providing your own OpenAI API key.'})}\n\n" return elif token_count > 195000: yield f"data: {json.dumps({'error': f'Repository is too large (>195k tokens) for analysis. OpenAI o4-mini\'s max context length is 200k tokens. Current size: {token_count} tokens.'})}\n\n" return # Prepare prompts first_system_prompt = SYSTEM_FIRST_PROMPT third_system_prompt = SYSTEM_THIRD_PROMPT if body.instructions: first_system_prompt = ( first_system_prompt + "\n" + ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT ) third_system_prompt = ( third_system_prompt + "\n" + ADDITIONAL_SYSTEM_INSTRUCTIONS_PROMPT ) # Phase 1: Get explanation yield f"data: {json.dumps({'status': 'explanation_sent', 'message': 'Sending explanation request to o4-mini...'})}\n\n" await asyncio.sleep(0.1) yield f"data: {json.dumps({'status': 'explanation', 'message': 'Analyzing repository structure...'})}\n\n" explanation = "" async for chunk in o4_service.call_o4_api_stream( system_prompt=first_system_prompt, data={ "file_tree": file_tree, "readme": readme, "instructions": body.instructions, }, api_key=body.api_key, reasoning_effort="medium", ): explanation += chunk yield f"data: {json.dumps({'status': 'explanation_chunk', 'chunk': chunk})}\n\n" if "BAD_INSTRUCTIONS" in explanation: yield f"data: {json.dumps({'error': 'Invalid or unclear instructions provided'})}\n\n" return # Phase 2: Get component mapping yield f"data: {json.dumps({'status': 'mapping_sent', 'message': 'Sending component mapping request to o4-mini...'})}\n\n" await asyncio.sleep(0.1) yield f"data: {json.dumps({'status': 'mapping', 'message': 'Creating component mapping...'})}\n\n" full_second_response = "" async for chunk in o4_service.call_o4_api_stream( system_prompt=SYSTEM_SECOND_PROMPT, data={"explanation": explanation, "file_tree": file_tree}, api_key=body.api_key, reasoning_effort="low", ): full_second_response += chunk yield f"data: {json.dumps({'status': 'mapping_chunk', 'chunk': chunk})}\n\n" # i dont think i need this anymore? but keep it here for now # Extract component mapping start_tag = "" end_tag = "" component_mapping_text = full_second_response[ full_second_response.find(start_tag) : full_second_response.find( end_tag ) ] # Phase 3: Generate Mermaid diagram yield f"data: {json.dumps({'status': 'diagram_sent', 'message': 'Sending diagram generation request to o4-mini...'})}\n\n" await asyncio.sleep(0.1) yield f"data: {json.dumps({'status': 'diagram', 'message': 'Generating diagram...'})}\n\n" mermaid_code = "" async for chunk in o4_service.call_o4_api_stream( system_prompt=third_system_prompt, data={ "explanation": explanation, "component_mapping": component_mapping_text, "instructions": body.instructions, }, api_key=body.api_key, reasoning_effort="low", ): mermaid_code += chunk yield f"data: {json.dumps({'status': 'diagram_chunk', 'chunk': chunk})}\n\n" # Process final diagram mermaid_code = mermaid_code.replace("\`\`\`mermaid", "").replace("\`\`\`", "") if "BAD_INSTRUCTIONS" in mermaid_code: yield f"data: {json.dumps({'error': 'Invalid or unclear instructions provided'})}\n\n" return processed_diagram = process_click_events( mermaid_code, body.username, body.repo, default_branch ) # Send final result yield f"data: {json.dumps({ 'status': 'complete', 'diagram': processed_diagram, 'explanation': explanation, 'mapping': component_mapping_text })}\n\n" except Exception as e: yield f"data: {json.dumps({'error': str(e)})}\n\n" return StreamingResponse( event_generator(), media_type="text/event-stream", headers={ "X-Accel-Buffering": "no", # Hint to Nginx "Cache-Control": "no-cache", "Connection": "keep-alive", }, ) except Exception as e: return {"error": str(e)} ``` ## /backend/app/routers/modify.py ```py path="/backend/app/routers/modify.py" from fastapi import APIRouter, Request, HTTPException from dotenv import load_dotenv # from app.services.claude_service import ClaudeService # from app.core.limiter import limiter from anthropic._exceptions import RateLimitError from app.prompts import SYSTEM_MODIFY_PROMPT from pydantic import BaseModel from app.services.o1_mini_openai_service import OpenAIO1Service load_dotenv() router = APIRouter(prefix="/modify", tags=["Claude"]) # Initialize services # claude_service = ClaudeService() o1_service = OpenAIO1Service() # Define the request body model class ModifyRequest(BaseModel): instructions: str current_diagram: str repo: str username: str explanation: str @router.post("") # @limiter.limit("2/minute;10/day") async def modify(request: Request, body: ModifyRequest): try: # Check instructions length if not body.instructions or not body.current_diagram: return {"error": "Instructions and/or current diagram are required"} elif ( len(body.instructions) > 1000 or len(body.current_diagram) > 100000 ): # just being safe return {"error": "Instructions exceed maximum length of 1000 characters"} if body.repo in [ "fastapi", "streamlit", "flask", "api-analytics", "monkeytype", ]: return {"error": "Example repos cannot be modified"} # modified_mermaid_code = claude_service.call_claude_api( # system_prompt=SYSTEM_MODIFY_PROMPT, # data={ # "instructions": body.instructions, # "explanation": body.explanation, # "diagram": body.current_diagram, # }, # ) modified_mermaid_code = o1_service.call_o1_api( system_prompt=SYSTEM_MODIFY_PROMPT, data={ "instructions": body.instructions, "explanation": body.explanation, "diagram": body.current_diagram, }, ) # Check for BAD_INSTRUCTIONS response if "BAD_INSTRUCTIONS" in modified_mermaid_code: return {"error": "Invalid or unclear instructions provided"} return {"diagram": modified_mermaid_code} except RateLimitError as e: raise HTTPException( status_code=429, detail="Service is currently experiencing high demand. Please try again in a few minutes.", ) except Exception as e: return {"error": str(e)} ``` ## /backend/app/services/claude_service.py ```py path="/backend/app/services/claude_service.py" from anthropic import Anthropic from dotenv import load_dotenv from app.utils.format_message import format_user_message load_dotenv() class ClaudeService: def __init__(self): self.default_client = Anthropic() def call_claude_api( self, system_prompt: str, data: dict, api_key: str | None = None ) -> str: """ Makes an API call to Claude and returns the response. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Returns: str: Claude's response text """ # Create the user message with the data user_message = format_user_message(data) # Use custom client if API key provided, otherwise use default client = Anthropic(api_key=api_key) if api_key else self.default_client message = client.messages.create( model="claude-3-5-sonnet-latest", max_tokens=4096, temperature=0, system=system_prompt, messages=[ {"role": "user", "content": [{"type": "text", "text": user_message}]} ], ) return message.content[0].text # type: ignore def count_tokens(self, prompt: str) -> int: """ Counts the number of tokens in a prompt. Args: prompt (str): The prompt to count tokens for Returns: int: Number of input tokens """ response = self.default_client.messages.count_tokens( model="claude-3-5-sonnet-latest", messages=[{"role": "user", "content": prompt}], ) return response.input_tokens ``` ## /backend/app/services/github_service.py ```py path="/backend/app/services/github_service.py" import requests import jwt import time from datetime import datetime, timedelta from dotenv import load_dotenv import os load_dotenv() class GitHubService: def __init__(self, pat: str | None = None): # Try app authentication first self.client_id = os.getenv("GITHUB_CLIENT_ID") self.private_key = os.getenv("GITHUB_PRIVATE_KEY") self.installation_id = os.getenv("GITHUB_INSTALLATION_ID") # Use provided PAT if available, otherwise fallback to env PAT self.github_token = pat or os.getenv("GITHUB_PAT") # If no credentials are provided, warn about rate limits if ( not all([self.client_id, self.private_key, self.installation_id]) and not self.github_token ): print( "\033[93mWarning: No GitHub credentials provided. Using unauthenticated requests with rate limit of 60 requests/hour.\033[0m" ) self.access_token = None self.token_expires_at = None # autopep8: off def _generate_jwt(self): now = int(time.time()) payload = { "iat": now, "exp": now + (10 * 60), # 10 minutes "iss": self.client_id, } # Convert PEM string format to proper newlines return jwt.encode(payload, self.private_key, algorithm="RS256") # type: ignore # autopep8: on def _get_installation_token(self): if self.access_token and self.token_expires_at > datetime.now(): # type: ignore return self.access_token jwt_token = self._generate_jwt() response = requests.post( f"https://api.github.com/app/installations/{ self.installation_id}/access_tokens", headers={ "Authorization": f"Bearer {jwt_token}", "Accept": "application/vnd.github+json", }, ) data = response.json() self.access_token = data["token"] self.token_expires_at = datetime.now() + timedelta(hours=1) return self.access_token def _get_headers(self): # If no credentials are available, return basic headers if ( not all([self.client_id, self.private_key, self.installation_id]) and not self.github_token ): return {"Accept": "application/vnd.github+json"} # Use PAT if available if self.github_token: return { "Authorization": f"token {self.github_token}", "Accept": "application/vnd.github+json", } # Otherwise use app authentication token = self._get_installation_token() return { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", } def _check_repository_exists(self, username, repo): """ Check if the repository exists using the GitHub API. """ api_url = f"https://api.github.com/repos/{username}/{repo}" response = requests.get(api_url, headers=self._get_headers()) if response.status_code == 404: raise ValueError("Repository not found.") elif response.status_code != 200: raise Exception( f"Failed to check repository: {response.status_code}, {response.json()}" ) def get_default_branch(self, username, repo): """Get the default branch of the repository.""" api_url = f"https://api.github.com/repos/{username}/{repo}" response = requests.get(api_url, headers=self._get_headers()) if response.status_code == 200: return response.json().get("default_branch") return None def get_github_file_paths_as_list(self, username, repo): """ Fetches the file tree of an open-source GitHub repository, excluding static files and generated code. Args: username (str): The GitHub username or organization name repo (str): The repository name Returns: str: A filtered and formatted string of file paths in the repository, one per line. """ def should_include_file(path): # Patterns to exclude excluded_patterns = [ # Dependencies "node_modules/", "vendor/", "venv/", # Compiled files ".min.", ".pyc", ".pyo", ".pyd", ".so", ".dll", ".class", # Asset files ".jpg", ".jpeg", ".png", ".gif", ".ico", ".svg", ".ttf", ".woff", ".webp", # Cache and temporary files "__pycache__/", ".cache/", ".tmp/", # Lock files and logs "yarn.lock", "poetry.lock", "*.log", # Configuration files ".vscode/", ".idea/", ] return not any(pattern in path.lower() for pattern in excluded_patterns) # Try to get the default branch first branch = self.get_default_branch(username, repo) if branch: api_url = f"https://api.github.com/repos/{ username}/{repo}/git/trees/{branch}?recursive=1" response = requests.get(api_url, headers=self._get_headers()) if response.status_code == 200: data = response.json() if "tree" in data: # Filter the paths and join them with newlines paths = [ item["path"] for item in data["tree"] if should_include_file(item["path"]) ] return "\n".join(paths) # If default branch didn't work or wasn't found, try common branch names for branch in ["main", "master"]: api_url = f"https://api.github.com/repos/{ username}/{repo}/git/trees/{branch}?recursive=1" response = requests.get(api_url, headers=self._get_headers()) if response.status_code == 200: data = response.json() if "tree" in data: # Filter the paths and join them with newlines paths = [ item["path"] for item in data["tree"] if should_include_file(item["path"]) ] return "\n".join(paths) raise ValueError( "Could not fetch repository file tree. Repository might not exist, be empty or private." ) def get_github_readme(self, username, repo): """ Fetches the README contents of an open-source GitHub repository. Args: username (str): The GitHub username or organization name repo (str): The repository name Returns: str: The contents of the README file. Raises: ValueError: If repository does not exist or has no README. Exception: For other unexpected API errors. """ # First check if the repository exists self._check_repository_exists(username, repo) # Then attempt to fetch the README api_url = f"https://api.github.com/repos/{username}/{repo}/readme" response = requests.get(api_url, headers=self._get_headers()) if response.status_code == 404: raise ValueError("No README found for the specified repository.") elif response.status_code != 200: raise Exception( f"Failed to fetch README: { response.status_code}, {response.json()}" ) data = response.json() readme_content = requests.get(data["download_url"]).text return readme_content ``` ## /backend/app/services/o1_mini_openai_service.py ```py path="/backend/app/services/o1_mini_openai_service.py" from openai import OpenAI from dotenv import load_dotenv from app.utils.format_message import format_user_message import tiktoken import os import aiohttp import json from typing import AsyncGenerator load_dotenv() class OpenAIO1Service: def __init__(self): self.default_client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), ) self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models self.base_url = "https://api.openai.com/v1/chat/completions" def call_o1_api( self, system_prompt: str, data: dict, api_key: str | None = None, ) -> str: """ Makes an API call to OpenAI o1-mini and returns the response. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Returns: str: o1-mini's response text """ # Create the user message with the data user_message = format_user_message(data) # Use custom client if API key provided, otherwise use default client = OpenAI(api_key=api_key) if api_key else self.default_client try: print( f"Making non-streaming API call to o1-mini with API key: {'custom key' if api_key else 'default key'}" ) completion = client.chat.completions.create( model="o1-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], max_completion_tokens=12000, # Adjust as needed temperature=0.2, ) print("API call completed successfully") if completion.choices[0].message.content is None: raise ValueError("No content returned from OpenAI o1-mini") return completion.choices[0].message.content except Exception as e: print(f"Error in OpenAI o1-mini API call: {str(e)}") raise async def call_o1_api_stream( self, system_prompt: str, data: dict, api_key: str | None = None, ) -> AsyncGenerator[str, None]: """ Makes a streaming API call to OpenAI o1-mini and yields the responses. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Yields: str: Chunks of o1-mini's response text """ # Create the user message with the data user_message = format_user_message(data) headers = { "Content-Type": "application/json", "Authorization": f"Bearer {api_key or self.default_client.api_key}", } payload = { "model": "o1-mini", "messages": [ { "role": "user", "content": f""" {system_prompt} {user_message} """, }, ], "max_completion_tokens": 12000, "stream": True, } try: async with aiohttp.ClientSession() as session: async with session.post( self.base_url, headers=headers, json=payload ) as response: if response.status != 200: error_text = await response.text() print(f"Error response: {error_text}") raise ValueError( f"OpenAI API returned status code {response.status}: {error_text}" ) line_count = 0 async for line in response.content: line = line.decode("utf-8").strip() if not line: continue line_count += 1 if line.startswith("data: "): if line == "data: [DONE]": break try: data = json.loads(line[6:]) content = ( data.get("choices", [{}])[0] .get("delta", {}) .get("content") ) if content: yield content except json.JSONDecodeError as e: print(f"JSON decode error: {e} for line: {line}") continue if line_count == 0: print("Warning: No lines received in stream response") except aiohttp.ClientError as e: print(f"Connection error: {str(e)}") raise ValueError(f"Failed to connect to OpenAI API: {str(e)}") except Exception as e: print(f"Unexpected error in streaming API call: {str(e)}") raise def count_tokens(self, prompt: str) -> int: """ Counts the number of tokens in a prompt. Args: prompt (str): The prompt to count tokens for Returns: int: Estimated number of input tokens """ num_tokens = len(self.encoding.encode(prompt)) return num_tokens ``` ## /backend/app/services/o3_mini_openai_service.py ```py path="/backend/app/services/o3_mini_openai_service.py" from openai import OpenAI from dotenv import load_dotenv from app.utils.format_message import format_user_message import tiktoken import os import aiohttp import json from typing import AsyncGenerator, Literal load_dotenv() class OpenAIo3Service: def __init__(self): self.default_client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), ) self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models self.base_url = "https://api.openai.com/v1/chat/completions" def call_o3_api( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> str: """ Makes an API call to OpenAI o3-mini and returns the response. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Returns: str: o3-mini's response text """ # Create the user message with the data user_message = format_user_message(data) # Use custom client if API key provided, otherwise use default client = OpenAI(api_key=api_key) if api_key else self.default_client try: print( f"Making non-streaming API call to o3-mini with API key: {'custom key' if api_key else 'default key'}" ) completion = client.chat.completions.create( model="o3-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], max_completion_tokens=12000, # Adjust as needed temperature=0.2, reasoning_effort=reasoning_effort, ) print("API call completed successfully") if completion.choices[0].message.content is None: raise ValueError("No content returned from OpenAI o3-mini") return completion.choices[0].message.content except Exception as e: print(f"Error in OpenAI o3-mini API call: {str(e)}") raise async def call_o3_api_stream( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> AsyncGenerator[str, None]: """ Makes a streaming API call to OpenAI o3-mini and yields the responses. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Yields: str: Chunks of o3-mini's response text """ # Create the user message with the data user_message = format_user_message(data) headers = { "Content-Type": "application/json", "Authorization": f"Bearer {api_key or self.default_client.api_key}", } # payload = { # "model": "o3-mini", # "messages": [ # { # "role": "user", # "content": f""" # # {system_prompt} # # # {user_message} # # """, # }, # ], # "max_completion_tokens": 12000, # "stream": True, # } payload = { "model": "o3-mini", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], "max_completion_tokens": 12000, "stream": True, "reasoning_effort": reasoning_effort, } try: async with aiohttp.ClientSession() as session: async with session.post( self.base_url, headers=headers, json=payload ) as response: if response.status != 200: error_text = await response.text() print(f"Error response: {error_text}") raise ValueError( f"OpenAI API returned status code {response.status}: {error_text}" ) line_count = 0 async for line in response.content: line = line.decode("utf-8").strip() if not line: continue line_count += 1 if line.startswith("data: "): if line == "data: [DONE]": break try: data = json.loads(line[6:]) content = ( data.get("choices", [{}])[0] .get("delta", {}) .get("content") ) if content: yield content except json.JSONDecodeError as e: print(f"JSON decode error: {e} for line: {line}") continue if line_count == 0: print("Warning: No lines received in stream response") except aiohttp.ClientError as e: print(f"Connection error: {str(e)}") raise ValueError(f"Failed to connect to OpenAI API: {str(e)}") except Exception as e: print(f"Unexpected error in streaming API call: {str(e)}") raise def count_tokens(self, prompt: str) -> int: """ Counts the number of tokens in a prompt. Args: prompt (str): The prompt to count tokens for Returns: int: Estimated number of input tokens """ num_tokens = len(self.encoding.encode(prompt)) return num_tokens ``` ## /backend/app/services/o3_mini_openrouter_service.py ```py path="/backend/app/services/o3_mini_openrouter_service.py" from openai import OpenAI from dotenv import load_dotenv from app.utils.format_message import format_user_message import tiktoken import os import aiohttp import json from typing import Literal, AsyncGenerator load_dotenv() class OpenRouterO3Service: def __init__(self): self.default_client = OpenAI( base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENROUTER_API_KEY"), ) self.encoding = tiktoken.get_encoding("o200k_base") self.base_url = "https://openrouter.ai/api/v1/chat/completions" def call_o3_api( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> str: """ Makes an API call to OpenRouter O3 and returns the response. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Returns: str: O3's response text """ # Create the user message with the data user_message = format_user_message(data) # Use custom client if API key provided, otherwise use default client = ( OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key) if api_key else self.default_client ) completion = client.chat.completions.create( extra_headers={ "HTTP-Referer": "https://gitdiagram.com", # Optional. Site URL for rankings on openrouter.ai. "X-Title": "gitdiagram", # Optional. Site title for rankings on openrouter.ai. }, model="openai/o3-mini", # Can be configured as needed reasoning_effort=reasoning_effort, # Can be adjusted based on needs messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], max_completion_tokens=12000, # Adjust as needed temperature=0.2, ) if completion.choices[0].message.content is None: raise ValueError("No content returned from OpenRouter O3") return completion.choices[0].message.content async def call_o3_api_stream( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> AsyncGenerator[str, None]: """ Makes a streaming API call to OpenRouter O3 and yields the responses. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Yields: str: Chunks of O3's response text """ # Create the user message with the data user_message = format_user_message(data) headers = { "HTTP-Referer": "https://gitdiagram.com", "X-Title": "gitdiagram", "Authorization": f"Bearer {api_key or self.default_client.api_key}", "Content-Type": "application/json", } payload = { "model": "openai/o3-mini", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], "max_tokens": 12000, "temperature": 0.2, "stream": True, "reasoning_effort": reasoning_effort, } buffer = "" async with aiohttp.ClientSession() as session: async with session.post( self.base_url, headers=headers, json=payload ) as response: async for line in response.content: line = line.decode("utf-8").strip() if line.startswith("data: "): if line == "data: [DONE]": break try: data = json.loads(line[6:]) if ( content := data.get("choices", [{}])[0] .get("delta", {}) .get("content") ): yield content except json.JSONDecodeError: # Skip any non-JSON lines (like the OPENROUTER PROCESSING comments) continue def count_tokens(self, prompt: str) -> int: """ Counts the number of tokens in a prompt. Note: This is a rough estimate as OpenRouter may not provide direct token counting. Args: prompt (str): The prompt to count tokens for Returns: int: Estimated number of input tokens """ num_tokens = len(self.encoding.encode(prompt)) return num_tokens ``` ## /backend/app/services/o4_mini_openai_service.py ```py path="/backend/app/services/o4_mini_openai_service.py" from openai import OpenAI from dotenv import load_dotenv from app.utils.format_message import format_user_message import tiktoken import os import aiohttp import json from typing import AsyncGenerator, Literal load_dotenv() class OpenAIo4Service: def __init__(self): self.default_client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), ) self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models self.base_url = "https://api.openai.com/v1/chat/completions" def call_o4_api( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> str: """ Makes an API call to OpenAI o4-mini and returns the response. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Returns: str: o4-mini's response text """ # Create the user message with the data user_message = format_user_message(data) # Use custom client if API key provided, otherwise use default client = OpenAI(api_key=api_key) if api_key else self.default_client try: print( f"Making non-streaming API call to o4-mini with API key: {'custom key' if api_key else 'default key'}" ) completion = client.chat.completions.create( model="o4-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], max_completion_tokens=12000, # Adjust as needed temperature=0.2, reasoning_effort=reasoning_effort, ) print("API call completed successfully") if completion.choices[0].message.content is None: raise ValueError("No content returned from OpenAI o4-mini") return completion.choices[0].message.content except Exception as e: print(f"Error in OpenAI o4-mini API call: {str(e)}") raise async def call_o4_api_stream( self, system_prompt: str, data: dict, api_key: str | None = None, reasoning_effort: Literal["low", "medium", "high"] = "low", ) -> AsyncGenerator[str, None]: """ Makes a streaming API call to OpenAI o4-mini and yields the responses. Args: system_prompt (str): The instruction/system prompt data (dict): Dictionary of variables to format into the user message api_key (str | None): Optional custom API key Yields: str: Chunks of o4-mini's response text """ # Create the user message with the data user_message = format_user_message(data) headers = { "Content-Type": "application/json", "Authorization": f"Bearer {api_key or self.default_client.api_key}", } # payload = { # "model": "o3-mini", # "messages": [ # { # "role": "user", # "content": f""" # # {system_prompt} # # # {user_message} # # """, # }, # ], # "max_completion_tokens": 12000, # "stream": True, # } payload = { "model": "o4-mini", "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], "max_completion_tokens": 12000, "stream": True, "reasoning_effort": reasoning_effort, } try: async with aiohttp.ClientSession() as session: async with session.post( self.base_url, headers=headers, json=payload ) as response: if response.status != 200: error_text = await response.text() print(f"Error response: {error_text}") raise ValueError( f"OpenAI API returned status code {response.status}: {error_text}" ) line_count = 0 async for line in response.content: line = line.decode("utf-8").strip() if not line: continue line_count += 1 if line.startswith("data: "): if line == "data: [DONE]": break try: data = json.loads(line[6:]) content = ( data.get("choices", [{}])[0] .get("delta", {}) .get("content") ) if content: yield content except json.JSONDecodeError as e: print(f"JSON decode error: {e} for line: {line}") continue if line_count == 0: print("Warning: No lines received in stream response") except aiohttp.ClientError as e: print(f"Connection error: {str(e)}") raise ValueError(f"Failed to connect to OpenAI API: {str(e)}") except Exception as e: print(f"Unexpected error in streaming API call: {str(e)}") raise def count_tokens(self, prompt: str) -> int: """ Counts the number of tokens in a prompt. Args: prompt (str): The prompt to count tokens for Returns: int: Estimated number of input tokens """ num_tokens = len(self.encoding.encode(prompt)) return num_tokens ``` ## /backend/app/utils/format_message.py ```py path="/backend/app/utils/format_message.py" def format_user_message(data: dict[str, str]) -> str: """ Formats a dictionary of data into a structured user message with XML-style tags. Args: data (dict[str, str]): Dictionary of key-value pairs to format Returns: str: Formatted message with each key-value pair wrapped in appropriate tags """ parts = [] for key, value in data.items(): # Map keys to their XML-style tags if key == "file_tree": parts.append(f"\n{value}\n") elif key == "readme": parts.append(f"\n{value}\n") elif key == "explanation": parts.append(f"\n{value}\n") elif key == "component_mapping": parts.append(f"\n{value}\n") elif key == "instructions": parts.append(f"\n{value}\n") elif key == "diagram": parts.append(f"\n{value}\n") return "\n\n".join(parts) ``` ## /backend/deploy.sh ```sh path="/backend/deploy.sh" #!/bin/bash # Exit on any error set -e # Navigate to project directory cd ~/gitdiagram # Pull latest changes git pull origin main # Build and restart containers with production environment docker-compose down ENVIRONMENT=production docker-compose up --build -d # Remove unused images docker image prune -f # Show logs only if --logs flag is passed if [ "$1" == "--logs" ]; then docker-compose logs -f else echo "Deployment complete! Run 'docker-compose logs -f' to view logs" fi ``` ## /backend/entrypoint.sh ```sh path="/backend/entrypoint.sh" #!/bin/bash echo "Current ENVIRONMENT: $ENVIRONMENT" if [ "$ENVIRONMENT" = "development" ]; then echo "Starting in development mode with hot reload..." exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload elif [ "$ENVIRONMENT" = "production" ]; then echo "Starting in production mode with multiple workers..." exec uvicorn app.main:app \ --host 0.0.0.0 \ --port 8000 \ --timeout-keep-alive 300 \ --workers 2 \ --loop uvloop \ --http httptools else echo "ENVIRONMENT must be set to either 'development' or 'production'" exit 1 fi ``` ## /backend/nginx/api.conf ```conf path="/backend/nginx/api.conf" server { server_name api.gitdiagram.com; # Block requests with no valid Host header if ($host !~ ^(api.gitdiagram.com)$) { return 444; } # Strictly allow only GET, POST, and OPTIONS requests for the specified paths (defined in my fastapi app) location ~ ^/(generate(/cost|/stream)?|modify|)?$ { if ($request_method !~ ^(GET|POST|OPTIONS)$) { return 444; } proxy_pass http://127.0.0.1:8000; include proxy_params; proxy_redirect off; # Disable buffering for SSE proxy_buffering off; proxy_cache off; # Required headers for SSE proxy_set_header Connection ''; proxy_http_version 1.1; } # Return 444 for everything else (no response, just close connection) location / { return 444; # keep access log on } # Add timeout settings proxy_connect_timeout 300; proxy_send_timeout 300; proxy_read_timeout 300; send_timeout 300; listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/api.gitdiagram.com/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/api.gitdiagram.com/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = api.gitdiagram.com) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name api.gitdiagram.com; return 404; # managed by Certbot } ``` ## /backend/nginx/setup_nginx.sh ```sh path="/backend/nginx/setup_nginx.sh" #!/bin/bash # Exit on any error set -e # Check if running as root if [ "$EUID" -ne 0 ]; then echo "Please run as root or with sudo" exit 1 fi # Copy Nginx configuration echo "Copying Nginx configuration..." cp "$(dirname "$0")/api.conf" /etc/nginx/sites-available/api ln -sf /etc/nginx/sites-available/api /etc/nginx/sites-enabled/ # Test Nginx configuration echo "Testing Nginx configuration..." nginx -t # Reload Nginx echo "Reloading Nginx..." systemctl reload nginx echo "Nginx configuration updated successfully!" ``` ## /backend/requirements.txt aiohappyeyeballs==2.4.6 aiohttp==3.11.12 aiosignal==1.3.2 annotated-types==0.7.0 anthropic==0.42.0 anyio==4.7.0 api-analytics==1.2.5 attrs==25.1.0 certifi==2024.12.14 cffi==1.17.1 charset-normalizer==3.4.0 click==8.1.7 cryptography==44.0.0 Deprecated==1.2.15 distro==1.9.0 dnspython==2.7.0 email_validator==2.2.0 fastapi==0.115.6 fastapi-cli==0.0.6 frozenlist==1.5.0 h11==0.14.0 httpcore==1.0.7 httptools==0.6.4 httpx==0.28.1 idna==3.10 Jinja2==3.1.4 jiter==0.8.2 limits==3.14.1 markdown-it-py==3.0.0 MarkupSafe==3.0.2 mdurl==0.1.2 multidict==6.1.0 openai==1.61.1 packaging==24.2 propcache==0.2.1 pycparser==2.22 pydantic==2.10.3 pydantic_core==2.27.1 Pygments==2.18.0 PyJWT==2.10.1 python-dotenv==1.0.1 python-multipart==0.0.19 PyYAML==6.0.2 regex==2024.11.6 requests==2.32.3 rich==13.9.4 rich-toolkit==0.12.0 shellingham==1.5.4 slowapi==0.1.9 sniffio==1.3.1 starlette==0.41.3 tiktoken==0.8.0 tqdm==4.67.1 typer==0.15.1 typing_extensions==4.12.2 urllib3==2.2.3 uvicorn==0.34.0 uvloop==0.21.0 watchfiles==1.0.3 websockets==14.1 wrapt==1.17.0 yarl==1.18.3 ## /components.json ```json path="/components.json" { "$schema": "https://ui.shadcn.com/schema.json", "style": "default", "rsc": true, "tsx": true, "tailwind": { "config": "tailwind.config.ts", "css": "src/styles/globals.css", "baseColor": "neutral", "cssVariables": true, "prefix": "" }, "aliases": { "components": "~/components", "utils": "~/lib/utils", "ui": "~/components/ui", "lib": "~/lib", "hooks": "~/hooks" }, "iconLibrary": "lucide" } ``` ## /docker-compose.yml ```yml path="/docker-compose.yml" services: api: build: context: ./backend dockerfile: Dockerfile ports: - "8000:8000" volumes: - ./backend:/app env_file: - .env environment: - ENVIRONMENT=${ENVIRONMENT:-development} # Default to development if not set restart: unless-stopped ``` ## /docs/readme_img.png Binary file available at https://raw.githubusercontent.com/ahmedkhaleel2004/gitdiagram/refs/heads/main/docs/readme_img.png ## /drizzle.config.ts ```ts path="/drizzle.config.ts" import { type Config } from "drizzle-kit"; import { env } from "~/env"; export default { schema: "./src/server/db/schema.ts", dialect: "postgresql", dbCredentials: { url: env.POSTGRES_URL, }, tablesFilter: ["gitdiagram_*"], } satisfies Config; ``` ## /next.config.js ```js path="/next.config.js" /** * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful * for Docker builds. */ import "./src/env.js"; /** @type {import("next").NextConfig} */ const config = { reactStrictMode: false, async rewrites() { return [ { source: "/ingest/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*", }, { source: "/ingest/:path*", destination: "https://us.i.posthog.com/:path*", }, { source: "/ingest/decide", destination: "https://us.i.posthog.com/decide", }, ]; }, // This is required to support PostHog trailing slash API requests skipTrailingSlashRedirect: true, }; export default config; ``` ## /package.json ```json path="/package.json" { "name": "gitdiagram", "version": "0.1.0", "private": true, "type": "module", "scripts": { "build": "next build", "check": "next lint && tsc --noEmit", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "dev": "next dev --turbo", "lint": "next lint", "lint:fix": "next lint --fix", "preview": "next build && next start", "start": "next start", "typecheck": "tsc --noEmit", "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache" }, "dependencies": { "@neondatabase/serverless": "^0.10.4", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-progress": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.6", "@t3-oss/env-nextjs": "^0.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dotenv": "^16.4.7", "drizzle-orm": "^0.33.0", "geist": "^1.3.0", "ldrs": "^1.0.2", "lucide-react": "^0.468.0", "mermaid": "^11.4.1", "next": "^15.0.1", "next-themes": "^0.4.6", "postgres": "^3.4.4", "posthog-js": "^1.203.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.4.0", "sonner": "^2.0.3", "svg-pan-zoom": "^3.6.2", "tailwind-merge": "^2.5.5", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.3" }, "devDependencies": { "@types/eslint": "^8.56.10", "@types/node": "^20.14.10", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/svg-pan-zoom": "^3.4.0", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "drizzle-kit": "^0.24.0", "eslint": "^8.57.0", "eslint-config-next": "^15.0.1", "eslint-plugin-drizzle": "^0.2.3", "postcss": "^8.4.39", "prettier": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.5", "tailwind-scrollbar": "^4.0.0", "tailwindcss": "^3.4.3", "typescript": "^5.5.3" }, "ct3aMetadata": { "initVersion": "7.38.1" }, "packageManager": "pnpm@9.13.0" } ``` The content has been capped at 50000 tokens, and files over NaN bytes have been omitted. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.