Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions app/api/v1/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,13 @@ def get_active_project(

if project_id is not None:
project = get_project_by_id(supabase, project_id)
if project and project["workspace_id"] == workspace_id and project.get("status") == "active":
if project and project["workspace_id"] == workspace_id and project.get("status") in {"active", None}:
return project

# Get most recent active project
projects = list_projects_for_workspace(supabase, workspace_id)
for project in projects:
if project.get("status") == "active":
if project.get("status") in {"active", None}:
return project
return None

Expand Down
54 changes: 29 additions & 25 deletions app/api/v1/routes/auto_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,30 +176,33 @@ def get_auto_pipeline(

response["results"] = {
"brand_summary": pipeline["brand_summary"] or "",
"personas": [
{"name": p["name"], "role": p.get("role", ""), "summary": p["summary"], "pain_points": p.get("pain_points", [])}
for p in personas
],
"keywords": [
{"keyword": k["keyword"], "score": k.get("priority_score", 0), "source": k.get("source", "")}
for k in keywords
],
"subreddits": [
{"name": s["name"], "fit_score": s.get("fit_score", 0), "subscribers": s.get("subscribers", 0), "description": s.get("description", "")}
for s in subreddits
],
"opportunities": [
{"title": o["title"], "subreddit": o["subreddit_name"], "platform": o.get("platform", "reddit"), "score": o.get("score", 0), "author": o.get("author", "")}
for o in visible_opportunities
],
"drafts": [
{
"title": opportunity_titles.get(d["opportunity_id"], "Reply Draft"),
"opportunity_title": opportunity_titles.get(d["opportunity_id"], "Reply Draft"),
"content": d["content"],
}
for d in drafts
],
"personas": _slice_run_results(
[{"name": p["name"], "role": p.get("role", ""), "summary": p["summary"], "pain_points": p.get("pain_points", [])} for p in personas],
pipeline.get("personas_generated", 0)
),
"keywords": _slice_run_results(
[{"keyword": k["keyword"], "score": k.get("priority_score", 0), "source": k.get("source", "")} for k in keywords],
pipeline.get("keywords_generated", 0)
),
"subreddits": _slice_run_results(
[{"name": s["name"], "fit_score": s.get("fit_score", 0), "subscribers": s.get("subscribers", 0), "description": s.get("description", "")} for s in subreddits],
pipeline.get("subreddits_found", 0)
),
"opportunities": _slice_run_results(
[{"title": o["title"], "subreddit": o["subreddit_name"], "platform": o.get("platform", "reddit"), "score": o.get("score", 0), "author": o.get("author", "")} for o in visible_opportunities],
pipeline.get("opportunities_found", 0)
),
"drafts": _slice_run_results(
[
{
"title": opportunity_titles.get(d["opportunity_id"], "Reply Draft"),
"opportunity_title": opportunity_titles.get(d["opportunity_id"], "Reply Draft"),
"content": d["content"],
}
for d in drafts
],
pipeline.get("drafts_generated", 0)
),
}

return response
Expand All @@ -218,7 +221,8 @@ def list_auto_pipelines(
ensure_workspace_membership(supabase, workspace["id"], current_user["id"])
proj = get_active_project(supabase, workspace["id"], project_id)
if not proj:
raise HTTPException(404, "No active project found. Please create a project first.")
from app.api.v1.deps import ensure_default_project
proj = ensure_default_project(supabase, workspace)

pipelines = list_auto_pipelines_for_project(supabase, proj["id"], limit=limit, offset=offset)

Expand Down
4 changes: 4 additions & 0 deletions app/api/v1/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from supabase import Client

from app.api.v1.deps import (
ensure_default_project,
ensure_workspace_membership,
get_active_project,
get_current_user,
Expand Down Expand Up @@ -82,6 +83,9 @@ def dashboard(
ensure_workspace_membership(supabase, workspace["id"], current_user["id"])

projects = list_projects_for_workspace(supabase, workspace["id"])
if not projects:
default_proj = ensure_default_project(supabase, workspace)
projects = [default_proj]
selected_project = get_active_project(supabase, workspace["id"], project_id)
project_ids = [selected_project["id"]] if selected_project else [p["id"] for p in projects]

Expand Down
28 changes: 14 additions & 14 deletions app/api/v1/routes/scrapers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, status
from pydantic import BaseModel
from supabase import Client

from app.api.v1.deps import get_current_user, get_current_workspace, ensure_workspace_membership
from app.api.v1.deps import ensure_workspace_membership, get_current_user, get_current_workspace
from app.db.supabase_client import get_supabase
from app.db.tables.custom_scrapers import (
delete_custom_scraper,
list_custom_scrapers_for_workspace,
upsert_custom_scraper,
delete_custom_scraper,
)
from pydantic import BaseModel
from app.schemas.v1.scrapers import CustomScraperResponse, CustomScraperCreateRequest
from app.schemas.v1.scrapers import CustomScraperCreateRequest, CustomScraperResponse
from app.services.infrastructure.llm.service import LLMService

router = APIRouter(prefix="/v1/scrapers", tags=["scrapers"])
Expand Down Expand Up @@ -47,10 +47,10 @@ def create_scraper_endpoint(
) -> CustomScraperResponse:
"""Create or update a custom scraper configuration for a specific platform."""
ensure_workspace_membership(supabase, workspace["id"], current_user["id"])

data = payload.model_dump()
data["workspace_id"] = workspace["id"]

scraper = upsert_custom_scraper(supabase, data)
return CustomScraperResponse.model_validate(scraper)

Expand All @@ -64,10 +64,10 @@ def delete_scraper_endpoint(
) -> None:
"""Delete a custom scraper configuration."""
ensure_workspace_membership(supabase, workspace["id"], current_user["id"])

# Optional: verify the scraper belongs to the workspace before deleting
# RLS handles this mostly, but good practice.

delete_custom_scraper(supabase, scraper_id)


Expand All @@ -90,18 +90,18 @@ def scrapers_chat_endpoint(
"Do NOT give them individual field mappings (like external_id, title, body) because our app uses an autonomous LLM to parse those automatically! "
"ONLY give them the exact values to copy-paste into the 4 form fields. Be extremely clear and simple, assuming zero technical knowledge."
)

# Format messages into a single prompt string since LLMService.call_text expects a string
prompt_lines = []
for msg in payload.history:
role_label = "Assistant" if msg.role == "assistant" else "User"
prompt_lines.append(f"{role_label}: {msg.content}")

prompt_lines.append(f"User: {payload.message}")
prompt_lines.append("Assistant:")

final_prompt = "\n\n".join(prompt_lines)

llm = LLMService()
reply = llm.call_text(
prompt=final_prompt,
Expand All @@ -110,5 +110,5 @@ def scrapers_chat_endpoint(
)
if not reply:
reply = "Sorry, I couldn't process that right now."

return ChatResponse(reply=reply)
97 changes: 81 additions & 16 deletions app/api/v1/routes/visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@
create_prompt_run,
list_prompt_runs_for_prompt_set,
list_prompt_sets_for_project,
update_prompt_run,
)
from app.db.tables.visibility import (
create_prompt_set as create_prompt_set_db,
)
from app.services.infrastructure.llm.providers._registry import get_configured_providers
from app.services.infrastructure.llm.service import VisibilityRunner
from app.services.product.visibility import CitationExtractor, MentionDetector

print("VISIBILITY FILE LOADED")
print(__file__)


logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1", tags=["visibility"])

Expand All @@ -51,7 +57,7 @@ def list_prompt_sets(
"id": s["id"],
"name": s["name"],
"category": s["category"],
"prompts": s.get("prompts", []),
"prompts": s.get("prompts_json", []),
"target_models": s.get("target_models", []),
"is_active": s.get("is_active", True),
"schedule": s.get("schedule", "manual"),
Expand Down Expand Up @@ -80,7 +86,7 @@ def create_prompt_set(
"project_id": proj["id"],
"name": payload.get("name", "Untitled"),
"category": payload.get("category", "general"),
"prompts": payload.get("prompts", []),
"prompts_json": payload.get("prompts", []),
"target_models": payload.get("target_models", ["chatgpt", "perplexity", "gemini", "claude"]),
"schedule": payload.get("schedule", "manual"),
},
Expand Down Expand Up @@ -112,6 +118,16 @@ def run_prompt_set(
# Get prompt set and verify workspace access
from app.db.tables.visibility import get_prompt_set_by_id
ps = get_prompt_set_by_id(supabase, psid)

print("TYPE =", type(ps))
print("PS =", repr(ps))

print("========== PROMPT SET ==========")
print(ps)
print("PROMPTS =", ps.get("prompts"))
print("MODELS =", ps.get("target_models"))
print("================================")

if not ps:
raise HTTPException(404, "Prompt set not found.")

Expand All @@ -128,22 +144,60 @@ def run_prompt_set(
extractor = CitationExtractor()

results = []
for prompt_text in ps.get("prompts", []):
for model in ps.get("target_models", ["chatgpt"]):
pr = create_prompt_run(
supabase,
{
"prompt_set_id": ps["id"],
"model_name": model,
"prompt_text": prompt_text,
"status": "running",
},
)
available = get_configured_providers()

print("PROMPTS:", ps.get("prompts"))
print("MODELS:", ps.get("target_models"))
print("AVAILABLE:", get_configured_providers())
print("PROMPTS TYPE:", type(ps.get("prompts")))

prompts = ps.get("prompts") or []
models = ps.get("target_models") or ["chatgpt"]

print("PROMPTS LEN:", len(prompts))
print("MODELS LEN:", len(models))

for prompt_text in prompts:
print("ENTERED PROMPT LOOP:", prompt_text)

for model in models:
print("ENTERED MODEL LOOP:", model)

if model not in available:
results.append({
"prompt": prompt_text[:80],
"model": model,
"brand_mentioned": False,
"citations": 0,
"error": True,
"error_message": f"Provider '{model}' is not configured",
})
continue

print("RUN_PROMPT_SET NEW VERSION")
print(ps)

payload = {
"prompt_set_id": ps["id"],
"project_id": proj["id"],
"model_name": model,
"provider": model,
"status": "running",
}

print("PROMPT RUN PAYLOAD =", payload)

pr = create_prompt_run(supabase, payload)

response_text = runner.run_prompt(prompt_text, model)
print("===================================")
print("PROMPT =", prompt_text)
print("MODEL =", model)
print("RAW RESPONSE =", repr(response_text))
print("TYPE =", type(response_text))
print("===================================")
if response_text:
# Update prompt run as complete
from app.db.tables.visibility import update_prompt_run
update_prompt_run(
supabase,
pr["id"],
Expand Down Expand Up @@ -314,18 +368,29 @@ def visibility_prompt_results(
all_responses = list_ai_responses_for_runs(supabase, run_ids)
ai_responses_by_run = {resp["prompt_run_id"]: resp for resp in all_responses}

prompt_sets = list_prompt_sets_for_project(supabase, proj["id"])
prompt_map = {
p["id"]: p
for p in prompt_sets
}
items = []

for r in runs:
resp = ai_responses_by_run.get(r["id"])
ps = prompt_map.get(r["prompt_set_id"])
prompt_text = ""
prompts = ps.get("prompts") or ps.get("prompts_json") or []
if prompts:
prompt_text = prompts[0]
items.append({
"id": r["id"],
"prompt_text": r["prompt_text"],
"prompt_text": prompt_text,
"model_name": r["model_name"],
"status": r["status"],
"brand_mentioned": resp["brand_mentioned"] if resp else False,
"competitor_mentions": resp.get("competitor_mentions", []) if resp else [],
"sentiment": resp.get("sentiment") if resp else None,
"citations_count": 0, # Would need another batch query if needed
"citations_count": 0,
"completed_at": r.get("completed_at"),
})

Expand Down
9 changes: 1 addition & 8 deletions app/db/tables/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@


def _map_user(user: dict[str, Any] | None) -> dict[str, Any] | None:
if user and "supabase_user_id" in user:
user = dict(user) # copy to avoid mutating stored row (fixes mock aliasing)
user["supabase_uid"] = user.pop("supabase_user_id")
return user


Expand All @@ -26,7 +23,7 @@ def get_user_by_id(db: Client, user_id: int) -> dict[str, Any] | None:

def get_user_by_supabase_id(db: Client, supabase_user_id: str) -> dict[str, Any] | None:
"""Get a user by Supabase user ID."""
result = db.table(USERS_TABLE).select("*").eq("supabase_user_id", supabase_user_id).execute()
result = db.table(USERS_TABLE).select("*").eq("supabase_uid", supabase_user_id).execute()
return _map_user(result.data[0]) if result.data else None


Expand All @@ -39,17 +36,13 @@ def get_user_by_email(db: Client, email: str) -> dict[str, Any] | None:
def create_user(db: Client, user_data: dict[str, Any]) -> dict[str, Any]:
"""Create a new user."""
data = dict(user_data)
if "supabase_uid" in data:
data["supabase_user_id"] = data.pop("supabase_uid")
result = db.table(USERS_TABLE).insert(data).execute()
return _map_user(result.data[0]) # type: ignore


def update_user(db: Client, user_id: int, update_data: dict[str, Any]) -> dict[str, Any] | None:
"""Update a user."""
data = dict(update_data)
if "supabase_uid" in data:
data["supabase_user_id"] = data.pop("supabase_uid")
result = db.table(USERS_TABLE).update(data).eq("id", user_id).execute()
return _map_user(result.data[0]) if result.data else None

Expand Down
Loading