forked from 0xWheatyz/SPARC
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bd9dccdb8 |
+56
-1
@@ -9,7 +9,7 @@ from typing import Annotated, List
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
from slowapi import Limiter
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
@@ -389,6 +389,61 @@ async def get_analytics(
|
||||
)
|
||||
|
||||
|
||||
# ============== Export Endpoints ==============
|
||||
|
||||
|
||||
@app.get("/export/{company_name}", tags=["Export"])
|
||||
async def export_company_csv(
|
||||
company_name: str,
|
||||
_: UserResponse = Depends(get_current_user),
|
||||
):
|
||||
"""Export analysis results for a company as a CSV file.
|
||||
|
||||
Returns all stored analysis records for the given company, including
|
||||
analysis type, model used, response text, and timestamp.
|
||||
|
||||
Args:
|
||||
company_name: Company name to export results for
|
||||
|
||||
Returns:
|
||||
CSV file download
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
|
||||
db = get_db_client()
|
||||
# Query all non-cached analysis results for this company
|
||||
with db.get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT company_name, analysis_type, model, response, timestamp
|
||||
FROM llm_messages
|
||||
WHERE LOWER(company_name) = LOWER(%s) AND is_cached = FALSE
|
||||
ORDER BY timestamp DESC
|
||||
""",
|
||||
(company_name,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if not rows:
|
||||
raise HTTPException(status_code=404, detail=f"No analysis results found for '{company_name}'")
|
||||
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["company_name", "analysis_type", "model", "analysis", "timestamp"])
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
|
||||
output.seek(0)
|
||||
safe_name = company_name.replace(" ", "_").lower()
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f'attachment; filename="sparc_{safe_name}_export.csv"'},
|
||||
)
|
||||
|
||||
|
||||
# ============== System Endpoints ==============
|
||||
|
||||
|
||||
|
||||
+1
-13
@@ -1,4 +1,3 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
@@ -11,8 +10,6 @@ import serpapi
|
||||
from SPARC import config
|
||||
from SPARC.types import Patent, Patents
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SERP:
|
||||
def query(company: str, days_back: int = None) -> Patents:
|
||||
@@ -47,7 +44,6 @@ class SERP:
|
||||
"tbs": date_filter,
|
||||
"api_key": config.api_key,
|
||||
}
|
||||
logger.info("Querying Google Patents for '%s' (last %d days)", company, days_back)
|
||||
search = serpapi.search(params)
|
||||
# Convert results to Patent objects, skipping any without PDF links
|
||||
patent_ids = []
|
||||
@@ -56,10 +52,8 @@ class SERP:
|
||||
pdf_link = patent.get("pdf")
|
||||
if pdf_link:
|
||||
patent_ids.append(Patent(patent_id=patent["publication_number"], pdf_link=pdf_link, summary=None))
|
||||
else:
|
||||
logger.debug("Skipping patent %s (no PDF link)", patent.get("publication_number", "unknown"))
|
||||
# Patents without PDF links are skipped (see docstring for details)
|
||||
|
||||
logger.info("Found %d patents with PDF links for '%s'", len(patent_ids), company)
|
||||
return Patents(patents=patent_ids)
|
||||
|
||||
def save_patents(patent: Patent) -> Patent:
|
||||
@@ -76,13 +70,9 @@ class SERP:
|
||||
os.makedirs("patents", exist_ok=True)
|
||||
|
||||
if not (os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0):
|
||||
logger.info("Downloading PDF for %s", patent.patent_id)
|
||||
response = requests.get(patent.pdf_link)
|
||||
with open(pdf_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
logger.debug("Saved %d bytes to %s", len(response.content), pdf_path)
|
||||
else:
|
||||
logger.debug("Using cached PDF for %s at %s", patent.patent_id, pdf_path)
|
||||
|
||||
patent.pdf_path = pdf_path
|
||||
return patent
|
||||
@@ -100,13 +90,11 @@ class SERP:
|
||||
Dictionary containing all extracted sections
|
||||
"""
|
||||
|
||||
logger.debug("Parsing patent PDF: %s", pdf_path)
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
# Extract all text
|
||||
full_text = ""
|
||||
for page in pdf.pages:
|
||||
full_text += page.extract_text() + "\n"
|
||||
logger.debug("Extracted text from %d pages (%d chars)", len(pdf.pages), len(full_text))
|
||||
|
||||
# Define section patterns (common in patents)
|
||||
sections = {
|
||||
|
||||
@@ -126,6 +126,23 @@ export const analysisApi = {
|
||||
},
|
||||
};
|
||||
|
||||
// Export API
|
||||
export const exportApi = {
|
||||
exportCsv: async (companyName: string): Promise<void> => {
|
||||
const response = await api.get(`/export/${encodeURIComponent(companyName)}`, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `sparc_${companyName.toLowerCase().replace(/\s+/g, '_')}_export.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
};
|
||||
|
||||
// Analytics API
|
||||
export const analyticsApi = {
|
||||
getAnalytics: async (days = 30): Promise<Analytics> => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { analysisApi } from '../api/client';
|
||||
import { Search, CheckCircle, AlertCircle, Clock, FileText } from 'lucide-react';
|
||||
import { analysisApi, exportApi } from '../api/client';
|
||||
import { Search, CheckCircle, AlertCircle, Clock, FileText, Download } from 'lucide-react';
|
||||
import type { CompanyAnalysis } from '../types';
|
||||
|
||||
export function Analysis() {
|
||||
@@ -106,9 +106,18 @@ export function Analysis() {
|
||||
{/* Analysis Content */}
|
||||
{result.success && result.analysis && (
|
||||
<div className="bg-bg-card/60 backdrop-blur-lg border border-primary/15 rounded-2xl p-6">
|
||||
<h3 className="text-lg font-semibold text-text-primary border-b-2 border-primary/30 pb-2 mb-4">
|
||||
<div className="flex items-center justify-between border-b-2 border-primary/30 pb-2 mb-4">
|
||||
<h3 className="text-lg font-semibold text-text-primary">
|
||||
AI Analysis Results
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => exportApi.exportCsv(result.company_name)}
|
||||
className="flex items-center gap-2 text-sm bg-primary/20 hover:bg-primary/30 text-primary font-medium px-3 py-1.5 rounded-lg transition-colors"
|
||||
>
|
||||
<Download size={14} />
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div className="text-text-primary whitespace-pre-wrap leading-relaxed">
|
||||
{result.analysis}
|
||||
|
||||
Reference in New Issue
Block a user