Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
80baf01
feat: revamped ui
Saahi30 Jul 19, 2025
d3fe0ba
feat: added new tables
Saahi30 Jul 19, 2025
00c4703
feat: added fastapi models
Saahi30 Jul 19, 2025
0432220
feat: added pydantic schemas
Saahi30 Jul 19, 2025
ab357c6
feat: add api routes and minor security improvements
Saahi30 Jul 20, 2025
20e6bdd
feat: add smart features to searchbar
Saahi30 Jul 20, 2025
425c9ab
feat: added more endpoints for refinement
Saahi30 Jul 20, 2025
8c0f605
fix: add missing SponsorshipCreate import
Saahi30 Jul 20, 2025
635f252
fix: resolve CSS import order and TypeScript errors
Saahi30 Jul 20, 2025
d1b8b33
feat: connect frontend to backend with API integration and AI search
Saahi30 Jul 20, 2025
6f528ef
docs: add frontend-backend integration documentation
Saahi30 Jul 20, 2025
e62ba6b
fix: separate AI API service and fix 404 error for AI queries
Saahi30 Jul 20, 2025
d79c411
feat: improve AI response display with better formatting
Saahi30 Jul 20, 2025
34ede84
fix: remove the metric cards from brand dashboard
Saahi30 Jul 20, 2025
e36a966
fix: changed primary call model to kimi-k2
Saahi30 Jul 21, 2025
6658f61
fix: llm parsing inconsistency fix
Saahi30 Jul 27, 2025
6420e00
feat: add redis cloud connect
Saahi30 Jul 27, 2025
4a4382e
feat: add hybrid orchestration and session management
Saahi30 Jul 27, 2025
1cdee7c
fix: fix session management and response types
Saahi30 Jul 27, 2025
ce9e4d1
feat: chat component
Saahi30 Jul 27, 2025
7cc96af
feat: backend assistant integration
Saahi30 Jul 27, 2025
8c7cfad
feat: add dashboard route
Saahi30 Jul 29, 2025
0d833db
feat: add the brand overview dashboard
Saahi30 Jul 30, 2025
da4415b
feat: update dashboard overview for design
Saahi30 Aug 1, 2025
a11acd2
feat: Added contracts schemas
Saahi30 Aug 1, 2025
65fba45
feat: main Contracts dashboard
Saahi30 Aug 1, 2025
21e7896
feat: comprehensive contract details modal
Saahi30 Aug 1, 2025
cb8950a
built the remaining features
Saahi30 Aug 1, 2025
fd36d56
feat: add database queries for contracts
Saahi30 Aug 2, 2025
9823585
feat: create api system
Saahi30 Aug 2, 2025
80970cc
feat: register all endpoints and routing
Saahi30 Aug 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Backend/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ password=[YOUR-PASSWORD]
host=
port=5432
dbname=postgres
GROQ_API_KEY=
GROQ_API_KEY=your_groq_api_key_here
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

🧩 Analysis chain

Avoid real secrets; align variable names with backend; document Redis SSL.

  • Don’t commit real keys; keep placeholders. Add a header comment reminding contributors to copy this file to .env and never commit real values.
  • Verify the backend actually reads GROQ_API_KEY, YOUTUBE_API_KEY, and Redis vars as named here.
  • Redis Cloud typically requires TLS. The example suggests non‑SSL; either mark REDIS_SSL=true in examples or annotate when to disable.

Run to confirm env var usage:

Also applies to: 10-10, 12-15


🏁 Script executed:

#!/bin/bash
rg -nP --type=py -C2 '\bos\.getenv\(\s*"(GROQ_API_KEY|YOUTUBE_API_KEY|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD)"'

Length of output: 1746


🏁 Script executed:

#!/bin/bash
# Get full redis_client.py to see complete redis.Redis() call
cat Backend/app/services/redis_client.py

Length of output: 834


Add REDIS_SSL to .env-example and make SSL configurable in redis_client.py.

Env var names are correctly aligned with backend usage (verified: GROQ_API_KEY, YOUTUBE_API_KEY, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD all read correctly).

However, the .env-example is missing REDIS_SSL configuration. Currently, redis_client.py hardcodes ssl=False (line 14), which:

  • Cannot be overridden per deployment
  • Contradicts typical Redis Cloud requirements (which mandate TLS)
  • Makes the comment "works without SSL" misleading

Add REDIS_SSL=true to .env-example and update redis_client.py to read it: ssl=os.getenv("REDIS_SSL", "false").lower() == "true" (or similar, depending on your parsing preference).

πŸ€– Prompt for AI Agents
In Backend/.env-example around line 6 add a REDIS_SSL entry (e.g.,
REDIS_SSL=true) so deployments can opt into TLS; then update
Backend/redis_client.py (around the current ssl=False on line 14) to read the
env var and set ssl accordingly using something like ssl =
os.getenv("REDIS_SSL", "false").lower() == "true", and update the inline comment
to reflect that SSL is now configurable and defaults to false.

SUPABASE_URL=
SUPABASE_KEY=
GEMINI_API_KEY=
YOUTUBE_API_KEY=
YOUTUBE_API_KEY=

# Redis Cloud configuration
REDIS_HOST=your-redis-cloud-host
REDIS_PORT=12345
REDIS_PASSWORD=your-redis-cloud-password
6 changes: 6 additions & 0 deletions Backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from .routes.post import router as post_router
from .routes.chat import router as chat_router
from .routes.match import router as match_router
from .routes.brand_dashboard import router as brand_dashboard_router
from .routes.ai_query import router as ai_query_router
from .routes.contracts import router as contracts_router
from sqlalchemy.exc import SQLAlchemyError
import logging
import os
Expand Down Expand Up @@ -54,6 +57,9 @@ async def lifespan(app: FastAPI):
app.include_router(post_router)
app.include_router(chat_router)
app.include_router(match_router)
app.include_router(brand_dashboard_router)
app.include_router(ai_query_router)
app.include_router(contracts_router)
app.include_router(ai.router)
app.include_router(ai.youtube_router)

Expand Down
81 changes: 80 additions & 1 deletion Backend/app/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
TIMESTAMP,
)
from sqlalchemy.orm import relationship
from datetime import datetime
from datetime import datetime, timezone
from app.db.db import Base
import uuid

Expand Down Expand Up @@ -160,3 +160,82 @@ class SponsorshipPayment(Base):
brand = relationship(
"User", foreign_keys=[brand_id], back_populates="brand_payments"
)


# ============================================================================
# BRAND DASHBOARD MODELS
# ============================================================================

# Brand Profile Table (Extended brand information)
class BrandProfile(Base):
__tablename__ = "brand_profiles"

id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
company_name = Column(String, nullable=True)
website = Column(String, nullable=True)
industry = Column(String, nullable=True)
contact_person = Column(String, nullable=True)
contact_email = Column(String, nullable=True)
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)

# Relationships
user = relationship("User", backref="brand_profile")


# Campaign Metrics Table (Performance tracking)
class CampaignMetrics(Base):
__tablename__ = "campaign_metrics"

id = Column(String, primary_key=True, default=generate_uuid)
campaign_id = Column(String, ForeignKey("sponsorships.id"), nullable=False)
impressions = Column(Integer, nullable=True)
clicks = Column(Integer, nullable=True)
conversions = Column(Integer, nullable=True)
revenue = Column(DECIMAL(10, 2), nullable=True)
engagement_rate = Column(Float, nullable=True)
recorded_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)

# Relationships
campaign = relationship("Sponsorship", backref="metrics")


# Contracts Table (Contract management)
class Contract(Base):
__tablename__ = "contracts"

id = Column(String, primary_key=True, default=generate_uuid)
sponsorship_id = Column(String, ForeignKey("sponsorships.id"), nullable=False)
creator_id = Column(String, ForeignKey("users.id"), nullable=False)
brand_id = Column(String, ForeignKey("users.id"), nullable=False)
contract_url = Column(String, nullable=True)
status = Column(String, default="draft") # draft, signed, completed, cancelled
created_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)

# Relationships
sponsorship = relationship("Sponsorship", backref="contracts")
creator = relationship("User", foreign_keys=[creator_id], backref="creator_contracts")
brand = relationship("User", foreign_keys=[brand_id], backref="brand_contracts")


# Creator Matches Table (AI-powered matching)
class CreatorMatch(Base):
__tablename__ = "creator_matches"

id = Column(String, primary_key=True, default=generate_uuid)
brand_id = Column(String, ForeignKey("users.id"), nullable=False)
creator_id = Column(String, ForeignKey("users.id"), nullable=False)
match_score = Column(Float, nullable=True)
matched_at = Column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)

# Relationships
brand = relationship("User", foreign_keys=[brand_id], backref="creator_matches")
creator = relationship("User", foreign_keys=[creator_id], backref="brand_matches")
244 changes: 244 additions & 0 deletions Backend/app/routes/ai_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
from fastapi import APIRouter, HTTPException, Query, Depends, Request
from typing import Dict, Any, Optional
from pydantic import BaseModel
import logging
from ..services.ai_router import ai_router
from ..services.redis_client import get_session_state, save_session_state
import uuid

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Define Router
router = APIRouter(prefix="/api/ai", tags=["AI Query"])

# Pydantic models for request/response
class AIQueryRequest(BaseModel):
query: str
brand_id: Optional[str] = None
context: Optional[Dict[str, Any]] = None

class AIQueryResponse(BaseModel):
intent: str
route: Optional[str] = None
parameters: Dict[str, Any] = {}
follow_up_needed: bool = False
follow_up_question: Optional[str] = None
explanation: str
original_query: str
timestamp: str

Comment on lines +22 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | πŸ”΄ Critical

Response model drops session_id/result; include them and fix mutable defaults.

FastAPI strips fields not in the response_model. Frontend expects session_id and result, but they’re not defined, so they won’t be returned. Also avoid {} as a default.

-from pydantic import BaseModel
+from pydantic import BaseModel, Field
@@
 class AIQueryResponse(BaseModel):
     intent: str
     route: Optional[str] = None
-    parameters: Dict[str, Any] = {}
+    parameters: Dict[str, Any] = Field(default_factory=dict)
     follow_up_needed: bool = False
     follow_up_question: Optional[str] = None
     explanation: str
     original_query: str
     timestamp: str
+    session_id: Optional[str] = None
+    result: Optional[Any] = None
+    error: Optional[str] = None
@@
-        response = AIQueryResponse(
+        response = AIQueryResponse(
             intent=result.get("intent", "unknown"),
             route=result.get("route"),
             parameters=params,
-            follow_up_needed=not all_params_present and not only_optional_params or api_error is not None,
+            follow_up_needed=((not all_params_present and not only_optional_params) or (api_error is not None)),
             follow_up_question=(result.get("follow_up_question") if not all_params_present and not only_optional_params else None),
             explanation=(result.get("explanation", "") if not api_error else f"An error occurred while processing your request: {api_error}"),
             original_query=result.get("original_query", request.query),
             timestamp=result.get("timestamp", ""),
         )
@@
-        response_dict = response.dict()
+        response_dict = response.dict()
@@
-        if api_result is not None:
-            response_dict["result"] = api_result
-        if api_error is not None:
-            response_dict["error"] = api_error
-        return response_dict
+        if api_result is not None:
+            response_dict["result"] = api_result
+        if api_error is not None:
+            response_dict["error"] = api_error
+        return response_dict

Also applies to: 143-152, 154-182

πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 22-31 (and also apply same fixes
at 143-152 and 154-182), the AIQueryResponse Pydantic model is missing the
session_id and result fields which causes FastAPI to strip them from responses,
and it uses a mutable default (parameters: Dict = {}), which is unsafe; add
session_id: Optional[str] (or str if required) and result: Optional[Dict[str,
Any]] (or the correct type) to the model, and replace mutable defaults with None
and proper default_factory where needed (e.g., parameters: Dict[str, Any] =
Field(default_factory=dict) or parameters: Optional[Dict[str, Any]] = None) so
fields are returned to the frontend and defaults are immutable/initialized
per-instance.

@router.post("/query", response_model=AIQueryResponse)
async def process_ai_query(request: AIQueryRequest, http_request: Request):
"""
Process a natural language query through AI and return routing information
"""
try:
# Validate input
if not request.query or len(request.query.strip()) == 0:
raise HTTPException(status_code=400, detail="Query cannot be empty")

# Process query through AI router
result = await ai_router.process_query(
query=request.query.strip(),
brand_id=request.brand_id
)

# --- Hybrid Orchestration Logic ---
# Extended intent-to-parameter mapping for all available routes
intent_param_map = {
"dashboard_overview": {"required": ["brand_id"], "optional": []},
"brand_profile": {"required": ["user_id"], "optional": []},
"campaigns": {"required": ["brand_id"], "optional": ["campaign_id"]},
"creator_matches": {"required": ["brand_id"], "optional": []},
"creator_search": {"required": ["brand_id"], "optional": ["industry", "min_engagement", "location"]},
"creator_profile": {"required": ["creator_id", "brand_id"], "optional": []},
"analytics_performance": {"required": ["brand_id"], "optional": []},
"analytics_revenue": {"required": ["brand_id"], "optional": []},
"contracts": {"required": ["brand_id"], "optional": ["contract_id"]},
}
intent = result.get("route")
params = result.get("parameters", {})

# Debug: Log the parameters to understand the type issue
logger.info(f"Intent: {intent}")
logger.info(f"Params: {params}")
logger.info(f"Params type: {type(params)}")
for key, value in params.items():
logger.info(f" {key}: {value} (type: {type(value)})")

api_result = None
api_error = None
# Prepare arguments for API calls, including optional params if present
def get_api_args(intent, params):
args = {}
if intent in intent_param_map:
# Add required params
for param in intent_param_map[intent]["required"]:
if params.get(param) is not None:
args[param] = params[param]
# Add optional params if present
for param in intent_param_map[intent]["optional"]:
if params.get(param) is not None:
args[param] = params[param]
return args

# Check if all required params are present
all_params_present = True
missing_params = []
if intent in intent_param_map:
for param in intent_param_map[intent]["required"]:
if not params.get(param):
all_params_present = False
missing_params.append(param)

# Allow queries with only optional params if API supports it (e.g., creator_search with filters)
only_optional_params = False
if intent in intent_param_map and not all_params_present:
# If at least one optional param is present and no required params are present
if (
len(intent_param_map[intent]["optional"]) > 0 and
all(params.get(p) is None for p in intent_param_map[intent]["required"]) and
any(params.get(p) is not None for p in intent_param_map[intent]["optional"])
):
only_optional_params = True

if (intent and all_params_present) or (intent and only_optional_params):
try:
api_args = get_api_args(intent, params)
# Use aliases for get_campaigns and get_contracts
if intent == "creator_search":
from ..routes.brand_dashboard import search_creators
api_result = await search_creators(**api_args)
elif intent == "dashboard_overview":
from ..routes.brand_dashboard import get_dashboard_overview
api_result = await get_dashboard_overview(**api_args)
elif intent == "creator_matches":
from ..routes.brand_dashboard import get_creator_matches
api_result = await get_creator_matches(**api_args)
elif intent == "brand_profile":
from ..routes.brand_dashboard import get_brand_profile
api_result = await get_brand_profile(**api_args)
elif intent == "campaigns":
from ..routes.brand_dashboard import get_brand_campaigns as get_campaigns
api_result = await get_campaigns(**api_args)
elif intent == "creator_profile":
from ..routes.brand_dashboard import get_creator_profile
api_result = await get_creator_profile(**api_args)
elif intent == "analytics_performance":
from ..routes.brand_dashboard import get_campaign_performance
api_result = await get_campaign_performance(**api_args)
elif intent == "analytics_revenue":
from ..routes.brand_dashboard import get_revenue_analytics
api_result = await get_revenue_analytics(**api_args)
elif intent == "contracts":
from ..routes.brand_dashboard import get_brand_contracts as get_contracts
api_result = await get_contracts(**api_args)
except Exception as api_exc:
logger.error(f"API call failed for intent '{intent}': {api_exc}")
api_error = str(api_exc)

# Convert to response model, add 'result' field for actual data
response = AIQueryResponse(
intent=result.get("intent", "unknown"),
route=result.get("route"),
parameters=params,
follow_up_needed=not all_params_present and not only_optional_params or api_error is not None,
follow_up_question=(result.get("follow_up_question") if not all_params_present and not only_optional_params else None),
explanation=(result.get("explanation", "") if not api_error else f"An error occurred while processing your request: {api_error}"),
original_query=result.get("original_query", request.query),
timestamp=result.get("timestamp", ""),
)
# Attach result if available
response_dict = response.dict()
# 1. Get or generate session_id
session_id = http_request.headers.get("X-Session-ID")
if not session_id and request.context:
session_id = request.context.get("session_id")
if not session_id:
session_id = str(uuid.uuid4())

# 2. Load previous state from Redis
state = await get_session_state(session_id)
prev_params = state.get("params", {})
prev_intent = state.get("intent")

# 3. Merge new params and intent
# Use new intent if present, else previous
intent = result.get("route") or prev_intent
params = {**prev_params, **result.get("parameters", {})}
state["params"] = params
state["intent"] = intent

# 4. Save updated state to Redis
await save_session_state(session_id, state)

response_dict["session_id"] = session_id
if api_result is not None:
response_dict["result"] = api_result
if api_error is not None:
response_dict["error"] = api_error
return response_dict
except HTTPException:
raise
except Exception as e:
logger.error(f"Error processing AI query: {e}")
raise HTTPException(status_code=500, detail="Failed to process AI query")

@router.get("/routes")
async def get_available_routes():
"""
Get list of available routes that the AI can route to
"""
try:
routes = ai_router.list_available_routes()
return {
"available_routes": routes,
"total_routes": len(routes)
}
except Exception as e:
logger.error(f"Error fetching available routes: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch routes")

@router.get("/route/{route_name}")
async def get_route_info(route_name: str):
"""
Get detailed information about a specific route
"""
try:
route_info = ai_router.get_route_info(route_name)
if not route_info:
raise HTTPException(status_code=404, detail=f"Route '{route_name}' not found")

return {
"route_name": route_name,
"info": route_info
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching route info: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch route info")

@router.post("/test")
async def test_ai_query(query: str = Query(..., description="Test query")):
"""
Test endpoint for AI query processing (for development)
"""
try:
# Process test query
result = await ai_router.process_query(query=query)

return {
"test_query": query,
"result": result,
"status": "success"
}
except Exception as e:
logger.error(f"Error in test AI query: {e}")
return {
"test_query": query,
"error": str(e),
"status": "error"
}
Loading