Skip to content

Commit f5cea1e

Browse files
committed
Adding Credit System
1 parent f3677e7 commit f5cea1e

File tree

22 files changed

+1242
-225
lines changed

22 files changed

+1242
-225
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ __pycache__/
44
*.png
55
helpers/.nltk_resources_cache
66
**/.nltk
7-
.vscode/settings.json
8-
7+
.vscode/settings.json

app/api/v1/dependencies/feature_flags.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Optional
22
from fastapi import Request
3-
from app.core.settings import settings
3+
from app.core.config import settings
44

55
async def check_advanced_features(request: Request) -> bool:
66
"""

app/api/v1/routes/auth.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from fastapi import APIRouter
2+
import supabase
3+
4+
from app.core.security import authenticate_user, create_user
5+
from app.core.config import settings
6+
7+
router = APIRouter(tags=["auth"])
8+
9+
@router.post("/auth/signup")
10+
async def signup(email: str, password: str):
11+
token_data = await create_user(email, password)
12+
return {"access_token": token_data}
13+
14+
@router.post("/auth/login")
15+
async def login(email: str, password: str):
16+
token_data = await authenticate_user(email, password)
17+
18+
return {"access_token": token_data}

app/api/v1/routes/ideas.py

Lines changed: 138 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from fastapi import APIRouter, Depends, Request, Response
1+
from fastapi import APIRouter, Depends, HTTPException, Request, Response
22
from typing import List
33
import json
44

55
from app.core.limiter import limiter
6-
from app.core.settings import settings
6+
from app.core.config import settings
77
from app.services.clustering import summarize_clusters
8+
from app.services.credits import CreditService
89
from ..dependencies.auth import verify_token
910

1011
from ....services.analyzer import centroid_analysis
@@ -19,9 +20,8 @@
1920
async def rank_ideas(
2021
request: Request,
2122
ideaRequest: IdeaRequest,
22-
token: str = Depends(verify_token), # No effect for now; verification is disabled
23+
user_info: dict = Depends(verify_token),
2324
) -> AnalysisResponse:
24-
print('Ranking ideas')
2525
"""
2626
Analyze and rank ideas based on semantic similarity.
2727
@@ -31,96 +31,167 @@ async def rank_ideas(
3131
- Generate relationship graphs (optional)
3232
- Create pairwise similarity matrix (optional)
3333
34+
Credits required:
35+
- Basic analysis: 1 credit per 100 ideas
36+
- Relationship graph: 3 credits
37+
- Cluster names: 5 credits
38+
3439
Returns:
3540
AnalysisResponse containing ranked ideas and optional advanced analysis
3641
3742
Raises:
3843
HTTPException(400): If input data is invalid
44+
HTTPException(402): If insufficient credits
3945
HTTPException(429): If rate limit is exceeded
4046
"""
41-
42-
# Extract raw ideas for analysis
47+
print('Ranking ideas')
48+
4349
ideas = [item.idea for item in ideaRequest.ideas]
44-
id_mapping = {item.idea: {'id': item.id or 1, 'author_id': item.author_id} for item in ideaRequest.ideas}
45-
if len(ideas) < 4:
46-
return Response(status_code=400, content='Please provide at least 4 items to analyze')
50+
num_ideas = len(ideas)
51+
total_bytes = sum(len(item.idea.encode('utf-8')) for item in ideaRequest.ideas)
52+
53+
if num_ideas < 4:
54+
return Response(status_code=400, content='Please provide at least 4 items to analyze')
55+
56+
# Check credits for basic analysis
57+
user_id = user_info["user_id"]
58+
59+
operations = ["basic_analysis"]
60+
61+
if ideaRequest.advanced_features:
62+
if ideaRequest.advanced_features.relationship_graph:
63+
operations.append("relationship_graph")
64+
if ideaRequest.advanced_features.cluster_names:
65+
operations.append("cluster_names")
66+
67+
if not await CreditService.has_sufficient_credits(
68+
user_id, operations, num_ideas, total_bytes
69+
):
70+
raise HTTPException(
71+
status_code=402,
72+
detail=f"Insufficient credits for analysis. Available credits: {await CreditService.get_credits(user_id)}"
73+
)
4774

4875
# Perform core analysis
4976
results, plot_data = centroid_analysis(ideas)
77+
await CreditService.deduct_credits(user_id, "basic_analysis", num_ideas, total_bytes)
5078

51-
# Create ranked ideas response
79+
response = await build_base_response(ideas, results, plot_data)
80+
81+
if ideaRequest.advanced_features:
82+
response = await process_advanced_features(
83+
ideaRequest, response, user_id, ideas, plot_data, num_ideas, total_bytes
84+
)
85+
86+
print('Results calculated successfully!\n', response)
87+
return AnalysisResponse(**response)
88+
89+
def _generate_edges(ranked_ideas: List[RankedIdea], similarity_matrix: List[List[float]]) -> List[dict]:
90+
"""
91+
Generate graph edges showing relationships between ideas and to centroid.
92+
93+
Creates two types of edges:
94+
1. Between ideas based on pairwise similarity
95+
2. From each idea to the centroid based on similarity scores
96+
"""
97+
edges = []
98+
99+
# Create edges between ideas
100+
for i, idea_from in enumerate(ranked_ideas):
101+
if i+1 > len(similarity_matrix):
102+
break
103+
for j, idea_to in enumerate(ranked_ideas[i+1:], i+1):
104+
edges.append({
105+
"from_id": idea_from.id,
106+
"to_id": idea_to.id,
107+
"similarity": similarity_matrix[i][j]
108+
})
109+
110+
# Create edges to centroid
111+
for idea in ranked_ideas:
112+
edges.append({
113+
"from_id": idea.id,
114+
"to_id": "Centroid",
115+
"similarity": idea.similarity_score
116+
})
117+
118+
return edges
119+
120+
async def build_base_response(ideas: List[str], results: Results, plot_data: PlotData) -> dict:
121+
"""Build base response with ranked ideas and similarity scores"""
52122
ranked_ideas = [
53123
RankedIdea(
54-
id=id_mapping[idea]['id'],
55-
author_id=id_mapping[idea]['author_id'],
124+
id=str(idx),
56125
idea=idea,
57-
similarity_score=results["similarity"][index],
58-
cluster_id=plot_data["kmeans_data"]["cluster"][index],
126+
similarity_score=results["similarity"][idx],
127+
cluster_id=plot_data["kmeans_data"]["cluster"][idx],
59128
)
60-
for index, idea in enumerate(results["ideas"])
129+
for idx, idea in enumerate(results["ideas"])
61130
]
62131

63-
# Sort by similarity score
64132
ranked_ideas.sort(key=lambda x: x.similarity_score, reverse=True)
65133

66-
# Build response with optional advanced features
67-
response = {
134+
return {
68135
"ranked_ideas": ranked_ideas,
69136
"relationship_graph": None,
70137
"pairwise_similarity_matrix": None,
71138
"cluster_names": None
72139
}
73-
74140

75-
if ideaRequest.advanced_features:
76-
if ideaRequest.advanced_features.relationship_graph:
77-
coords = plot_data.get("scatter_points", [])
78-
nodes = [
79-
{
80-
"id": idea.id,
81-
"coordinates": {
82-
"x": coords[i][0],
83-
"y": coords[i][1]
84-
}
85-
}
86-
for i, idea in enumerate(ranked_ideas)]
87-
# Add the centroid:
88-
nodes.append({"id": "Centroid", "coordinates": {"x": coords[-1][0], "y": coords[-1][0]}})
89-
response["relationship_graph"] = RelationshipGraph(
90-
nodes=nodes,
91-
edges=_generate_edges(ranked_ideas, plot_data.get("pairwise_similarity", []))
92-
)
93-
94-
if ideaRequest.advanced_features.pairwise_similarity_matrix:
95-
response["pairwise_similarity_matrix"] = plot_data.get("pairwise_similarity")
96-
97-
if ideaRequest.advanced_features.cluster_names:
98-
response["cluster_names"] = await summarize_clusters(ranked_ideas)
99-
print('Results calculated successfully!\n', response)
100-
return AnalysisResponse(**response)
141+
async def process_advanced_features(
142+
request: IdeaRequest,
143+
response: dict,
144+
user_id: str,
145+
ideas: List[str],
146+
plot_data: PlotData,
147+
num_ideas: int,
148+
total_bytes: int
149+
) -> dict:
150+
"""Process and add advanced features if credits are available"""
151+
if request.advanced_features.relationship_graph:
152+
response["relationship_graph"] = build_relationship_graph(
153+
response["ranked_ideas"], plot_data
154+
)
155+
CreditService.deduct_credits(user_id, "relationship_graph", num_ideas, total_bytes)
156+
157+
if request.advanced_features.cluster_names:
158+
response["cluster_names"] = await summarize_clusters(response["ranked_ideas"])
159+
CreditService.deduct_credits(user_id, "cluster_names", num_ideas, total_bytes)
160+
161+
if request.advanced_features.pairwise_similarity_matrix:
162+
response["pairwise_similarity_matrix"] = plot_data.pairwise_similarity
163+
164+
return response
101165

102-
def _generate_edges(ranked_ideas: List[RankedIdea], similarity_matrix: List) -> List[dict]:
103-
"""Generate graph edges based on similarity scores"""
104-
edges = []
166+
def build_relationship_graph(ranked_ideas: List[RankedIdea], plot_data: PlotData) -> RelationshipGraph:
167+
"""
168+
Builds a graph representation of idea relationships including:
169+
- Nodes with coordinates from MDS analysis
170+
- Edges showing similarity between ideas
171+
- Centroid connections
172+
"""
173+
coords = plot_data.scatter_points
105174

106-
for i, idea_from in enumerate(ranked_ideas):
107-
if i+1 > len(similarity_matrix): break
108-
for j, idea_to in enumerate(ranked_ideas[i+1:], i+1):
109-
edge = {
110-
"from_id": idea_from.id,
111-
"to_id": idea_to.id,
112-
"similarity": similarity_matrix[i][j]
113-
}
114-
edges.append(edge)
115-
116-
# Add edges to centroid
117-
for i, idea in enumerate(ranked_ideas):
118-
edge = {
119-
"from_id": idea.id,
120-
"to_id": "Centroid", # centroid id
121-
"similarity": idea.similarity_score
175+
# Create nodes including centroid
176+
nodes = [
177+
{
178+
"id": idea.id,
179+
"coordinates": {
180+
"x": coords[i][0],
181+
"y": coords[i][1]
182+
}
183+
}
184+
for i, idea in enumerate(ranked_ideas)
185+
]
186+
nodes.append({
187+
"id": "Centroid",
188+
"coordinates": {
189+
"x": coords[-1][0],
190+
"y": coords[-1][1]
122191
}
123-
edges.append(edge)
192+
})
124193

125-
126-
return edges
194+
# Generate edges between ideas and to centroid
195+
edges = _generate_edges(ranked_ideas, plot_data.pairwise_similarity)
196+
197+
return RelationshipGraph(nodes=nodes, edges=edges)

app/core/config.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from pydantic_settings import SettingsConfigDict, BaseSettings
2+
from typing import Dict, TypedDict
3+
4+
class OperationCost(TypedDict):
5+
base_cost: int
6+
per_hundred_items: int
7+
per_kilobyte: int
8+
9+
class Settings(BaseSettings):
10+
PROJECT_NAME: str
11+
12+
# List all published versions here. This enables us to manage them better; i.e. gradually phase them out etc...
13+
API_V1_STR: str
14+
API_V0_STR: str
15+
16+
# Security
17+
SECRET_KEY: str
18+
ACCESS_TOKEN_EXPIRE_MINUTES: int
19+
20+
# Rate Limiting
21+
GLOBAL_RATE_LIMIT: str
22+
RATE_LIMIT_PER_USER: str
23+
24+
# MongoDB settings if we use it
25+
MONGODB_URI: str
26+
27+
# OpenAI if used:
28+
OPENAI_API_KEY: str
29+
30+
# Supabase postgres db settings
31+
DATABASE_URL: str
32+
DATABASE_POOLER_URL: str
33+
DATABASE_KEY: str
34+
DATABASE_PROJECT_URL: str
35+
SUPABASE_SERVICE_ROLE_KEY: str
36+
37+
OPERATION_COSTS: Dict[str, OperationCost] = {
38+
"basic_analysis": {
39+
"base_cost": 1,
40+
"per_hundred_items": 1,
41+
"per_kilobyte": 1
42+
},
43+
"relationship_graph": {
44+
"base_cost": 3,
45+
"per_hundred_items": 2,
46+
"per_kilobyte": 1
47+
},
48+
"cluster_names": {
49+
"base_cost": 3,
50+
"per_hundred_items": 1,
51+
"per_kilobyte": 1
52+
}
53+
}
54+
55+
GUEST_DAILY_CREDITS: int
56+
GUEST_MAX_CREDITS: int
57+
USER_DAILY_CREDITS: int
58+
USER_MAX_CREDITS: int
59+
60+
model_config = SettingsConfigDict(env_file=".env")
61+
62+
settings = Settings()

app/core/db.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from supabase import create_client, Client
2+
from .config import settings
3+
4+
db: Client = create_client(
5+
settings.DATABASE_PROJECT_URL,
6+
settings.SUPABASE_SERVICE_ROLE_KEY
7+
)

app/core/limiter.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
# slowapi:
1010
from slowapi import Limiter
1111
from slowapi.util import get_remote_address
12+
from app.core.config import settings
13+
14+
def get_identifier(request):
15+
"""Get unique identifier for rate limiting based on auth status"""
16+
if "authorization" in request.headers:
17+
return request.headers["authorization"]
18+
return get_remote_address(request)
1219

1320
limiter = Limiter(
14-
key_func=get_remote_address,
15-
default_limits=["1/minute"],
21+
key_func=get_identifier,
22+
default_limits=[settings.RATE_LIMIT_PER_USER],
1623
strategy="fixed-window",
1724
storage_uri="memory://", # if we have auto-scaling / multiple servers we'd want to change this to a database
18-
)
25+
)

0 commit comments

Comments
 (0)