# Project export: Gitship

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

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Turn any GitHub repository into a production-ready Docker container instantly with AI-powered Dockerfile generation.
- Devpost: https://devpost.com/software/gitship-ikg2wq
- GitHub: https://github.com/hasnainaliasghar/Gitship
- Demo: https://gitship.up.railway.app/
- Video: https://www.youtube.com/embed/3bOnsEgbSI0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — Hasnain Ali Asghar (5 commits), Mr. Ahtasham Ul Haq (2 commits)

## Devpost submission (written by the team)

### Inspiration

Developers often struggle with writing optimized, production-ready Dockerfiles. Figuring out the perfect base image, dependency management commands, and security best practices can be tedious and prone to error. For the OpenAI Build Week, we wanted to completely eliminate this friction and create a tool that does the heavy lifting for you—generating perfect Dockerfiles automatically from nothing but a GitHub URL.

### What it does

Gitship is an AI-powered web application that takes any public GitHub repository link, deeply analyzes the entire codebase structure, and generates a custom, highly optimized Dockerfile. Not only does it stream the generation process live to the user, but it also automatically detects the tech stack (Python, Node.js, Go, etc.) and even suggests a docker-compose.yml file for multi-container setups. You can even pass it custom instructions like "Use Alpine Linux" or "Include PostgreSQL".

### How we built it

We built the backend using Python and FastAPI, heavily utilizing WebSockets to stream real-time responses to the UI. Here is the flow: When a user submits a URL, the backend temporarily clones the repository locally. It uses gitingest to create a dense, token-optimized text summary of the codebase and file tree. This context is passed to Groq's API (running Llama models) with a highly specialized prompt. The generated infrastructure code is streamed live back to the frontend. The frontend is built with vanilla HTML/CSS and TailwindCSS via Jinja2 templates. We integrated the Monaco Editor (the engine behind VS Code) to display the generated code with syntax highlighting. Throughout our development, we also used GPT-5.6 and Codex for architectural planning, debugging, and backend scaffolding.

### Challenges we ran into

Handling large repositories was tricky. We had to ensure we extracted the relevant dependency files (package.json, requirements.txt, etc.) without blowing past the LLM's context window. Additionally, maintaining a stable WebSocket connection to stream the AI's response character-by-character required careful asynchronous state management in FastAPI to ensure the UI updated perfectly without dropping chunks.

### Accomplishments we're proud of

We are incredibly proud of the absolute speed of Gitship. By utilizing Groq's API, the Dockerfiles are generated almost instantaneously. We also managed to build a very clean, responsive, and intuitive UI that feels like a premium developer tool right out of the box.

### What we learned

We significantly deepened our understanding of asynchronous Python and WebSocket architecture. We also learned advanced prompt engineering techniques specifically tailored for infrastructure-as-code (IaC) generation, ensuring the AI outputs valid Docker syntax rather than conversational text.

### What's next

We plan to build a native GitHub App integration so Gitship can automatically open a Pull Request with the generated Dockerfile directly in the user's repository. We also plan to integrate one-click deployments to cloud providers right from our dashboard.

## README (from the GitHub repository)

# **Gitship** 🐳

![Gitship app screenshot](docs/Gitship-picture.png)

**Turn any GitHub repository into a production-ready Docker container with AI-powered Dockerfile generation.**

[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/)
[![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/downloads/)
[![FastAPI](https://img.shields.io/badge/FastAPI-0.68+-00a393.svg)](https://fastapi.tiangolo.com/)

Gitship is an AI web application that automatically generates production-ready Dockerfiles by analyzing GitHub repositories.<br>
Paste in a GitHub URL and get a tailored **Dockerfile** back, complete with intelligent base image selection, dependency management, and Docker best practices.

> 🏆 Built for **[OpenAI Build Week](https://openai.devpost.com/)** : Developer Tools track.

## 🤖 How Codex & GPT-5.6 Were Used

- **Codex:**  Codex used to write complete code, such as the FastAPI backend scaffolding, the WebSocket streaming logic, or the Dockerfile-generation prompt engineering <br><br>
- **GPT-5.6:**  GPT-5.6 Model was used for planning the architecture, writing prompts, debugging, reviewing code.

## ✨ Features

- **🤖 AI-Powered Analysis:** Uses Groq's Llama models to analyze repository structure and generate intelligent Dockerfiles<br><br>
- **⚡ Real-time Streaming:** Watch the AI generate your Dockerfile live via WebSocket streaming<br><br>
- **🎯 Smart Detection:** Automatically detects technology stacks (Python, Node.js, Java, Go, etc.)<br><br>
- **🔧 Production-Ready Output:** Generates Dockerfiles following best practices: proper security, multi-stage builds, and optimization<br><br>
- **📋 Custom Instructions:** Add your own requirements for specialized environments<br><br>
- **📄 Docker Compose Support:** Automatically suggests a `docker-compose.yml` for multi-service applications<br><br>
- **🎨 Modern UI:** Clean, responsive interface with Monaco editor for syntax highlighting<br><br>
- **📱 Mobile Friendly:** Works seamlessly on desktop and mobile

## 🚀 Quick Start

### Prerequisites

- Python 3.9 or higher
- Git
- A Groq API key ([console.groq.com](https://console.groq.com/))

### Installation

1. **Clone the repository:**
   ```bash
   git clone https://github.com/hasnainaliasghar/Gitship.git
   cd Gitship
   ```

2. **Install dependencies:**
   ```bash
   pip install -r requirements.txt
   ```

3. **Set up environment variables:**
   ```bash
   # Create .env file
   echo "GROQ_API_KEY=your_groq_api_key_here" > .env
   echo "GROQ_MODEL=llama-3.1-70b-versatile" >> .env
   ```

4. **Run the application:**
   ```bash
   python app.py
   ```

5. **Open your browser:**
   Navigate to `http://localhost:8000`

## 🛠️ How It Works

1. **Repository Cloning:** Gitship clones the target GitHub repository locally using Git
2. **Code Analysis:** Uses [gitingest](https://github.com/cyclotruc/gitingest) to analyze repository structure and extract relevant context
3. **AI Generation:** Sends the analysis to Groq's API with specialized prompts for Dockerfile generation
4. **Smart Optimization:** The AI considers:
   - Technology stack detection
   - Dependency management
   - Security best practices
   - Multi-stage builds where beneficial
   - Port configuration
   - Environment variables
   - Health checks

## 📁 Project Structure

```
Gitship/
├── app.py                 # Main FastAPI application
├── requirements.txt       # Python dependencies
├── .env                   # Environment variables (create this)
├── static/                # Static assets (icons, CSS)
├── templates/
│   └── index.jinja        # Main HTML template
└── tools/                 # Core functionality modules
    ├── __init__.py
    ├── create_container.py  # AI Dockerfile generation
    ├── git_operations.py    # GitHub repository cloning
    └── gitingest.py          # Repository analysis
```

## 🔧 Configuration

### Environment Variables

| Variable      | Description                          | Required |
|----------------|---------------------------------------|----------|
| `GROQ_API_KEY` | Your Groq API key                    | Yes      |
| `GROQ_MODEL`   | Groq model to use (e.g. `llama-3.1-70b-versatile`) | No |
| `PORT`         | Server port (default: `8000`)        | No       |
| `HOST`         | Server host (default: `0.0.0.0`)     | No       |

### Advanced Usage

You can also use the tools programmatically:

```python
from tools import clone_repo_tool, gitingest_tool, create_container_tool
import asyncio

async def generate_dockerfile(github_url):
    # Clone repository
    clone_result = await clone_repo_tool(github_url)

    # Analyze with gitingest
    analysis = await gitingest_tool(clone_result['local_path'])

    # Generate Dockerfile
    dockerfile = await create_container_tool(
        gitingest_summary=analysis['summary'],
        gitingest_tree=analysis['tree'],
        gitingest_content=analysis['content']
    )

    return dockerfile

# Usage
result = asyncio.run(generate_dockerfile("https://github.com/user/repo"))
print(result['dockerfile'])
```

## 🎨 Customization

Use the "Additional instructions" field to customize generation, for example:

- `"Use Alpine Linux for smaller image size"`
- `"Include Redis and PostgreSQL"`
- `"Optimize for production deployment"`
- `"Add development tools for debugging"`

## 📝 License

This project is licensed under the MIT License , see the [LICENSE](LICENSE) file for details.

## 🔗 Links

- **GitHub Repository:** [github.com/hasnainaliasghar/Gitship](https://github.com/hasnainaliasghar/Gitship)
- **Issues:** [Report bugs or request features](https://github.com/hasnainaliasghar/Gitship/issues)

---

*Turn any repository into a container in seconds.*


## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 36 KB.
- FastAPI (technology) — detected in the code
- Python (language) — detected in the code
- Docker (technology) — claimed on Devpost, not found in the code
- HTML (language) — claimed on Devpost, not found in the code
- Tailwind CSS (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (14 of 14)

```
.env.example
.gitignore
app.py
colors.txt
fetch_models.py
LICENSE
README.md
rename_gitship.py
requirements.txt
templates/index.jinja
tools/__init__.py
tools/create_container.py
tools/git_operations.py
tools/gitingest.py
```

### Dependencies

- requirements.txt: api-analytics[fastapi], fastapi, gitingest, jinja2, openai-agents, python-dotenv, python-multipart, uvicorn[standard]

### Recent commits (newest first)

- update readme
- add picture
- Update README with AI usage details
- Revise README for AI model updates and features
- Update UI header link and example repository buttons
- Remove image from README
- Update branding to Gitship and configure Groq API

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

### requirements.txt

```
openai-agents
python-dotenv 
gitingest
fastapi
uvicorn[standard]
jinja2
python-multipart
api-analytics[fastapi]
```

### app.py

```python
"""Minimal FastAPI app for GitHub URL to Dockerfile generator."""

import asyncio
import json
import os
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Form, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pathlib import Path
from tools import gitingest_tool, clone_repo_tool, create_container_tool
from api_analytics.fastapi import Analytics

# Load environment variables
load_dotenv()

# Initialize FastAPI app
app = FastAPI(title="GitHub to Dockerfile Generator")

# Add API Analytics middleware
app.add_middleware(Analytics, api_key=os.getenv("FASTAPI_ANALYTICS_KEY"))

# Setup templates
templates = Jinja2Templates(directory="templates")

# Mount static files (we'll create this directory)
static_dir = Path("static")
static_dir.mkdir(exist_ok=True)
app.mount("/static", StaticFiles(directory=static_dir), name="static")

# Store for session data
sessions = {}


@app.get("/favicon.ico")
async def favicon():
    """Serve the main favicon."""
    return FileResponse("static/icons8-docker-doodle-32.png")


@app.get("/favicon-16x16.png")
async def favicon_16():
    """Serve 16x16 favicon."""
    return FileResponse("static/icons8-docker-doodle-16.png")


@app.get("/favicon-32x32.png") 
async def favicon_32():
    """Serve 32x32 favicon."""
    return FileResponse("static/icons8-docker-doodle-32.png")


@app.get("/apple-touch-icon.png")
async def apple_touch_icon():
    """Serve Apple touch icon (120x120 is close to the 180x180 standard)."""
    return FileResponse("static/icons8-docker-doodle-120.png")


@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
    """Home page with the input form."""
    return templates.TemplateResponse(request, "index.jinja", {
        "request": request,
        "repo_url": "",
        "loading": False,
        "streaming": False,
        "result": None,
        "error": None
    })

@app.post("/", response_class=HTMLResponse) 
async def generate_dockerfile(
    request: Request, 
    repo_url: str = Form(...),
    additional_instructions_hidden: str = Form("")
):
    """Redirect to streaming page for Dockerfile generation."""
    # Store the repo URL and additional instructions in a session (simple in-memory for demo)
    session_id = str(hash(repo_url + str(asyncio.get_event_loop().time())))
    sessions[session_id] = {
        "repo_url": repo_url,
        "additional_instructions": additional_instructions_hidden.strip() if additional_instructions_hidden else "",
        "status": "pending"
    }
    
    # Redirect to streaming page
    return templates.TemplateResponse(request, "index.jinja", {
        "request": request,
        "repo_url": repo_url,
        "loading": False,
        "streaming": True,
        "session_id": session_id,
        "result": None,
        "error": None
    })


@app.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
    """WebSocket endpoint for streaming Dockerfile generation."""
    await websocket.accept()
    clone_result = None
    
    try:
        if session_id not in sessions:
            await websocket.send_text(json.dumps({
                "type": "error",
                "content": "Invalid session ID"
            }))
            return
        
        repo_url = sessions[session_id]["repo_url"]
        additional_instructions = sessions[session_id].get("additional_instructions", "")
        
        # Step 1: Clone repository
        await websocket.send_text(json.dumps({
            "type": "status", 
            "content": f"🔄 Cloning repository: {repo_url}"
        }))
        
        clone_result = await clone_repo_tool(repo_url)
        
        if not clone_result["success"]:
            await websocket.send_text(json.dumps({
                "type": "error",
                "content": f"Failed to clone repository: {clone_result['error']}"
            }))
            return
        
        # Step 2: Analyze with gitingest
        await websocket.send_text(json.dumps({
            "type": "status",
            "content": "📊 Analyzing repository structure..."
        }))
        
        ingest_result = await gitingest_tool(clone_result['local_path'])
        
        if not ingest_result["success"]:
            await websocket.send_text(json.dumps({
                "type": "error",
                "content": f"Failed to analyze repository: {ingest_result['error']}"
            }))
            return
        
        # Step 3: Generate Dockerfile with streaming
        await websocket.send_text(json.dumps({
            "type": "status",
            "content": "🐳 Generating Dockerfile with AI..."
        }))
        
        container_result = await create_container_tool(
            gitingest_summary=ingest_result['summary'],
            gitingest_tree=ingest_result['tree'], 
            gitingest_content=ingest_result['content'],
            project_name=clone_result['repo_name'],
            websocket=websocket,  # Pass WebSocket for streaming
            additional_instructions=additional_instructions
        )
        
        if not container_result["success"]:
            await websocket.send_text(json.dumps({
                "type": "error",
                "content": f"Failed to generate Dockerfile: {container_result['error']}"
            }))
            return
        
        # Send final result
        final_result = {
            "project_name": container_result['project_name'],
            "technology_stack": container_result['technology_stack'],
            "dockerfile": container_result['dockerfile'],
            "docker_compose": container_result.get('docker_compose_suggestion', ''),
            "reasoning": container_result.get('base_image_reasoning', ''),
            "additional_notes": container_result.get('additional_notes', ''),
            "repo
[truncated — 1760 more characters]
```

### fetch_models.py

```python
import os
import requests
from dotenv import load_dotenv

load_dotenv()
api_key = os.getenv("GROQ_API_KEY")

if not api_key:
    print("No API key found in .env")
    exit(1)

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}

response = requests.get("https://api.groq.com/openai/v1/models", headers=headers)
if response.status_code == 200:
    models = response.json()
    print("Available models:")
    for model in models.get("data", []):
        print(f"- {model.get('id')}")
else:
    print(f"Error fetching models: {response.status_code} - {response.text}")

```

### rename_gitship.py

```python
import os

def replace_in_file(filepath):
    try:
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()
        
        new_content = content.replace('Gitship', 'Gitship')
        new_content = new_content.replace('gitship', 'gitship')
        
        if new_content != content:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(new_content)
            print(f"Updated {filepath}")
    except Exception as e:
        print(f"Error reading {filepath}: {e}")

for root, dirs, files in os.walk('.'):
    if '.git' in root or '__pycache__' in root or 'repos' in root or 'static' in root or 'venv' in root:
        continue
    for file in files:
        if file.endswith(('.py', '.jinja', '.md', '.txt')):
            replace_in_file(os.path.join(root, file))

```

### tools/__init__.py

```python
"""
Tools package for the OpenAI Agents SDK.

This package contains various tools that can be used by AI agents.
"""

from .gitingest import gitingest_tool, gitingest_function
from .git_operations import clone_repo_tool, git_operations_function
from .create_container import create_container_tool, create_container_function

__all__ = [
    'gitingest_tool',
    'gitingest_function', 
    'clone_repo_tool',
    'git_operations_function',
    'create_container_tool',
    'create_container_function'
] 
```

### tools/gitingest.py

```python
import asyncio
import os
from gitingest import ingest_async
from typing import Dict, Any


async def gitingest_tool(local_repo_path: str) -> Dict[str, Any]:
    """
    Analyze a local GitHub repository using gitingest and return structured results.
    
    Args:
        local_repo_path (str): The local path to the cloned repository to analyze
        
    Returns:
        Dict[str, Any]: Dictionary containing summary, tree, and content
    """
    try:
        # Check if the local path exists
        if not os.path.exists(local_repo_path):
            raise FileNotFoundError(f"Local repository path does not exist: {local_repo_path}")
        
        if not os.path.isdir(local_repo_path):
            raise ValueError(f"Path is not a directory: {local_repo_path}")
        
        # Use gitingest to analyze the local repository
        summary, tree, content = await ingest_async(
            source=local_repo_path,
            max_file_size=5 * 1024 * 1024,  # 5MB limit per file
        )
        
        return {
            "success": True,
            "summary": summary,
            "tree": tree,
            "content": content,
            "local_path": local_repo_path
        }
        
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "local_path": local_repo_path
        }


def run_gitingest(local_repo_path: str) -> Dict[str, Any]:
    """
    Synchronous wrapper for the gitingest tool.
    
    Args:
        local_repo_path (str): The local path to the cloned repository to analyze
        
    Returns:
        Dict[str, Any]: Dictionary containing analysis results
    """
    return asyncio.run(gitingest_tool(local_repo_path))


# Tool definition for OpenAI Agents SDK
gitingest_function = {
    "type": "function",
    "function": {
        "name": "analyze_local_repo",
        "description": "Analyze a locally cloned repository and extract its structure and content for analysis",
        "parameters": {
            "type": "object",
            "properties": {
                "local_repo_path": {
                    "type": "string",
                    "description": "The local file system path to the cloned repository (e.g., ./repos/my-repo)"
                }
            },
            "required": ["local_repo_path"]
        }
    }
} 
```

### tools/git_operations.py

```python
import asyncio
import os
import shutil
import tempfile
import subprocess
from urllib.parse import urlparse
from typing import Dict, Any


async def clone_repo_tool(github_url: str, target_dir: str = "repos") -> Dict[str, Any]:
    """
    Clone a GitHub repository locally for future usage.
    
    Args:
        github_url (str): The GitHub repository URL to clone
        target_dir (str): Directory where to clone the repository (default: "repos")
        
    Returns:
        Dict[str, Any]: Dictionary containing clone results and local path
    """
    try:
        # Parse the GitHub URL to extract repo name
        parsed_url = urlparse(github_url)
        if not parsed_url.netloc == "github.com":
            return {
                "success": False,
                "error": "Only GitHub URLs are supported",
                "url": github_url
            }
        
        # Extract repo name from URL path
        path_parts = parsed_url.path.strip('/').split('/')
        if len(path_parts) < 2:
            return {
                "success": False,
                "error": "Invalid GitHub URL format",
                "url": github_url
            }
        
        owner, repo_name = path_parts[0], path_parts[1]
        if repo_name.endswith('.git'):
            repo_name = repo_name[:-4]
        
        # Create target directory if it doesn't exist
        os.makedirs(target_dir, exist_ok=True)
        
        # Use a separate workspace for every request. Shared directories make
        # simultaneous generations delete files that another request is using.
        workspace_dir = tempfile.mkdtemp(prefix=f"{owner}_{repo_name}_", dir=target_dir)
        local_path = os.path.join(workspace_dir, "repository")
        
        kwargs = {
            'stdout': asyncio.subprocess.PIPE,
            'stderr': asyncio.subprocess.PIPE
        }
        if os.name == 'nt':
            kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
            
        process = await asyncio.create_subprocess_exec(
            "git", "clone", "--depth", "1", github_url, local_path,
            **kwargs
        )
        
        stdout, stderr = await process.communicate()
        
        if process.returncode == 0:
            # Get repository info
            repo_size = get_directory_size(local_path)
            file_count = count_files(local_path)
            
            return {
                "success": True,
                "local_path": local_path,
                "repo_name": f"{owner}/{repo_name}",
                "repo_size_mb": round(repo_size / (1024 * 1024), 2),
                "file_count": file_count,
                "workspace_dir": workspace_dir,
                "url": github_url,
                "message": f"Successfully cloned {owner}/{repo_name} to {local_path}"
            }
        else:
            shutil.rmtree(workspace_dir, ignore_errors=True)
            return {
                "success": False,
                "error": stderr.decode('utf-8') if stderr else "Clone failed",
                "url": github_url
            }
            
    except Exception as e:
        return {
            "success": False,
            "error": str(e),
            "url": github_url
        }


def get_directory_size(path: str) -> int:
    """Get total size of directory in bytes."""
    total_size = 0
    try:
        for dirpath, dirnames, filenames in os.walk(path):
            for filename in filenames:
                file_path = os.path.join(dirpath, filename)
                if os.path.exists(file_path):
                    total_size += os.path.getsize(file_path)
    except Exception:
        pass
    return total_size


def count_files(path: str) -> int:
    """Count total number of files in directory."""
    file_count = 0
    try:
        for dirpath, dirnames, filenames in os.walk(path):
            file_count += len(filenames)
    except Exception:
        pass
    return file_count


def run_clone_repo(github_url: str, target_dir: str = "repos") -> Dict[str, Any]:
    """
    Synchronous wrapper for the clone repo tool.
    
    Args:
        github_url (str): The GitHub repository URL to clone
        target_dir (str): Directory where to clone the repository
        
    Returns:
        Dict[str, Any]: Dictionary containing clone results
    """
    return asyncio.run(clone_repo_tool(github_url, target_dir))


# Tool definition for OpenAI Agents SDK
git_operations_function = {
    "type": "function",
    "function": {
        "name": "clone_github_repo",
        "description": "Clone a GitHub repository locally for analysis and future usage",
        "parameters": {
            "type": "object",
            "properties": {
                "github_url": {
                    "type": "string",
                    "description": "The GitHub repository URL to clone (e.g., https://github.com/user/repo)"
                },
                "target_dir": {
                    "type": "string",
                    "description": "Directory where to clone the repository (default: 'repos')",
                    "default": "repos"
                }
            },
            "required": ["github_url"]
        }
    }
} 

```

### tools/create_container.py

```python
import asyncio
import os
import json
from typing import Dict, Any, Optional, Union
from openai import AsyncOpenAI
from dotenv import load_dotenv
import re

# Load environment variables
load_dotenv()


async def create_container_tool(
    gitingest_summary: str,
    gitingest_tree: str,
    gitingest_content: str,
    project_name: Optional[str] = None,
    additional_instructions: Optional[str] = None,
    max_context_chars: int = 5000,  # Limit to stay well within context window of Groq free tier (6000 tokens limit)
    websocket: Optional[Any] = None  # WebSocket connection for streaming
) -> Dict[str, Any]:
    """
    Generate a Dockerfile using the Groq API based on gitingest context.
    
    Args:
        gitingest_summary (str): Summary from gitingest analysis
        gitingest_tree (str): Directory tree from gitingest
        gitingest_content (str): Full content from gitingest
        project_name (str, optional): Name of the project for the container
        additional_instructions (str, optional): Additional instructions for the Dockerfile generation
        max_context_chars (int): Maximum characters to send in context
        websocket (Any, optional): WebSocket connection for streaming
        
    Returns:
        Dict[str, Any]: Dictionary containing the generated Dockerfile and metadata
    """
    try:
        # Groq exposes an OpenAI-compatible chat completions endpoint.
        api_key = os.getenv("GROQ_API_KEY")
        if not api_key:
            raise ValueError("GROQ_API_KEY not found in environment variables")
        
        client = AsyncOpenAI(
            api_key=api_key,
            base_url="https://api.groq.com/openai/v1",
        )
        model = os.getenv("GROQ_MODEL", "llama-3.1-8b-instant")
        
        # Truncate content if it exceeds max context to avoid hitting limits
        truncated_content = gitingest_content
        if len(gitingest_content) > max_context_chars:
            truncated_content = gitingest_content[:max_context_chars] + "\n\n... [Content truncated due to length] ..."
        
        # Create the prompt for Dockerfile generation
        additional_instructions_section = ""
        if additional_instructions and additional_instructions.strip():
            additional_instructions_section = f"\n\nADDITIONAL INSTRUCTIONS:\n{additional_instructions.strip()}"
        
        prompt = f"""Based on the following repository analysis, generate a comprehensive and production-ready Dockerfile.

PROJECT SUMMARY:
{gitingest_summary}

DIRECTORY STRUCTURE:
{gitingest_tree}

SOURCE CODE CONTEXT:
{truncated_content}{additional_instructions_section}

Please generate a Dockerfile that:
1. Uses appropriate base images for the detected technology stack
2. Includes proper dependency management
3. Sets up the correct working directory structure
4. Exposes necessary ports
5. Includes health checks where appropriate
6. Follows Docker best practices (multi-stage builds if beneficial, minimal layers, etc.)
7. Handles environment variables and configuration
8. Sets up proper user permissions for security

If you detect multiple services or a complex architecture, provide a main Dockerfile and suggest docker-compose.yml structure.

IMPORTANT: Respond ONLY with a valid JSON object. Do not include any markdown formatting, explanations, or code blocks. The response must be parseable JSON.

Required JSON format:
{{
  "dockerfile": "FROM python:3.9-slim\\nWORKDIR /app\\nCOPY . .\\nRUN pip install -r requirements.txt\\nEXPOSE 8000\\nCMD [\\"python\\", \\"app.py\\"]",
  "base_image_reasoning": "Explanation of why you chose the base image",
  "technology_stack": "Detected technologies and frameworks",
  "port_recommendations": ["8000", "80"],
  "additional_notes": "Any important setup or deployment notes",
  "docker_compose_suggestion": "Optional docker-compose.yml content if multiple services detected"
}}"""

        # Make API call to generate Dockerfile with streaming
        websocket_active = await _emit_ws_message(websocket, "status", "🐳 Generating Dockerfile...")
        if websocket_active:
            print("Generating Dockerfile (streaming response)")
        
        response = await client.chat.completions.create(
            model=model,
            messages=[
                {
                    "role": "system",
                    "content": "You are an expert DevOps engineer specializing in containerization. Generate production-ready Dockerfiles based on repository analysis. ALWAYS respond with valid JSON only - no markdown, no explanations, no code blocks. Just pure JSON that can be parsed directly."
                },
                {
                    "role": "user",
                    "content": prompt
                }
            ],
            temperature=0.3,  # Lower temperature for more consistent output
            max_completion_tokens=2000,  # Sufficient for Dockerfile generation
            stream=True       # Enable streaming
        )
        
        # Collect the streaming response and print in real-time
        dockerfile_response = ""
        if websocket_active:
            websocket_active = await _emit_ws_message(websocket, "stream_start", "Starting generation...")
        print("Streaming model response")
        
        async for chunk in response:
            if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
                content = chunk.choices[0].delta.content
                dockerfile_response += content
                # Only emit chunks if WebSocket is still active
                if websocket_active:
                    websocket_active = await _emit_ws_message(websocket, "chunk", content)
        
        print("Generation complete")
        if websocket_active:
            await _emit_ws_message(websocket, "status", "✅ Generation complete!")
        
        # Try to parse as JSON, fallback to plain text if needed
        try:
            # First try direct J
[truncated — 7587 more characters]
```