alexrs commited on
Commit
dd43fdc
·
1 Parent(s): 7570c1d

Initial commit

Browse files
Files changed (6) hide show
  1. .gitignore +4 -0
  2. README.md +14 -1
  3. app.py +546 -0
  4. img/invoice-1.jpg +3 -0
  5. requirements.txt +2 -0
  6. style.css +394 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .Python
README.md CHANGED
@@ -8,6 +8,19 @@ sdk_version: 6.14.0
8
  python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
8
  python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
+ models:
12
+ - CohereLabs/command-a-plus-05-2026
13
  ---
14
 
15
+ # Command A+
16
+
17
+ Hugging Face Space for **Command A+**
18
+
19
+ ## Local run
20
+
21
+ ```bash
22
+ python -m venv .venv && source .venv/bin/activate
23
+ pip install -r requirements.txt
24
+ export COHERE_API_KEY=...
25
+ python app.py
26
+ ```
app.py ADDED
@@ -0,0 +1,546 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hugging Face Gradio Space: Command A+ multimodal chat demo."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import logging
7
+ import mimetypes
8
+ import os
9
+ import re
10
+ from collections.abc import Iterator
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import gradio as gr
15
+ from cohere import ClientV2
16
+ from cohere.core.api_error import ApiError
17
+
18
+ APP_ROOT = Path(__file__).resolve().parent
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ DEFAULT_MODEL_ID = "command-a-plus-05-2026"
23
+ DEFAULT_TEMPERATURE = 0.2
24
+ IMAGE_DETAIL = "auto"
25
+ MAX_IMAGES_PER_REQUEST = 20
26
+ MAX_TOTAL_IMAGE_BYTES = 20 * 1024 * 1024
27
+ MAX_TOTAL_IMAGE_LABEL = f"{MAX_TOTAL_IMAGE_BYTES // (1024 * 1024)} MB"
28
+ IMAGE_MIME_TYPES = {"image/gif", "image/jpeg", "image/png", "image/webp"}
29
+ THINKING_BLOCK_RE = re.compile(r"<\s*think\s*>.*?<\s*/\s*think\s*>", re.IGNORECASE | re.DOTALL)
30
+
31
+ model_id = os.getenv("COMMAND_A_PLUS_MODEL_ID", DEFAULT_MODEL_ID).strip() or DEFAULT_MODEL_ID
32
+
33
+ api_key = os.getenv("COHERE_API_KEY", "").strip()
34
+ client: ClientV2 | None
35
+ if api_key:
36
+ client = ClientV2(api_key=api_key, client_name="hf-command-a-plus-05-2026")
37
+ else:
38
+ client = None
39
+ logger.warning("COHERE_API_KEY is not set; inference is disabled until configured.")
40
+
41
+ APP_THEME = gr.themes.Soft(
42
+ primary_hue="stone",
43
+ secondary_hue="green",
44
+ neutral_hue="stone",
45
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
46
+ ).set(
47
+ body_background_fill="#ffffff",
48
+ body_background_fill_dark="#07110f",
49
+ body_text_color="#212121",
50
+ body_text_color_dark="#f7f5ef",
51
+ body_text_color_subdued="#75758a",
52
+ body_text_color_subdued_dark="#b9b8ad",
53
+ block_background_fill="#ffffff",
54
+ block_background_fill_dark="#0d1714",
55
+ block_border_color="#d9d9dd",
56
+ block_border_color_dark="rgba(238, 236, 231, 0.22)",
57
+ block_label_text_color="#17171c",
58
+ block_label_text_color_dark="#f7f5ef",
59
+ input_background_fill="#ffffff",
60
+ input_background_fill_dark="#07110f",
61
+ input_border_color="#d9d9dd",
62
+ input_border_color_dark="rgba(238, 236, 231, 0.28)",
63
+ button_primary_background_fill="#17171c",
64
+ button_primary_background_fill_dark="#f7f5ef",
65
+ button_primary_background_fill_hover="#003c33",
66
+ button_primary_background_fill_hover_dark="#edfce9",
67
+ button_primary_text_color="#ffffff",
68
+ button_primary_text_color_dark="#07110f",
69
+ link_text_color="#003c33",
70
+ link_text_color_dark="#7fd3b0",
71
+ )
72
+
73
+ def _extract_content_parts(content: object) -> tuple[str, str]:
74
+ """Extract visible text and thinking text from Cohere content shapes."""
75
+ if content is None:
76
+ return "", ""
77
+ if isinstance(content, str):
78
+ return content, ""
79
+ if isinstance(content, list):
80
+ parts = [_extract_content_parts(block) for block in content]
81
+ return "".join(text for text, _ in parts), "".join(thinking for _, thinking in parts)
82
+ if isinstance(content, dict):
83
+ text = str(content.get("text") or "")
84
+ thinking = str(content.get("thinking") or "")
85
+ if not text and not thinking and "content" in content:
86
+ return _extract_content_parts(content.get("content"))
87
+ return text, thinking
88
+
89
+ text = getattr(content, "text", None)
90
+ thinking = getattr(content, "thinking", None)
91
+ return (str(text) if text is not None else ""), (str(thinking) if thinking is not None else "")
92
+
93
+
94
+ def _extract_text(content: object) -> str:
95
+ return _extract_content_parts(content)[0]
96
+
97
+
98
+ def _strip_thinking_blocks(text: str) -> str:
99
+ return THINKING_BLOCK_RE.sub("", text).strip()
100
+
101
+
102
+ def _format_response(output: str, thinking: str) -> str:
103
+ thinking = thinking.strip()
104
+ if not thinking:
105
+ return output
106
+ if not output:
107
+ return f"<think>{thinking}</think>"
108
+ return f"<think>{thinking}</think>\n\n{output}"
109
+
110
+
111
+ def _file_path_or_url(file_value: object) -> str | None:
112
+ if isinstance(file_value, str):
113
+ return file_value
114
+ if isinstance(file_value, dict):
115
+ raw_value = file_value.get("path") or file_value.get("name") or file_value.get("url")
116
+ return str(raw_value) if raw_value else None
117
+ path = getattr(file_value, "path", None)
118
+ return str(path) if path else None
119
+
120
+
121
+ def _guess_mime_type(path_or_url: str, file_value: object) -> str:
122
+ guess_from = path_or_url
123
+ if isinstance(file_value, dict):
124
+ guess_from = str(file_value.get("orig_name") or file_value.get("name") or path_or_url)
125
+ return mimetypes.guess_type(guess_from)[0] or "image/png"
126
+
127
+
128
+ def _data_url_decoded_size(url: str) -> int:
129
+ """Best-effort size estimate for a `data:` URL payload (base64 or percent-encoded)."""
130
+ _, _, payload = url.partition(",")
131
+ if not payload:
132
+ return 0
133
+ head = url.split(",", 1)[0]
134
+ if ";base64" in head:
135
+ padding = payload.count("=")
136
+ return max(0, (len(payload) * 3) // 4 - padding)
137
+ return len(payload)
138
+
139
+
140
+ class _ImageBudget:
141
+ """Enforce the Cohere API per-request image count and total-byte limits."""
142
+
143
+ def __init__(self) -> None:
144
+ self.count = 0
145
+ self.bytes = 0
146
+
147
+ def add(self, size: int) -> None:
148
+ self.count += 1
149
+ if self.count > MAX_IMAGES_PER_REQUEST:
150
+ raise gr.Error(
151
+ f"This conversation exceeds the {MAX_IMAGES_PER_REQUEST}-image limit per request. "
152
+ "Start a new chat or remove some images."
153
+ )
154
+ self.bytes += max(0, size)
155
+ if self.bytes > MAX_TOTAL_IMAGE_BYTES:
156
+ raise gr.Error(
157
+ f"Total image data exceeds {MAX_TOTAL_IMAGE_LABEL} per request. "
158
+ "Use smaller images or fewer attachments."
159
+ )
160
+
161
+
162
+ def _image_url_block(
163
+ file_value: object,
164
+ budget: _ImageBudget,
165
+ *,
166
+ required: bool,
167
+ ) -> dict[str, Any] | None:
168
+ """Convert a Gradio file value into Cohere image_url content."""
169
+ path_or_url = _file_path_or_url(file_value)
170
+ if not path_or_url:
171
+ if required:
172
+ raise gr.Error("The uploaded image could not be read. Try uploading again.")
173
+ return None
174
+
175
+ if path_or_url.startswith(("http://", "https://")):
176
+ # Remote URLs: size is unknown client-side; count toward image cap only.
177
+ budget.add(0)
178
+ return {
179
+ "type": "image_url",
180
+ "image_url": {"url": path_or_url, "detail": IMAGE_DETAIL},
181
+ }
182
+
183
+ if path_or_url.startswith("data:"):
184
+ budget.add(_data_url_decoded_size(path_or_url))
185
+ return {
186
+ "type": "image_url",
187
+ "image_url": {"url": path_or_url, "detail": IMAGE_DETAIL},
188
+ }
189
+
190
+ path = Path(path_or_url)
191
+ if not path.is_file():
192
+ if required:
193
+ raise gr.Error("The uploaded image could not be read. Try uploading again.")
194
+ return None
195
+
196
+ mime_type = _guess_mime_type(path_or_url, file_value)
197
+ if mime_type not in IMAGE_MIME_TYPES:
198
+ raise gr.Error(
199
+ "Unsupported attachment. Use PNG, JPEG, WEBP, or non-animated GIF."
200
+ )
201
+
202
+ budget.add(path.stat().st_size)
203
+
204
+ raw = path.read_bytes()
205
+ b64 = base64.standard_b64encode(raw).decode("ascii")
206
+ return {
207
+ "type": "image_url",
208
+ "image_url": {"url": f"data:{mime_type};base64,{b64}", "detail": IMAGE_DETAIL},
209
+ }
210
+
211
+
212
+ def _message_dict_to_blocks(
213
+ message: dict[str, Any] | None,
214
+ budget: _ImageBudget,
215
+ *,
216
+ required_files: bool,
217
+ ) -> list[dict[str, Any]]:
218
+ if not message:
219
+ return []
220
+
221
+ blocks: list[dict[str, Any]] = []
222
+ text = str(message.get("text") or "").strip()
223
+ if text:
224
+ blocks.append({"type": "text", "text": text})
225
+
226
+ files = message.get("files") or []
227
+ if not isinstance(files, list):
228
+ files = [files]
229
+
230
+ for file_value in files:
231
+ image_block = _image_url_block(file_value, budget, required=required_files)
232
+ if image_block:
233
+ blocks.append(image_block)
234
+
235
+ if not text and files:
236
+ blocks.insert(0, {"type": "text", "text": "Please analyze the attached image(s)."})
237
+
238
+ return blocks
239
+
240
+
241
+ def _history_content_to_blocks(content: object, budget: _ImageBudget) -> list[dict[str, Any]]:
242
+ if isinstance(content, str):
243
+ text = _strip_thinking_blocks(content)
244
+ return [{"type": "text", "text": text}] if text else []
245
+
246
+ if isinstance(content, list):
247
+ blocks: list[dict[str, Any]] = []
248
+ for item in content:
249
+ blocks.extend(_history_content_to_blocks(item, budget))
250
+ return blocks
251
+
252
+ if isinstance(content, dict):
253
+ if content.get("path") or content.get("name") or content.get("url"):
254
+ image_block = _image_url_block(content, budget, required=False)
255
+ return [image_block] if image_block else []
256
+
257
+ text = _strip_thinking_blocks(_extract_text(content))
258
+ return [{"type": "text", "text": text}] if text else []
259
+
260
+ text = _strip_thinking_blocks(_extract_text(content))
261
+ return [{"type": "text", "text": text}] if text else []
262
+
263
+
264
+ def _cohere_content_from_blocks(blocks: list[dict[str, Any]]) -> str | list[dict[str, Any]]:
265
+ if len(blocks) == 1 and blocks[0].get("type") == "text":
266
+ return str(blocks[0].get("text") or "")
267
+ return blocks
268
+
269
+
270
+ def _append_history(
271
+ messages: list[dict[str, Any]],
272
+ history: list[dict[str, Any]] | None,
273
+ budget: _ImageBudget,
274
+ ) -> None:
275
+ for item in history or []:
276
+ role = item.get("role") if isinstance(item, dict) else None
277
+ if role not in {"assistant", "user"}:
278
+ continue
279
+
280
+ blocks = _history_content_to_blocks(item.get("content"), budget)
281
+ if not blocks:
282
+ continue
283
+
284
+ if role == "assistant":
285
+ text = "".join(str(block.get("text") or "") for block in blocks if block.get("type") == "text").strip()
286
+ if text:
287
+ messages.append({"role": "assistant", "content": text})
288
+ else:
289
+ messages.append({"role": "user", "content": _cohere_content_from_blocks(blocks)})
290
+
291
+
292
+ def _no_output_note(finish_reason: str) -> str:
293
+ """Friendly message when the stream ended without emitting any visible text."""
294
+ if finish_reason == "MAX_TOKENS":
295
+ return (
296
+ "_The model hit its native output-token cap before producing a final "
297
+ "answer (generated reasoning only). Try a shorter or simpler prompt._"
298
+ )
299
+ if finish_reason == "ERROR":
300
+ return "_The model returned an error before producing an answer. Please try again._"
301
+ if finish_reason == "STOP_SEQUENCE":
302
+ return "_The model stopped at a stop sequence before producing visible text._"
303
+ return (
304
+ f"_The model finished without producing a visible response "
305
+ f"(finish_reason={finish_reason}). Please try again or rephrase._"
306
+ )
307
+
308
+
309
+ def _format_api_error(exc: ApiError) -> str:
310
+ """Turn a Cohere ApiError into a short, user-readable diagnostic."""
311
+ body = exc.body
312
+ if isinstance(body, dict):
313
+ message = body.get("message") or body.get("error") or ""
314
+ body_text = str(message) if message else str(body)
315
+ else:
316
+ body_text = str(body or "").strip()
317
+
318
+ if exc.status_code == 404 and "page not found" in body_text.lower():
319
+ return (
320
+ f"Model `{model_id}` was not found on the Cohere API. "
321
+ "Check the model id or set the `COMMAND_A_PLUS_MODEL_ID` env var."
322
+ )
323
+ if exc.status_code in (401, 403):
324
+ return "Your `COHERE_API_KEY` was rejected. Check the secret in Space settings."
325
+ if exc.status_code == 429:
326
+ return "Rate-limited by the Cohere API. Please wait and try again."
327
+
328
+ return body_text[:240] or f"HTTP {exc.status_code}"
329
+
330
+
331
+ def respond(
332
+ message: dict[str, Any] | None,
333
+ history: list[dict[str, Any]],
334
+ ) -> Iterator[str]:
335
+ """Stream assistant text for a multimodal chat turn."""
336
+ if client is None:
337
+ yield (
338
+ "This Space needs a `COHERE_API_KEY` secret to call the Cohere API. "
339
+ "Add it in Space settings, then refresh the page."
340
+ )
341
+ return
342
+
343
+ messages: list[dict[str, Any]] = []
344
+ budget = _ImageBudget()
345
+ _append_history(messages, history, budget)
346
+
347
+ try:
348
+ current_blocks = _message_dict_to_blocks(message, budget, required_files=True)
349
+ except OSError as exc:
350
+ logger.exception("Failed to read image")
351
+ raise gr.Error("Could not read the image file.") from exc
352
+
353
+ if not current_blocks:
354
+ yield "Send a message or attach an image to start the conversation."
355
+ return
356
+
357
+ messages.append({"role": "user", "content": _cohere_content_from_blocks(current_blocks)})
358
+
359
+ output = ""
360
+ thinking_output = ""
361
+ finish_reason: str | None = None
362
+ event_counts: dict[str, int] = {}
363
+ try:
364
+ stream = client.chat_stream(
365
+ model=model_id,
366
+ messages=messages,
367
+ temperature=DEFAULT_TEMPERATURE,
368
+ thinking={"type": "enabled"},
369
+ )
370
+ for event in stream:
371
+ event_type = getattr(event, "type", None) or "unknown"
372
+ event_counts[event_type] = event_counts.get(event_type, 0) + 1
373
+
374
+ delta = getattr(event, "delta", None)
375
+
376
+ if event_type in ("content-delta", "content-start"):
377
+ msg = getattr(delta, "message", None) if delta is not None else None
378
+ if msg is None:
379
+ continue
380
+ text, thinking = _extract_content_parts(getattr(msg, "content", None))
381
+ if thinking:
382
+ thinking_output += thinking
383
+ yield _format_response(output, thinking_output)
384
+ if text:
385
+ output += text
386
+ yield _format_response(output, thinking_output)
387
+ elif event_type == "message-end":
388
+ # delta carries finish_reason and (sometimes) usage info.
389
+ finish_reason = getattr(delta, "finish_reason", None)
390
+ if finish_reason is None and isinstance(delta, dict):
391
+ finish_reason = delta.get("finish_reason")
392
+ logger.info(
393
+ "Cohere stream ended: finish_reason=%s, output_len=%d, thinking_len=%d, events=%s",
394
+ finish_reason, len(output), len(thinking_output), event_counts,
395
+ )
396
+
397
+ if not output:
398
+ reason_text = (finish_reason or "unknown").upper()
399
+ logger.warning(
400
+ "Stream produced no visible text. finish_reason=%s, thinking_len=%d, events=%s",
401
+ reason_text, len(thinking_output), event_counts,
402
+ )
403
+ note = _no_output_note(reason_text)
404
+ yield _format_response(note, thinking_output)
405
+ except ApiError as exc:
406
+ logger.exception("Cohere API error (status=%s)", exc.status_code)
407
+ detail = _format_api_error(exc)
408
+ gr.Warning(f"Cohere API error ({exc.status_code}). {detail}")
409
+ yield _format_response(output + f"\n\n_Cohere API error_: {detail}", thinking_output)
410
+ except Exception as exc:
411
+ logger.exception("Unexpected error calling Cohere API")
412
+ gr.Warning(f"Unexpected error: {exc}")
413
+ yield _format_response(output + f"\n\n_Unexpected error_: {exc}", thinking_output)
414
+
415
+
416
+ def _example_message(text: str, files: list[str] | None = None) -> dict[str, Any]:
417
+ return {"text": text, "files": files or []}
418
+
419
+
420
+ INVOICE_IMAGE = str(APP_ROOT / "img" / "invoice-1.jpg")
421
+
422
+
423
+ def build_examples() -> tuple[list[dict[str, Any]], list[str]]:
424
+ """Chat starter prompts. Mixes multimodal, reasoning, multilingual, and code tasks."""
425
+ examples = [
426
+ _example_message(
427
+ "What is the total amount of the invoice with and without tax?",
428
+ files=[INVOICE_IMAGE],
429
+ ),
430
+ _example_message(
431
+ "Extract every line item from this invoice as a JSON array with "
432
+ "description, quantity, unit price, and amount.",
433
+ files=[INVOICE_IMAGE],
434
+ ),
435
+ _example_message(
436
+ "```\nX +\n *\n```\n\n"
437
+ "Reason about the above scene depicted in the markdown code block. "
438
+ "If I interchange the locations of * and X, and then I interchange the "
439
+ "locations of * and +, and then I flip the image like a left-right mirror, "
440
+ "which symbol is on the leftmost part of the image?"
441
+ ),
442
+ _example_message(
443
+ "You are running a race and overtake the person at position 76487423. "
444
+ "What place are you in now?"
445
+ ),
446
+ _example_message(
447
+ "Twenty-four red socks and 24 blue socks are lying in a drawer in a dark "
448
+ "room. What is the minimum number of socks I must take out of the drawer "
449
+ "which will guarantee that I have at least two socks of the same color?"
450
+ ),
451
+ _example_message("Explique la théorie de la relativité en français."),
452
+ ]
453
+ labels = [
454
+ "Invoice: totals",
455
+ "Invoice: line items",
456
+ "Symbol reasoning",
457
+ "Overtaking puzzle",
458
+ "Socks in the dark",
459
+ "Relativité en français",
460
+ ]
461
+ return examples, labels
462
+
463
+
464
+ example_rows, example_labels = build_examples()
465
+
466
+ hero_markdown = f"""
467
+ <section class="hero">
468
+ <div class="hero-grid">
469
+ <div>
470
+ <h1>Command A+</h1>
471
+ </div>
472
+ </div>
473
+ <p class="compact-note">Model: <a href="https://huggingface.co/CohereLabs/command-a-plus-05-2026-bf16" target="_blank" rel="noopener noreferrer"><code>{model_id}</code></a> · Up to <code>{MAX_IMAGES_PER_REQUEST}</code> images or <code>{MAX_TOTAL_IMAGE_LABEL}</code> total per request (PNG, JPEG, WEBP, non-animated GIF) · By using this Space you agree to the
474
+ <a href="https://cohere.com/privacy" target="_blank" rel="noopener noreferrer">Cohere Privacy Policy</a>. Images are sent to the Cohere API for processing.</p>
475
+ </section>
476
+ """
477
+
478
+ placeholder_html = """
479
+ <div class="chat-placeholder">
480
+ <div class="placeholder-kicker">Command A+</div>
481
+ <strong>Ask about anything.</strong>
482
+ <span>Drop a document, chart, or photo and start the conversation.</span>
483
+ </div>
484
+ """
485
+
486
+ with gr.Blocks(title="Command A+", fill_height=True) as demo:
487
+ with gr.Column(elem_classes="app-shell"):
488
+ gr.Markdown(hero_markdown, sanitize_html=False)
489
+
490
+ if client is None:
491
+ gr.Markdown(
492
+ (
493
+ '<div class="status-banner"><strong>Configuration required.</strong> '
494
+ "Set the <code>COHERE_API_KEY</code> secret in Space settings to enable generation.</div>"
495
+ ),
496
+ sanitize_html=False,
497
+ )
498
+
499
+ chatbot = gr.Chatbot(
500
+ show_label=False,
501
+ layout="bubble",
502
+ min_height=520,
503
+ height="62vh",
504
+ placeholder=placeholder_html,
505
+ reasoning_tags=[("<think>", "</think>")],
506
+ elem_classes=["command-chatbot"],
507
+ latex_delimiters=[
508
+ {"left": "$$", "right": "$$", "display": True},
509
+ {"left": "\\[", "right": "\\]", "display": True},
510
+ {"left": "\\(", "right": "\\)", "display": False},
511
+ ],
512
+ )
513
+ textbox = gr.MultimodalTextbox(
514
+ file_types=["image"],
515
+ file_count="multiple",
516
+ sources=["upload"],
517
+ placeholder="Message Command A+ or attach images...",
518
+ lines=1,
519
+ max_lines=6,
520
+ show_label=False,
521
+ container=False,
522
+ submit_btn=True,
523
+ stop_btn=True,
524
+ elem_classes=["command-input"],
525
+ )
526
+
527
+ gr.ChatInterface(
528
+ fn=respond,
529
+ multimodal=True,
530
+ chatbot=chatbot,
531
+ textbox=textbox,
532
+ examples=example_rows,
533
+ example_labels=example_labels,
534
+ run_examples_on_click=True,
535
+ cache_examples=False,
536
+ delete_cache=(1800, 1800),
537
+ save_history=True,
538
+ stop_btn=True,
539
+ fill_width=True,
540
+ show_progress="minimal",
541
+ )
542
+
543
+ demo.queue(default_concurrency_limit=2)
544
+
545
+ if __name__ == "__main__":
546
+ demo.launch(theme=APP_THEME, css_paths="style.css")
img/invoice-1.jpg ADDED

Git LFS Details

  • SHA256: 3e0d3d39ef4e7ec354ede3e0a1c3b2cb7238e5f8f34a84a59ed3f10542b35f27
  • Pointer size: 131 Bytes
  • Size of remote file: 316 kB
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ cohere>=5.16.1
2
+ gradio==6.14.0
style.css ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --cohere-black: #000000;
3
+ --cohere-ink: #212121;
4
+ --cohere-primary: #17171c;
5
+ --cohere-green: #003c33;
6
+ --cohere-stone: #eeece7;
7
+ --cohere-pale-green: #edfce9;
8
+ --cohere-hairline: #d9d9dd;
9
+ --cohere-muted: #75758a;
10
+ --cohere-coral: #ff7759;
11
+ --cohere-canvas: #ffffff;
12
+ --cohere-surface: #ffffff;
13
+ --cohere-elevated: #fbfaf7;
14
+ --cohere-input: #ffffff;
15
+ --cohere-user-bg: #17171c;
16
+ --cohere-user-fg: #ffffff;
17
+ --cohere-user-code-bg: rgba(255, 255, 255, 0.12);
18
+ --cohere-user-code-border: rgba(255, 255, 255, 0.2);
19
+ --cohere-button-bg: #17171c;
20
+ --cohere-button-fg: #ffffff;
21
+ --cohere-button-hover: #003c33;
22
+ }
23
+
24
+ @media (prefers-color-scheme: dark) {
25
+ :root {
26
+ --cohere-black: #000000;
27
+ --cohere-ink: #f7f5ef;
28
+ --cohere-primary: #f7f5ef;
29
+ --cohere-green: #7fd3b0;
30
+ --cohere-stone: #202725;
31
+ --cohere-pale-green: #102e28;
32
+ --cohere-hairline: rgba(238, 236, 231, 0.22);
33
+ --cohere-muted: #b9b8ad;
34
+ --cohere-coral: #ff9a84;
35
+ --cohere-canvas: #07110f;
36
+ --cohere-surface: #0d1714;
37
+ --cohere-elevated: #111d19;
38
+ --cohere-input: #07110f;
39
+ --cohere-user-bg: #f7f5ef;
40
+ --cohere-user-fg: #07110f;
41
+ --cohere-user-code-bg: rgba(0, 0, 0, 0.12);
42
+ --cohere-user-code-border: rgba(0, 0, 0, 0.18);
43
+ --cohere-button-bg: #f7f5ef;
44
+ --cohere-button-fg: #07110f;
45
+ --cohere-button-hover: #edfce9;
46
+ }
47
+ }
48
+
49
+ :root.dark,
50
+ .dark,
51
+ body.dark,
52
+ .gradio-container.dark,
53
+ [data-theme="dark"] {
54
+ --cohere-black: #000000;
55
+ --cohere-ink: #f7f5ef;
56
+ --cohere-primary: #f7f5ef;
57
+ --cohere-green: #7fd3b0;
58
+ --cohere-stone: #202725;
59
+ --cohere-pale-green: #102e28;
60
+ --cohere-hairline: rgba(238, 236, 231, 0.22);
61
+ --cohere-muted: #b9b8ad;
62
+ --cohere-coral: #ff9a84;
63
+ --cohere-canvas: #07110f;
64
+ --cohere-surface: #0d1714;
65
+ --cohere-elevated: #111d19;
66
+ --cohere-input: #07110f;
67
+ --cohere-user-bg: #f7f5ef;
68
+ --cohere-user-fg: #07110f;
69
+ --cohere-user-code-bg: rgba(0, 0, 0, 0.12);
70
+ --cohere-user-code-border: rgba(0, 0, 0, 0.18);
71
+ --cohere-button-bg: #f7f5ef;
72
+ --cohere-button-fg: #07110f;
73
+ --cohere-button-hover: #edfce9;
74
+ }
75
+
76
+ .gradio-container {
77
+ color-scheme: light dark;
78
+ background: var(--cohere-canvas);
79
+ color: var(--cohere-ink);
80
+ }
81
+
82
+ .app-shell {
83
+ max-width: 1120px;
84
+ margin: 0 auto;
85
+ padding: 0 0.75rem 2rem;
86
+ }
87
+
88
+ @media (min-width: 768px) {
89
+ .app-shell {
90
+ padding: 0 1.25rem 2.5rem;
91
+ }
92
+ }
93
+
94
+ .hero {
95
+ margin: 0 0 1rem;
96
+ padding: 1rem 0 1.15rem;
97
+ border-bottom: 1px solid var(--cohere-hairline);
98
+ }
99
+
100
+ .hero-grid {
101
+ display: grid;
102
+ gap: 1rem;
103
+ }
104
+
105
+ @media (min-width: 820px) {
106
+ .hero-grid {
107
+ grid-template-columns: minmax(0, 1fr) minmax(320px, 1fr);
108
+ align-items: end;
109
+ }
110
+ }
111
+
112
+ .gradio-container .hero h1 {
113
+ margin: 0;
114
+ color: var(--cohere-primary) !important;
115
+ font-size: clamp(2.25rem, 11.5vw, 5.25rem);
116
+ font-weight: 500;
117
+ letter-spacing: -0.065em;
118
+ line-height: 0.95;
119
+ white-space: nowrap;
120
+ }
121
+
122
+ .hero-lead {
123
+ max-width: 650px;
124
+ margin: 0 !important;
125
+ color: var(--cohere-ink) !important;
126
+ font-size: clamp(1rem, 2.3vw, 1.18rem);
127
+ line-height: 1.42;
128
+ }
129
+
130
+ .compact-note {
131
+ margin: 0.9rem 0 0 !important;
132
+ color: var(--cohere-muted) !important;
133
+ font-size: 0.82rem;
134
+ line-height: 1.55;
135
+ }
136
+
137
+ .compact-note a {
138
+ color: var(--cohere-green) !important;
139
+ text-decoration: underline;
140
+ text-underline-offset: 0.16em;
141
+ }
142
+
143
+ .gradio-container code:not(pre code) {
144
+ border: 1px solid var(--cohere-hairline) !important;
145
+ border-radius: 0.35rem !important;
146
+ background: var(--cohere-stone) !important;
147
+ color: var(--cohere-primary) !important;
148
+ padding: 0.04rem 0.3rem !important;
149
+ font-size: 0.86em;
150
+ }
151
+
152
+ .status-banner {
153
+ margin: 0 0 1rem;
154
+ padding: 0.85rem 1rem;
155
+ border: 1px solid rgba(255, 119, 89, 0.55);
156
+ border-radius: 16px;
157
+ background: rgba(255, 119, 89, 0.1);
158
+ color: var(--cohere-primary);
159
+ }
160
+
161
+ .status-banner strong {
162
+ color: var(--cohere-primary);
163
+ }
164
+
165
+ .command-chatbot {
166
+ overflow: hidden;
167
+ border: 1px solid var(--cohere-hairline) !important;
168
+ border-radius: 22px !important;
169
+ background: linear-gradient(180deg, var(--cohere-surface) 0%, var(--cohere-elevated) 100%) !important;
170
+ }
171
+
172
+ .command-chatbot .bubble-wrap,
173
+ .command-chatbot .message-wrap,
174
+ .command-chatbot .message {
175
+ border-radius: 18px !important;
176
+ }
177
+
178
+ .command-chatbot .message.user,
179
+ .command-chatbot [data-testid="user"] .message {
180
+ background: var(--cohere-user-bg) !important;
181
+ color: var(--cohere-user-fg) !important;
182
+ }
183
+
184
+ .command-chatbot .message.user *,
185
+ .command-chatbot [data-testid="user"] .message *,
186
+ .command-chatbot .user .message *,
187
+ .command-chatbot .user-message *,
188
+ .command-chatbot .user-message * {
189
+ color: var(--cohere-user-fg) !important;
190
+ }
191
+
192
+ .command-chatbot .message.bot,
193
+ .command-chatbot [data-testid="bot"] .message {
194
+ border: 1px solid var(--cohere-hairline) !important;
195
+ background: var(--cohere-surface) !important;
196
+ color: var(--cohere-ink) !important;
197
+ }
198
+
199
+ .command-chatbot .message pre {
200
+ margin: 0.5rem 0 !important;
201
+ padding: 0.65rem 0.8rem !important;
202
+ border-radius: 10px !important;
203
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
204
+ font-size: 0.88em !important;
205
+ line-height: 1.45 !important;
206
+ overflow-x: auto !important;
207
+ white-space: pre !important;
208
+ }
209
+
210
+ .command-chatbot .message pre code {
211
+ background: transparent !important;
212
+ border: 0 !important;
213
+ padding: 0 !important;
214
+ font-size: inherit !important;
215
+ color: inherit !important;
216
+ }
217
+
218
+ .command-chatbot .message.bot pre,
219
+ .command-chatbot [data-testid="bot"] .message pre {
220
+ border: 1px solid var(--cohere-hairline) !important;
221
+ background: var(--cohere-stone) !important;
222
+ color: var(--cohere-primary) !important;
223
+ }
224
+
225
+ .command-chatbot .message.user pre,
226
+ .command-chatbot [data-testid="user"] .message pre {
227
+ border: 1px solid var(--cohere-user-code-border) !important;
228
+ background: var(--cohere-user-code-bg) !important;
229
+ color: var(--cohere-user-fg) !important;
230
+ }
231
+
232
+ .command-chatbot .message.user :not(pre) > code,
233
+ .command-chatbot [data-testid="user"] .message :not(pre) > code {
234
+ border: 1px solid var(--cohere-user-code-border) !important;
235
+ background: var(--cohere-user-code-bg) !important;
236
+ color: var(--cohere-user-fg) !important;
237
+ }
238
+
239
+ .command-chatbot details {
240
+ margin-bottom: 0.65rem;
241
+ border: 1px solid var(--cohere-hairline);
242
+ border-radius: 12px;
243
+ background: var(--cohere-elevated);
244
+ }
245
+
246
+ .command-chatbot summary {
247
+ padding: 0.45rem 0.65rem;
248
+ color: var(--cohere-muted);
249
+ cursor: pointer;
250
+ font-size: 0.84rem;
251
+ font-weight: 600;
252
+ }
253
+
254
+ .command-chatbot details[open] {
255
+ padding-bottom: 0.55rem;
256
+ }
257
+
258
+ .command-chatbot details > :not(summary) {
259
+ margin: 0.45rem 0.65rem 0;
260
+ color: var(--cohere-muted);
261
+ font-size: 0.9rem;
262
+ }
263
+
264
+ .chat-placeholder {
265
+ display: grid;
266
+ gap: 0.45rem;
267
+ max-width: 520px;
268
+ margin: 0 auto;
269
+ padding: 1.5rem;
270
+ text-align: center;
271
+ color: var(--cohere-ink);
272
+ }
273
+
274
+ .placeholder-kicker {
275
+ justify-self: center;
276
+ width: fit-content;
277
+ padding: 0.3rem 0.7rem;
278
+ border: 1px solid rgba(0, 60, 51, 0.18);
279
+ border-radius: 999px;
280
+ background: var(--cohere-pale-green);
281
+ color: var(--cohere-green);
282
+ font-size: 0.72rem;
283
+ font-weight: 700;
284
+ letter-spacing: 0.08em;
285
+ text-transform: uppercase;
286
+ }
287
+
288
+ .chat-placeholder strong {
289
+ font-size: clamp(1.3rem, 4vw, 2rem);
290
+ font-weight: 500;
291
+ letter-spacing: -0.035em;
292
+ }
293
+
294
+ .chat-placeholder span {
295
+ color: var(--cohere-muted);
296
+ line-height: 1.45;
297
+ }
298
+
299
+ .command-input {
300
+ margin-top: 0.85rem !important;
301
+ border: 2px solid var(--cohere-primary) !important;
302
+ border-radius: 22px !important;
303
+ background: var(--cohere-input) !important;
304
+ box-shadow: none !important;
305
+ outline: none !important;
306
+ }
307
+
308
+ .gradio-container div:has(> .command-input),
309
+ .gradio-container div:has(.command-input) {
310
+ background: transparent !important;
311
+ box-shadow: none !important;
312
+ outline: none !important;
313
+ }
314
+
315
+ .command-input,
316
+ .command-input > *,
317
+ .command-input > div,
318
+ .command-input [class*="container"],
319
+ .command-input [class*="wrap"],
320
+ .command-input [data-testid] {
321
+ background: var(--cohere-input) !important;
322
+ box-shadow: none !important;
323
+ outline: none !important;
324
+ }
325
+
326
+ .command-input textarea,
327
+ .command-input input,
328
+ .command-input [contenteditable="true"] {
329
+ min-height: 58px !important;
330
+ border-radius: 18px !important;
331
+ background: var(--cohere-input) !important;
332
+ color: var(--cohere-primary) !important;
333
+ font-size: 1rem !important;
334
+ line-height: 1.45 !important;
335
+ box-shadow: none !important;
336
+ outline: none !important;
337
+ }
338
+
339
+ .command-input textarea::placeholder,
340
+ .command-input input::placeholder {
341
+ color: var(--cohere-muted) !important;
342
+ opacity: 1 !important;
343
+ }
344
+
345
+ .command-input button {
346
+ border-radius: 999px !important;
347
+ }
348
+
349
+ .gradio-container button.primary,
350
+ .gradio-container .gr-button-primary {
351
+ border-radius: 999px !important;
352
+ background: var(--cohere-button-bg) !important;
353
+ color: var(--cohere-button-fg) !important;
354
+ }
355
+
356
+ .gradio-container button.primary:hover,
357
+ .gradio-container .gr-button-primary:hover {
358
+ background: var(--cohere-button-hover) !important;
359
+ }
360
+
361
+ .gradio-container button.secondary,
362
+ .gradio-container .gr-button-secondary {
363
+ border-radius: 999px !important;
364
+ }
365
+
366
+ .gradio-container .examples,
367
+ .gradio-container [data-testid="examples"] {
368
+ margin-top: -1.75rem !important;
369
+ margin-bottom: 1rem !important;
370
+ transform: translateY(-0.75rem);
371
+ border: 1px solid var(--cohere-hairline) !important;
372
+ border-radius: 16px !important;
373
+ background: var(--cohere-surface) !important;
374
+ box-shadow: none !important;
375
+ }
376
+
377
+ .gradio-container footer {
378
+ margin-top: 1rem;
379
+ }
380
+
381
+ @media (max-width: 640px) {
382
+ .app-shell {
383
+ padding-right: 0.5rem;
384
+ padding-left: 0.5rem;
385
+ }
386
+
387
+ .hero {
388
+ padding-top: 0.65rem;
389
+ }
390
+
391
+ .command-chatbot {
392
+ border-radius: 16px !important;
393
+ }
394
+ }