Matis082 commited on
Commit
c42f48f
·
0 Parent(s):

Transactable Gradio App

Browse files
Files changed (3) hide show
  1. README.md +33 -0
  2. app.py +219 -0
  3. requirements.txt +2 -0
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Transactable
3
+ emoji: 💰
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 5.0.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Transactable
14
+
15
+ Upload financial documents, analyze spending, and ask questions about your finances.
16
+
17
+ ## Features
18
+
19
+ - **Bulk Upload**: Upload up to 1000 documents at once
20
+ - **AI Analysis**: Extract spending, income, and categorize transactions
21
+ - **Smart Models**: Cost-optimized model routing (uses cheapest model that works)
22
+ - **Background Tasks**: Queue processing for large batches
23
+
24
+ ## Usage
25
+
26
+ 1. Enter your API key (get one at [transactable.dev](https://transactable.dev))
27
+ 2. Upload documents (PDF, images)
28
+ 3. Click Analyze to process
29
+ 4. Ask questions about your data
30
+
31
+ ## API
32
+
33
+ This is a frontend for the Transactable API. Deploy your own backend or use the hosted version.
app.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transactable Gradio App for Hugging Face Spaces."""
2
+
3
+ import asyncio
4
+ import os
5
+
6
+ import gradio as gr
7
+ import httpx
8
+
9
+ # API configuration
10
+ API_BASE_URL = os.getenv("API_BASE_URL", "https://transactable-api-4uhaa7eqfq-uc.a.run.app")
11
+
12
+
13
+ def get_headers(api_key: str):
14
+ headers = {"Content-Type": "application/json"}
15
+ if api_key:
16
+ headers["X-API-Key"] = api_key
17
+ return headers
18
+
19
+
20
+ # =============================================================================
21
+ # API FUNCTIONS
22
+ # =============================================================================
23
+
24
+
25
+ async def upload_files(files, api_key: str, progress=gr.Progress()):
26
+ """Upload files with progress tracking."""
27
+ if not files:
28
+ return "Please select files to upload.", ""
29
+
30
+ if not isinstance(files, list):
31
+ files = [files]
32
+
33
+ total = len(files)
34
+ file_ids = []
35
+ failed = 0
36
+
37
+ semaphore = asyncio.Semaphore(10)
38
+
39
+ async def upload_one(file):
40
+ nonlocal failed
41
+ async with semaphore:
42
+ try:
43
+ async with httpx.AsyncClient(timeout=60.0) as client:
44
+ headers = {"X-API-Key": api_key} if api_key else {}
45
+ with open(file.name, "rb") as f:
46
+ response = await client.post(
47
+ f"{API_BASE_URL}/api/v1/files/upload",
48
+ files={"file": (file.name.split("/")[-1], f)},
49
+ headers=headers,
50
+ )
51
+ if response.status_code == 200:
52
+ return response.json().get("id")
53
+ except Exception:
54
+ pass
55
+ failed += 1
56
+ return None
57
+
58
+ progress(0, desc="Uploading...")
59
+
60
+ batch_size = 50
61
+ for i in range(0, total, batch_size):
62
+ batch = files[i : i + batch_size]
63
+ results = await asyncio.gather(*[upload_one(f) for f in batch])
64
+ file_ids.extend([r for r in results if r])
65
+ progress((i + len(batch)) / total)
66
+
67
+ return (
68
+ f"**Uploaded:** {len(file_ids)} | **Failed:** {failed} | **Total:** {total}",
69
+ ",".join(file_ids),
70
+ )
71
+
72
+
73
+ async def analyze_files(file_ids: str, analysis_type: str, api_key: str, progress=gr.Progress()):
74
+ """Analyze uploaded files."""
75
+ if not file_ids:
76
+ return "Upload files first."
77
+
78
+ ids = [fid.strip() for fid in file_ids.split(",") if fid.strip()]
79
+ total = len(ids)
80
+
81
+ if total == 0:
82
+ return "No file IDs."
83
+
84
+ results = {"success": 0, "failed": 0, "spending": 0, "categories": {}}
85
+ semaphore = asyncio.Semaphore(5)
86
+
87
+ async def analyze_one(file_id):
88
+ async with semaphore:
89
+ try:
90
+ async with httpx.AsyncClient(timeout=180.0) as client:
91
+ response = await client.post(
92
+ f"{API_BASE_URL}/api/v1/files/{file_id}/analyze",
93
+ json={"analysis_type": analysis_type},
94
+ headers=get_headers(api_key),
95
+ )
96
+ if response.status_code == 200:
97
+ return response.json()
98
+ except Exception:
99
+ pass
100
+ return None
101
+
102
+ progress(0, desc="Analyzing...")
103
+
104
+ batch_size = 20
105
+ for i in range(0, total, batch_size):
106
+ batch = ids[i : i + batch_size]
107
+ batch_results = await asyncio.gather(*[analyze_one(fid) for fid in batch])
108
+
109
+ for r in batch_results:
110
+ if r:
111
+ results["success"] += 1
112
+ results["spending"] += r.get("total_spending", 0) or 0
113
+ for cat, amt in (r.get("categories") or {}).items():
114
+ results["categories"][cat] = results["categories"].get(cat, 0) + (amt or 0)
115
+ else:
116
+ results["failed"] += 1
117
+
118
+ progress((i + len(batch)) / total)
119
+
120
+ output = f"""## Analysis Complete
121
+
122
+ **✓ Success:** {results['success']} | **✗ Failed:** {results['failed']}
123
+ **Total Spending:** ${results['spending']:,.2f}
124
+
125
+ ### Top Categories
126
+ """
127
+ for cat, amt in sorted(results["categories"].items(), key=lambda x: -x[1])[:10]:
128
+ output += f"- {cat}: ${amt:,.2f}\n"
129
+
130
+ return output
131
+
132
+
133
+ async def ask_question(question: str, conversation_id: str, api_key: str):
134
+ """Ask a question about documents."""
135
+ if not question.strip():
136
+ return "Enter a question.", conversation_id
137
+
138
+ async with httpx.AsyncClient(timeout=60.0) as client:
139
+ payload = {"question": question}
140
+ if conversation_id:
141
+ payload["conversation_id"] = conversation_id
142
+
143
+ response = await client.post(
144
+ f"{API_BASE_URL}/api/v1/ask",
145
+ json=payload,
146
+ headers=get_headers(api_key),
147
+ )
148
+
149
+ if response.status_code == 200:
150
+ data = response.json()
151
+ return data.get("answer", "No answer."), data.get("conversation_id", conversation_id)
152
+ return f"Error: {response.status_code}", conversation_id
153
+
154
+
155
+ # =============================================================================
156
+ # GRADIO UI
157
+ # =============================================================================
158
+
159
+ with gr.Blocks(title="Transactable", theme=gr.themes.Soft()) as app:
160
+ gr.Markdown(
161
+ """
162
+ # 💰 Transactable
163
+ Upload financial documents, analyze spending, and ask questions.
164
+ """
165
+ )
166
+
167
+ with gr.Row():
168
+ api_key = gr.Textbox(
169
+ label="API Key",
170
+ placeholder="Enter your API key",
171
+ type="password",
172
+ scale=3,
173
+ )
174
+
175
+ with gr.Tabs():
176
+ # Upload Tab
177
+ with gr.TabItem("📤 Upload & Analyze"):
178
+ with gr.Row():
179
+ with gr.Column():
180
+ files = gr.File(
181
+ label="Documents (PDF, PNG, JPG)",
182
+ file_count="multiple",
183
+ file_types=[".pdf", ".png", ".jpg", ".jpeg"],
184
+ )
185
+ upload_btn = gr.Button("⬆️ Upload", variant="primary")
186
+
187
+ with gr.Column():
188
+ upload_status = gr.Markdown("Select files and click Upload.")
189
+ file_ids = gr.Textbox(label="File IDs", lines=2)
190
+
191
+ upload_btn.click(upload_files, [files, api_key], [upload_status, file_ids])
192
+
193
+ gr.Markdown("---")
194
+
195
+ with gr.Row():
196
+ analysis_type = gr.Dropdown(
197
+ ["spending", "income", "general"],
198
+ value="spending",
199
+ label="Analysis Type",
200
+ )
201
+ analyze_btn = gr.Button("🔍 Analyze All", variant="primary")
202
+
203
+ analysis_result = gr.Markdown()
204
+ analyze_btn.click(analyze_files, [file_ids, analysis_type, api_key], analysis_result)
205
+
206
+ # Q&A Tab
207
+ with gr.TabItem("💬 Ask"):
208
+ conversation = gr.State(value=None)
209
+ question = gr.Textbox(label="Question", placeholder="What was my total spending?")
210
+ ask_btn = gr.Button("Ask", variant="primary")
211
+ answer = gr.Markdown()
212
+
213
+ ask_btn.click(ask_question, [question, conversation, api_key], [answer, conversation])
214
+ question.submit(ask_question, [question, conversation, api_key], [answer, conversation])
215
+
216
+ gr.Markdown("---\n*Powered by [Transactable API](https://github.com/transactable)*")
217
+
218
+ if __name__ == "__main__":
219
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=5.0.0
2
+ httpx>=0.28.0