transactable / app.py
Matis082's picture
Transactable Gradio App
c42f48f
"""Transactable Gradio App for Hugging Face Spaces."""
import asyncio
import os
import gradio as gr
import httpx
# API configuration
API_BASE_URL = os.getenv("API_BASE_URL", "https://transactable-api-4uhaa7eqfq-uc.a.run.app")
def get_headers(api_key: str):
headers = {"Content-Type": "application/json"}
if api_key:
headers["X-API-Key"] = api_key
return headers
# =============================================================================
# API FUNCTIONS
# =============================================================================
async def upload_files(files, api_key: str, progress=gr.Progress()):
"""Upload files with progress tracking."""
if not files:
return "Please select files to upload.", ""
if not isinstance(files, list):
files = [files]
total = len(files)
file_ids = []
failed = 0
semaphore = asyncio.Semaphore(10)
async def upload_one(file):
nonlocal failed
async with semaphore:
try:
async with httpx.AsyncClient(timeout=60.0) as client:
headers = {"X-API-Key": api_key} if api_key else {}
with open(file.name, "rb") as f:
response = await client.post(
f"{API_BASE_URL}/api/v1/files/upload",
files={"file": (file.name.split("/")[-1], f)},
headers=headers,
)
if response.status_code == 200:
return response.json().get("id")
except Exception:
pass
failed += 1
return None
progress(0, desc="Uploading...")
batch_size = 50
for i in range(0, total, batch_size):
batch = files[i : i + batch_size]
results = await asyncio.gather(*[upload_one(f) for f in batch])
file_ids.extend([r for r in results if r])
progress((i + len(batch)) / total)
return (
f"**Uploaded:** {len(file_ids)} | **Failed:** {failed} | **Total:** {total}",
",".join(file_ids),
)
async def analyze_files(file_ids: str, analysis_type: str, api_key: str, progress=gr.Progress()):
"""Analyze uploaded files."""
if not file_ids:
return "Upload files first."
ids = [fid.strip() for fid in file_ids.split(",") if fid.strip()]
total = len(ids)
if total == 0:
return "No file IDs."
results = {"success": 0, "failed": 0, "spending": 0, "categories": {}}
semaphore = asyncio.Semaphore(5)
async def analyze_one(file_id):
async with semaphore:
try:
async with httpx.AsyncClient(timeout=180.0) as client:
response = await client.post(
f"{API_BASE_URL}/api/v1/files/{file_id}/analyze",
json={"analysis_type": analysis_type},
headers=get_headers(api_key),
)
if response.status_code == 200:
return response.json()
except Exception:
pass
return None
progress(0, desc="Analyzing...")
batch_size = 20
for i in range(0, total, batch_size):
batch = ids[i : i + batch_size]
batch_results = await asyncio.gather(*[analyze_one(fid) for fid in batch])
for r in batch_results:
if r:
results["success"] += 1
results["spending"] += r.get("total_spending", 0) or 0
for cat, amt in (r.get("categories") or {}).items():
results["categories"][cat] = results["categories"].get(cat, 0) + (amt or 0)
else:
results["failed"] += 1
progress((i + len(batch)) / total)
output = f"""## Analysis Complete
**✓ Success:** {results['success']} | **✗ Failed:** {results['failed']}
**Total Spending:** ${results['spending']:,.2f}
### Top Categories
"""
for cat, amt in sorted(results["categories"].items(), key=lambda x: -x[1])[:10]:
output += f"- {cat}: ${amt:,.2f}\n"
return output
async def ask_question(question: str, conversation_id: str, api_key: str):
"""Ask a question about documents."""
if not question.strip():
return "Enter a question.", conversation_id
async with httpx.AsyncClient(timeout=60.0) as client:
payload = {"question": question}
if conversation_id:
payload["conversation_id"] = conversation_id
response = await client.post(
f"{API_BASE_URL}/api/v1/ask",
json=payload,
headers=get_headers(api_key),
)
if response.status_code == 200:
data = response.json()
return data.get("answer", "No answer."), data.get("conversation_id", conversation_id)
return f"Error: {response.status_code}", conversation_id
# =============================================================================
# GRADIO UI
# =============================================================================
with gr.Blocks(title="Transactable", theme=gr.themes.Soft()) as app:
gr.Markdown(
"""
# 💰 Transactable
Upload financial documents, analyze spending, and ask questions.
"""
)
with gr.Row():
api_key = gr.Textbox(
label="API Key",
placeholder="Enter your API key",
type="password",
scale=3,
)
with gr.Tabs():
# Upload Tab
with gr.TabItem("📤 Upload & Analyze"):
with gr.Row():
with gr.Column():
files = gr.File(
label="Documents (PDF, PNG, JPG)",
file_count="multiple",
file_types=[".pdf", ".png", ".jpg", ".jpeg"],
)
upload_btn = gr.Button("⬆️ Upload", variant="primary")
with gr.Column():
upload_status = gr.Markdown("Select files and click Upload.")
file_ids = gr.Textbox(label="File IDs", lines=2)
upload_btn.click(upload_files, [files, api_key], [upload_status, file_ids])
gr.Markdown("---")
with gr.Row():
analysis_type = gr.Dropdown(
["spending", "income", "general"],
value="spending",
label="Analysis Type",
)
analyze_btn = gr.Button("🔍 Analyze All", variant="primary")
analysis_result = gr.Markdown()
analyze_btn.click(analyze_files, [file_ids, analysis_type, api_key], analysis_result)
# Q&A Tab
with gr.TabItem("💬 Ask"):
conversation = gr.State(value=None)
question = gr.Textbox(label="Question", placeholder="What was my total spending?")
ask_btn = gr.Button("Ask", variant="primary")
answer = gr.Markdown()
ask_btn.click(ask_question, [question, conversation, api_key], [answer, conversation])
question.submit(ask_question, [question, conversation, api_key], [answer, conversation])
gr.Markdown("---\n*Powered by [Transactable API](https://github.com/transactable)*")
if __name__ == "__main__":
app.launch()