Skip to content
Merged
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
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from app.routers import salesforce_router
from app.routers import ga4_router
from app.routers import intercom_router
from app.routers import github_analytics_router
from app.models import User
from app.auth import hash_password, GUEST_USER_ID, ADMIN_USER_ID

Expand Down Expand Up @@ -251,6 +252,7 @@ async def httpx_connect_error_handler(_request: Request, exc: httpx.ConnectError
app.include_router(salesforce_router.router, prefix=prefix)
app.include_router(ga4_router.router, prefix=prefix)
app.include_router(intercom_router.router, prefix=prefix)
app.include_router(github_analytics_router.router, prefix=prefix)
app.include_router(pipedrive_router.router, prefix=prefix)


Expand Down
22 changes: 22 additions & 0 deletions backend/app/routers/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from app.scripts.ask_stripe import ask_stripe
from app.scripts.ask_pipedrive import ask_pipedrive
from app.scripts.ask_ga4 import ask_ga4
from app.scripts.ask_github_analytics import ask_github_analytics
from app.scripts.ask_intercom import ask_intercom
from app.scripts.ask_salesforce import ask_salesforce
from app.scripts.ask_sql import ask_sql
Expand Down Expand Up @@ -562,6 +563,27 @@ async def dispatch_question(
history=history,
channel=channel,
)
elif source.type == "github_analytics":
meta = source.metadata_ or {}
gh_token = meta.get("token", "")
gh_owner = meta.get("owner", "")
gh_repo = meta.get("repo", "")
if not gh_token or not gh_owner or not gh_repo:
raise HTTPException(400, "GitHub Analytics source missing token, owner, or repo in metadata")
result = await ask_github_analytics(
token=gh_token,
owner=gh_owner,
repo=gh_repo,
question=question,
agent_description=agent.description or "",
source_name=source.name,
tables_data=meta.get("tablesData"),
schema_text=meta.get("schemaText"),
preview=meta.get("preview"),
llm_overrides=llm_overrides,
history=history,
channel=channel,
)
else:
raise HTTPException(400, f"Unsupported source type: {source.type}")

Expand Down
2 changes: 1 addition & 1 deletion backend/app/routers/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ async def create_source(
user: User = Depends(require_user),
):
"""Create a non-file source (BigQuery, Google Sheets, SQL). Credentials stored locally in metadata."""
valid_types = ("bigquery", "google_sheets", "sql_database", "firebase", "mongodb", "snowflake", "notion", "excel_online", "s3", "rest_api", "jira", "hubspot", "stripe", "pipedrive", "salesforce", "ga4", "intercom")
valid_types = ("bigquery", "google_sheets", "sql_database", "firebase", "mongodb", "snowflake", "notion", "excel_online", "s3", "rest_api", "jira", "hubspot", "stripe", "pipedrive", "salesforce", "ga4", "intercom", "github_analytics")
if body.type not in valid_types:
raise HTTPException(400, f"type must be one of: {', '.join(valid_types)}")
source_id = str(uuid.uuid4())
Expand Down
142 changes: 142 additions & 0 deletions backend/app/routers/github_analytics_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""
GitHub Analytics discovery and source metadata refresh.
- Test connection (validate PAT + repo access)
- Discover resources (repo stats, available tables)
- Refresh source metadata (fetch table data + preview rows)
"""
import asyncio
from fastapi import APIRouter, Depends, HTTPException
from app.auth import require_user
from app.models import User, Source
from app.database import get_db
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.routers.crud import _sanitize_for_json

router = APIRouter(prefix="/github-analytics", tags=["github_analytics"])


@router.post("/test-connection")
async def test_connection(
body: dict,
user: User = Depends(require_user),
):
"""Test a GitHub token and repo access. Body: { "token": "...", "owner": "...", "repo": "..." }."""
token = body.get("token")
owner = body.get("owner")
repo = body.get("repo")
if not token:
raise HTTPException(400, "token is required")
if not owner or not repo:
raise HTTPException(400, "owner and repo are required")

from app.scripts.ask_github_analytics import _test_connection_sync

loop = asyncio.get_event_loop()
try:
repo_data = await loop.run_in_executor(
None, lambda: _test_connection_sync(token, owner, repo)
)
return {"ok": True, "repoName": repo_data.get("full_name", "")}
except Exception as e:
raise HTTPException(400, f"Connection failed: {e}")


@router.post("/discover")
async def discover_resources(
body: dict,
user: User = Depends(require_user),
):
"""Discover GitHub repo resources. Body: { "token": "...", "owner": "...", "repo": "..." }."""
token = body.get("token")
owner = body.get("owner")
repo = body.get("repo")
if not token:
raise HTTPException(400, "token is required")
if not owner or not repo:
raise HTTPException(400, "owner and repo are required")

from app.scripts.ask_github_analytics import _discover_resources_sync

loop = asyncio.get_event_loop()
try:
resources = await loop.run_in_executor(
None, lambda: _discover_resources_sync(token, owner, repo)
)
except Exception as e:
raise HTTPException(400, str(e))

return resources


@router.post("/sources/{source_id}/refresh-metadata")
async def refresh_source_metadata(
source_id: str,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_user),
):
"""Fetch GitHub data and update source metadata with schema and preview."""
r = await db.execute(
select(Source).where(Source.id == source_id, Source.user_id == user.id)
)
source = r.scalar_one_or_none()
if not source:
raise HTTPException(404, "Source not found")
if source.type != "github_analytics":
raise HTTPException(400, "Source is not GitHub Analytics")

meta = dict(source.metadata_ or {})
token = meta.get("token")
owner = meta.get("owner")
repo = meta.get("repo")

if not token or not owner or not repo:
raise HTTPException(400, "Source missing token, owner, or repo")

from app.scripts.ask_github_analytics import (
_fetch_all_tables_sync,
_tables_to_sqlite,
_get_schema_text,
)

loop = asyncio.get_event_loop()
try:
tables_data = await loop.run_in_executor(
None, lambda: _fetch_all_tables_sync(token, owner, repo)
)
db_path = await loop.run_in_executor(
None, lambda: _tables_to_sqlite(tables_data)
)
schema_text = await loop.run_in_executor(
None, lambda: _get_schema_text(db_path)
)
except Exception as e:
raise HTTPException(400, str(e))
finally:
try:
import os
os.unlink(db_path)
except Exception:
pass

# Build preview from first 5 rows of each table
preview = {}
table_infos = []
for table_name, rows in tables_data.items():
preview[table_name] = rows[:5]
columns = list(rows[0].keys()) if rows else []
table_infos.append({
"table": table_name,
"row_count": len(rows),
"columns": columns,
})

meta["schema_text"] = schema_text
meta["preview"] = _sanitize_for_json(preview)
meta["table_infos"] = _sanitize_for_json(table_infos)
meta["schema"] = {"tables": [ti["table"] for ti in table_infos]}
source.metadata_ = _sanitize_for_json(meta)
await db.commit()
await db.refresh(source)
return {"metaJSON": source.metadata_}
Loading
Loading