Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
7ff5998
feat: introduce brand dashboard backend API endpoints
Saahi30 Sep 29, 2025
83c58ef
append brand_id query only when brandId is provided
Saahi30 Sep 29, 2025
5191523
allow zero min_engagement filter value in brandApi query
Saahi30 Sep 29, 2025
10d8042
fix: safe JSON parsing in brandApi fetch requests
Saahi30 Sep 29, 2025
b9da7a0
fix:(coderabbit)Remove unused external Google Fonts import in index.css
Saahi30 Sep 29, 2025
d05ec8a
Update link to use /brand/dashboard route
Saahi30 Sep 29, 2025
c6c5bc7
fix:coderabbit fixes
Saahi30 Sep 29, 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
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
4 changes: 4 additions & 0 deletions Backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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 sqlalchemy.exc import SQLAlchemyError
import logging
import os
Expand Down Expand Up @@ -54,6 +56,8 @@ 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(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
Comment on lines +22 to +26
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 | 🟠 Major

Use Field(default_factory=dict) for mutable default.

Avoid shared default dict across instances.

-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)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AIQueryResponse(BaseModel):
intent: str
route: Optional[str] = None
parameters: Dict[str, Any] = {}
follow_up_needed: bool = False
++ b/Backend/app/routes/ai_query.py
@@ -1,3 +1,4 @@
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any
class AIQueryResponse(BaseModel):
intent: str
@@ -5,7 +6,7 @@ class AIQueryResponse(BaseModel):
route: Optional[str] = None
parameters: Dict[str, Any] = Field(default_factory=dict)
follow_up_needed: bool = False
πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 22 to 26, the Pydantic model uses
a mutable default dict for the parameters field which can be shared across
instances; change the field to use Field(default_factory=dict) (i.e.,
parameters: Dict[str, Any] = Field(default_factory=dict)) and add the necessary
import from pydantic (Field) at the top of the file so each model instance gets
its own empty dict.

follow_up_question: Optional[str] = None
explanation: str
original_query: str
timestamp: str

@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)

Comment on lines +91 to +95
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 | 🟠 Major

Required‑param check treats falsy values as missing.

Use is None to avoid misclassifying valid falsy inputs (e.g., 0).

-            for param in intent_param_map[intent]["required"]:
-                if not params.get(param):
-                    all_params_present = False
-                    missing_params.append(param)
+            for param in intent_param_map[intent]["required"]:
+                if params.get(param) is None:
+                    all_params_present = False
+                    missing_params.append(param)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for param in intent_param_map[intent]["required"]:
if not params.get(param):
all_params_present = False
missing_params.append(param)
for param in intent_param_map[intent]["required"]:
if params.get(param) is None:
all_params_present = False
missing_params.append(param)
πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 91 to 95, the required-param
check treats any falsy value (like 0 or empty string) as missing because it uses
a truthiness test; change the check to explicitly test for None (e.g., use
params.get(param) is None or param not in params or params[param] is None) so
valid falsy inputs are accepted while truly absent parameters are flagged, and
update missing_params/all_params_present logic accordingly.

# 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:
Comment on lines +111 to +138
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

Do not call FastAPI route handlers directly; they rely on DI (Depends) and will error with unexpected kwargs.

Handlers like get_dashboard_overview expect current_user: User = Depends(...). Direct calls with **api_args will raise TypeError and bypass auth. Extract shared business logic into services and import/call those instead, or invoke via HTTP with proper auth/session.

Suggested approach (high-level):

  • Move logic from route handlers into functions/services that accept explicit params (e.g., brand_id), called by both the route and this AI layer.
  • Keep routes thin, only handling DI and response shaping.

I can draft a refactor plan or patches for specific endpoints if you confirm target service boundaries.

🧰 Tools
πŸͺ› Ruff (0.13.1)

138-138: Do not catch blind exception: Exception

(BLE001)

πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 111-138, the code directly calls
FastAPI route handlers (e.g., get_dashboard_overview) with **api_args which will
break because those handlers use DI (Depends) and auth; instead extract the
underlying business logic into standalone service functions that accept explicit
parameters (e.g., brand_id, filters) and return serializable results, update
each route handler to call the new service and keep DI only in the route, then
import and call those service functions from this AI layer; ensure the service
functions perform auth checks or accept an authenticated user parameter,
preserve the same return shape or adapt ai_query response handling, and
add/adjust tests and error handling accordingly.

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", ""),
Comment on lines +147 to +151
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 | 🟑 Minor

Parenthesize mixed and/or for clarity and correctness.

Current precedence is ambiguous.

-            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)),
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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", ""),
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", ""),
🧰 Tools
πŸͺ› Ruff (0.13.1)

147-147: Parenthesize a and b expressions when chaining and and or together, to make the precedence clear

Parenthesize the and subexpression

(RUF021)

πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 147 to 151, the boolean
expressions mixing and/or need explicit parentheses to enforce the intended
precedence: change follow_up_needed to ((not all_params_present and not
only_optional_params) or (api_error is not None)), change follow_up_question to
(result.get("follow_up_question") if (not all_params_present and not
only_optional_params) else None), and keep the
explanation/original_query/timestamp assignments as-is; this ensures correct
evaluation and removes ambiguity.

)
# 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)
Comment on lines +156 to +175
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 | 🟑 Minor

Ensure session state merge handles non-dict parameters robustly.

If the LLM returns a non-dict parameters, {**prev_params, **result.get("parameters", {})} will raise. Guard with isinstance(..., dict).

-        params = {**prev_params, **result.get("parameters", {})}
+        new_params = result.get("parameters", {}) or {}
+        if not isinstance(new_params, dict):
+            new_params = {}
+        params = {**prev_params, **new_params}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
# 3. Merge new params and intent
# Use new intent if present, else previous
intent = result.get("route") or prev_intent
new_params = result.get("parameters", {}) or {}
if not isinstance(new_params, dict):
new_params = {}
params = {**prev_params, **new_params}
state["params"] = params
state["intent"] = intent
πŸ€– Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 156 to 175, the merge of session
params assumes result.get("parameters") is a dict and will crash if it's not;
guard by ensuring both prev_params and incoming parameters are dicts (e.g., if
not isinstance(prev_params, dict) set prev_params = {} and if not
isinstance(result.get("parameters"), dict) set incoming_params = {}), then merge
using {**prev_params, **incoming_params}; keep the intent selection logic the
same and continue to update state["params"] and state["intent"] before saving.


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