Skip to content

Commit ae7d8a6

Browse files
fix #33: move to o3-mini, add cursor rules, fix window error
1 parent 7ba2a66 commit ae7d8a6

File tree

4 files changed

+294
-39
lines changed

4 files changed

+294
-39
lines changed

.cursor/rules/summary.mdc

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
description: summary of project
3+
globs:
4+
alwaysApply: true
5+
---
6+
# GitDiagram Project Summary
7+
8+
## Project Overview
9+
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.
10+
11+
## Key Features
12+
- Instant conversion of GitHub repositories into system design diagrams
13+
- Interactive components that link to source files and directories
14+
- Support for both public and private repositories (with GitHub token)
15+
- Customizable diagrams through user instructions
16+
- URL shortcut: replace `hub` with `diagram` in any GitHub URL to access its diagram
17+
18+
## Tech Stack
19+
- **Frontend**: Next.js 15, TypeScript, Tailwind CSS, ShadCN UI components
20+
- **Backend**: FastAPI (Python), Server Actions
21+
- **Database**: PostgreSQL with Drizzle ORM, Neon Database for serverless PostgreSQL
22+
- **AI**: Claude 3.5 Sonnet (previously) / OpenAI o3-mini (currently) for diagram generation
23+
- **Deployment**: Vercel (Frontend), EC2 (Backend)
24+
- **CI/CD**: GitHub Actions
25+
- **Analytics**: PostHog, Api-Analytics
26+
27+
## Architecture
28+
The project follows a modern full-stack architecture:
29+
30+
1. **Frontend (Next.js)**:
31+
- Organized using the App Router pattern
32+
- Uses server components and server actions
33+
- Implements Mermaid.js for rendering diagrams
34+
- Provides UI for repository input and diagram customization
35+
36+
2. **Backend (FastAPI)**:
37+
- Handles repository data extraction
38+
- Implements complex prompt engineering through a pipeline:
39+
- First prompt analyzes the repository and creates an explanation
40+
- Second prompt maps relevant directories and files to diagram components
41+
- Third prompt generates the final Mermaid.js code
42+
- Manages API rate limiting and authentication
43+
44+
3. **Database (PostgreSQL)**:
45+
- Stores user data, repository information, and generated diagrams
46+
- Uses Drizzle ORM for type-safe database operations
47+
48+
4. **AI Integration**:
49+
- Uses LLMs to analyze repository structure
50+
- Generates detailed diagrams based on file trees and README content
51+
- Implements sophisticated prompt engineering to extract accurate information
52+
53+
## Project Structure
54+
- `/src`: Frontend source code (Next.js) and server actions for db calls with drizzle
55+
- `/backend`: Python FastAPI backend
56+
- `/public`: Static assets
57+
- `/docs`: Documentation and images
58+
59+
## Development Setup
60+
The project supports both local development and self-hosting:
61+
- Dependencies managed with pnpm
62+
- Docker Compose for containerization
63+
- Environment configuration via .env files
64+
- Database initialization scripts
65+
66+
## Future Development
67+
- Implementation of font-awesome icons in diagrams
68+
- Embedded feature for progressive diagram updates as commits are made
69+
- Expanded API access for third-party integration

backend/app/routers/generate.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from fastapi.responses import StreamingResponse
33
from dotenv import load_dotenv
44
from app.services.github_service import GitHubService
5-
from app.services.o1_mini_openai_service import OpenAIO1Service
5+
from app.services.o3_mini_openai_service import OpenAIo3Service
66
from app.prompts import (
77
SYSTEM_FIRST_PROMPT,
88
SYSTEM_SECOND_PROMPT,
@@ -25,7 +25,7 @@
2525

2626
# Initialize services
2727
# claude_service = ClaudeService()
28-
o1_service = OpenAIO1Service()
28+
o3_service = OpenAIo3Service()
2929

3030

3131
# cache github data to avoid double API calls from cost and generate
@@ -65,8 +65,8 @@ async def get_generation_cost(request: Request, body: ApiRequest):
6565
# file_tree_tokens = claude_service.count_tokens(file_tree)
6666
# readme_tokens = claude_service.count_tokens(readme)
6767

68-
file_tree_tokens = o1_service.count_tokens(file_tree)
69-
readme_tokens = o1_service.count_tokens(readme)
68+
file_tree_tokens = o3_service.count_tokens(file_tree)
69+
readme_tokens = o3_service.count_tokens(readme)
7070

7171
# CLAUDE: Calculate approximate cost
7272
# Input cost: $3 per 1M tokens ($0.000003 per token)
@@ -75,7 +75,6 @@ async def get_generation_cost(request: Request, body: ApiRequest):
7575
# output_cost = 3500 * 0.000015
7676
# estimated_cost = input_cost + output_cost
7777

78-
# O3: Calculate approximate cost temp: o1-mini, same price as o3-mini
7978
# Input cost: $1.1 per 1M tokens ($0.0000011 per token)
8079
# Output cost: $4.4 per 1M tokens ($0.0000044 per token)
8180
input_cost = ((file_tree_tokens * 2 + readme_tokens) + 3000) * 0.0000011
@@ -149,13 +148,13 @@ async def event_generator():
149148

150149
# Token count check
151150
combined_content = f"{file_tree}\n{readme}"
152-
token_count = o1_service.count_tokens(combined_content)
151+
token_count = o3_service.count_tokens(combined_content)
153152

154153
if 50000 < token_count < 195000 and not body.api_key:
155154
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"
156155
return
157156
elif token_count > 195000:
158-
yield f"data: {json.dumps({'error': f'Repository is too large (>195k tokens) for analysis. OpenAI o1-mini\'s max context length is 200k tokens. Current size: {token_count} tokens.'})}\n\n"
157+
yield f"data: {json.dumps({'error': f'Repository is too large (>195k tokens) for analysis. OpenAI o3-mini\'s max context length is 200k tokens. Current size: {token_count} tokens.'})}\n\n"
159158
return
160159

161160
# Prepare prompts
@@ -174,18 +173,19 @@ async def event_generator():
174173
)
175174

176175
# Phase 1: Get explanation
177-
yield f"data: {json.dumps({'status': 'explanation_sent', 'message': 'Sending explanation request to o1-mini...'})}\n\n"
176+
yield f"data: {json.dumps({'status': 'explanation_sent', 'message': 'Sending explanation request to o3-mini...'})}\n\n"
178177
await asyncio.sleep(0.1)
179178
yield f"data: {json.dumps({'status': 'explanation', 'message': 'Analyzing repository structure...'})}\n\n"
180179
explanation = ""
181-
async for chunk in o1_service.call_o1_api_stream(
180+
async for chunk in o3_service.call_o3_api_stream(
182181
system_prompt=first_system_prompt,
183182
data={
184183
"file_tree": file_tree,
185184
"readme": readme,
186185
"instructions": body.instructions,
187186
},
188187
api_key=body.api_key,
188+
reasoning_effort="medium",
189189
):
190190
explanation += chunk
191191
yield f"data: {json.dumps({'status': 'explanation_chunk', 'chunk': chunk})}\n\n"
@@ -195,14 +195,15 @@ async def event_generator():
195195
return
196196

197197
# Phase 2: Get component mapping
198-
yield f"data: {json.dumps({'status': 'mapping_sent', 'message': 'Sending component mapping request to o1-mini...'})}\n\n"
198+
yield f"data: {json.dumps({'status': 'mapping_sent', 'message': 'Sending component mapping request to o3-mini...'})}\n\n"
199199
await asyncio.sleep(0.1)
200200
yield f"data: {json.dumps({'status': 'mapping', 'message': 'Creating component mapping...'})}\n\n"
201201
full_second_response = ""
202-
async for chunk in o1_service.call_o1_api_stream(
202+
async for chunk in o3_service.call_o3_api_stream(
203203
system_prompt=SYSTEM_SECOND_PROMPT,
204204
data={"explanation": explanation, "file_tree": file_tree},
205205
api_key=body.api_key,
206+
reasoning_effort="low",
206207
):
207208
full_second_response += chunk
208209
yield f"data: {json.dumps({'status': 'mapping_chunk', 'chunk': chunk})}\n\n"
@@ -218,18 +219,19 @@ async def event_generator():
218219
]
219220

220221
# Phase 3: Generate Mermaid diagram
221-
yield f"data: {json.dumps({'status': 'diagram_sent', 'message': 'Sending diagram generation request to o1-mini...'})}\n\n"
222+
yield f"data: {json.dumps({'status': 'diagram_sent', 'message': 'Sending diagram generation request to o3-mini...'})}\n\n"
222223
await asyncio.sleep(0.1)
223224
yield f"data: {json.dumps({'status': 'diagram', 'message': 'Generating diagram...'})}\n\n"
224225
mermaid_code = ""
225-
async for chunk in o1_service.call_o1_api_stream(
226+
async for chunk in o3_service.call_o3_api_stream(
226227
system_prompt=third_system_prompt,
227228
data={
228229
"explanation": explanation,
229230
"component_mapping": component_mapping_text,
230231
"instructions": body.instructions,
231232
},
232233
api_key=body.api_key,
234+
reasoning_effort="medium",
233235
):
234236
mermaid_code += chunk
235237
yield f"data: {json.dumps({'status': 'diagram_chunk', 'chunk': chunk})}\n\n"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
from openai import OpenAI
2+
from dotenv import load_dotenv
3+
from app.utils.format_message import format_user_message
4+
import tiktoken
5+
import os
6+
import aiohttp
7+
import json
8+
from typing import AsyncGenerator, Literal
9+
10+
load_dotenv()
11+
12+
13+
class OpenAIo3Service:
14+
def __init__(self):
15+
self.default_client = OpenAI(
16+
api_key=os.getenv("OPENAI_API_KEY"),
17+
)
18+
self.encoding = tiktoken.get_encoding("o200k_base") # Encoder for OpenAI models
19+
self.base_url = "https://api.openai.com/v1/chat/completions"
20+
21+
def call_o3_api(
22+
self,
23+
system_prompt: str,
24+
data: dict,
25+
api_key: str | None = None,
26+
reasoning_effort: Literal["low", "medium", "high"] = "low",
27+
) -> str:
28+
"""
29+
Makes an API call to OpenAI o3-mini and returns the response.
30+
31+
Args:
32+
system_prompt (str): The instruction/system prompt
33+
data (dict): Dictionary of variables to format into the user message
34+
api_key (str | None): Optional custom API key
35+
36+
Returns:
37+
str: o3-mini's response text
38+
"""
39+
# Create the user message with the data
40+
user_message = format_user_message(data)
41+
42+
# Use custom client if API key provided, otherwise use default
43+
client = OpenAI(api_key=api_key) if api_key else self.default_client
44+
45+
try:
46+
print(
47+
f"Making non-streaming API call to o3-mini with API key: {'custom key' if api_key else 'default key'}"
48+
)
49+
50+
completion = client.chat.completions.create(
51+
model="o3-mini",
52+
messages=[
53+
{"role": "system", "content": system_prompt},
54+
{"role": "user", "content": user_message},
55+
],
56+
max_completion_tokens=12000, # Adjust as needed
57+
temperature=0.2,
58+
reasoning_effort=reasoning_effort,
59+
)
60+
61+
print("API call completed successfully")
62+
63+
if completion.choices[0].message.content is None:
64+
raise ValueError("No content returned from OpenAI o3-mini")
65+
66+
return completion.choices[0].message.content
67+
68+
except Exception as e:
69+
print(f"Error in OpenAI o3-mini API call: {str(e)}")
70+
raise
71+
72+
async def call_o3_api_stream(
73+
self,
74+
system_prompt: str,
75+
data: dict,
76+
api_key: str | None = None,
77+
reasoning_effort: Literal["low", "medium", "high"] = "low",
78+
) -> AsyncGenerator[str, None]:
79+
"""
80+
Makes a streaming API call to OpenAI o3-mini and yields the responses.
81+
82+
Args:
83+
system_prompt (str): The instruction/system prompt
84+
data (dict): Dictionary of variables to format into the user message
85+
api_key (str | None): Optional custom API key
86+
87+
Yields:
88+
str: Chunks of o3-mini's response text
89+
"""
90+
# Create the user message with the data
91+
user_message = format_user_message(data)
92+
93+
headers = {
94+
"Content-Type": "application/json",
95+
"Authorization": f"Bearer {api_key or self.default_client.api_key}",
96+
}
97+
98+
# payload = {
99+
# "model": "o3-mini",
100+
# "messages": [
101+
# {
102+
# "role": "user",
103+
# "content": f"""
104+
# <VERY_IMPORTANT_SYSTEM_INSTRUCTIONS>
105+
# {system_prompt}
106+
# </VERY_IMPORTANT_SYSTEM_INSTRUCTIONS>
107+
# <USER_INSTRUCTIONS>
108+
# {user_message}
109+
# </USER_INSTRUCTIONS>
110+
# """,
111+
# },
112+
# ],
113+
# "max_completion_tokens": 12000,
114+
# "stream": True,
115+
# }
116+
117+
payload = {
118+
"model": "o3-mini",
119+
"messages": [
120+
{"role": "system", "content": system_prompt},
121+
{"role": "user", "content": user_message},
122+
],
123+
"max_completion_tokens": 12000,
124+
"stream": True,
125+
"reasoning_effort": reasoning_effort,
126+
}
127+
128+
try:
129+
async with aiohttp.ClientSession() as session:
130+
async with session.post(
131+
self.base_url, headers=headers, json=payload
132+
) as response:
133+
134+
if response.status != 200:
135+
error_text = await response.text()
136+
print(f"Error response: {error_text}")
137+
raise ValueError(
138+
f"OpenAI API returned status code {response.status}: {error_text}"
139+
)
140+
141+
line_count = 0
142+
async for line in response.content:
143+
line = line.decode("utf-8").strip()
144+
if not line:
145+
continue
146+
147+
line_count += 1
148+
149+
if line.startswith("data: "):
150+
if line == "data: [DONE]":
151+
break
152+
try:
153+
data = json.loads(line[6:])
154+
content = (
155+
data.get("choices", [{}])[0]
156+
.get("delta", {})
157+
.get("content")
158+
)
159+
if content:
160+
yield content
161+
except json.JSONDecodeError as e:
162+
print(f"JSON decode error: {e} for line: {line}")
163+
continue
164+
165+
if line_count == 0:
166+
print("Warning: No lines received in stream response")
167+
168+
except aiohttp.ClientError as e:
169+
print(f"Connection error: {str(e)}")
170+
raise ValueError(f"Failed to connect to OpenAI API: {str(e)}")
171+
except Exception as e:
172+
print(f"Unexpected error in streaming API call: {str(e)}")
173+
raise
174+
175+
def count_tokens(self, prompt: str) -> int:
176+
"""
177+
Counts the number of tokens in a prompt.
178+
179+
Args:
180+
prompt (str): The prompt to count tokens for
181+
182+
Returns:
183+
int: Estimated number of input tokens
184+
"""
185+
num_tokens = len(self.encoding.encode(prompt))
186+
return num_tokens

0 commit comments

Comments
 (0)