Download agent/main.py from SUPER321/anam_code: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/SUPER321/anam_code/resolve/main/agent/main.py
- Command line
-
hf download hf://spaces/SUPER321/anam_code/agent/main.py
-
curl -L -o main.py https://huggingface.co/spaces/SUPER321/anam_code/resolve/main/agent/main.py
10.2 kB
| import asyncio | |
| import json | |
| import uvicorn | |
| from fastapi import FastAPI, File, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import HTMLResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from typing import Any, List, Optional | |
| import blood_rag | |
| import reports | |
| import tools as notepad | |
| from agent import run_agent, stream_agent | |
| from config import PORT | |
| app = FastAPI(title="Agentic Notepad Server") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ OpenAI-compatible request types βββββββββββββββββββββββββββββββββββββββββββ | |
| class Message(BaseModel): | |
| role: str | |
| content: Any | |
| class ChatRequest(BaseModel): | |
| model: Optional[str] = None # None β fall back to LLM_MODEL in agent config | |
| messages: List[Message] | |
| stream: bool = False | |
| temperature: Optional[float] = None | |
| # ββ /v1/chat/completions βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def chat_completions(req: ChatRequest): | |
| msgs = [m.model_dump() for m in req.messages] | |
| model = req.model or None # pass None β agent uses LLM_MODEL from config | |
| print(f"[agent] βΆ using model: {model or 'default β ' + __import__('config').LLM_MODEL}", flush=True) | |
| if req.stream: | |
| async def event_stream(): | |
| async for token in stream_agent(msgs, model=model): | |
| chunk = { | |
| "id": "chatcmpl-agent", | |
| "object": "chat.completion.chunk", | |
| "choices": [{"delta": {"content": token}, "index": 0, "finish_reason": None}], | |
| } | |
| yield f"data: {json.dumps(chunk)}\n\n" | |
| done = { | |
| "id": "chatcmpl-agent", | |
| "object": "chat.completion.chunk", | |
| "choices": [{"delta": {}, "index": 0, "finish_reason": "stop"}], | |
| } | |
| yield f"data: {json.dumps(done)}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(event_stream(), media_type="text/event-stream") | |
| text = await run_agent(msgs, model=model) | |
| return { | |
| "id": "chatcmpl-agent", | |
| "object": "chat.completion", | |
| "model": model or "gpt-4o-mini", | |
| "choices": [{"message": {"role": "assistant", "content": text}, "finish_reason": "stop", "index": 0}], | |
| } | |
| # ββ Blood RAG routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def blood_rag_upload(file: UploadFile = File(...)): | |
| raw = await file.read() | |
| name = file.filename or "document" | |
| try: | |
| text = raw.decode("utf-8", errors="replace") | |
| except Exception: | |
| return {"error": "Could not decode file"} | |
| meta = await blood_rag.add_document(name, text) | |
| return meta | |
| async def blood_rag_list(): | |
| return blood_rag.list_documents() | |
| async def blood_rag_delete(doc_id: str): | |
| deleted = blood_rag.delete_document(doc_id) | |
| return {"deleted": deleted, "doc_id": doc_id} | |
| async def blood_rag_search(q: str, top_k: int = 4): | |
| result = await blood_rag.search(q, top_k=top_k) | |
| return {"query": q, "result": result} | |
| # ββ Blood report routes ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def upload_report(file: UploadFile = File(...)): | |
| raw = await file.read() | |
| return await reports.process_report_upload(raw, file.filename or "report.pdf") | |
| async def current_report(): | |
| return reports.get_current_report() | |
| async def delete_report(): | |
| reports.clear_report() | |
| return {"status": "cleared"} | |
| # ββ Notepad routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def notepad_page(): | |
| return _NOTEPAD_HTML | |
| async def notepad_events(): | |
| async def stream(): | |
| q = asyncio.Queue(maxsize=30) | |
| notepad.add_listener(q) | |
| try: | |
| yield f"data: {json.dumps(notepad.get_notes_snapshot())}\n\n" | |
| while True: | |
| try: | |
| payload = await asyncio.wait_for(q.get(), timeout=25) | |
| yield f"data: {payload}\n\n" | |
| except asyncio.TimeoutError: | |
| yield ": keepalive\n\n" | |
| finally: | |
| notepad.remove_listener(q) | |
| return StreamingResponse(stream(), media_type="text/event-stream") | |
| async def clear_notepad_route(): | |
| notepad.clear_notepad() | |
| return {"status": "cleared"} | |
| async def notepad_content(): | |
| return notepad.get_notes_snapshot() | |
| # ββ Notepad HTML βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _NOTEPAD_HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Live Notepad</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| background: #0d0d0d; color: #e0e0e0; | |
| min-height: 100vh; display: flex; flex-direction: column; | |
| align-items: center; padding: 40px 16px; gap: 20px; | |
| } | |
| header { text-align: center; } | |
| header h1 { font-size: 26px; font-weight: 700; letter-spacing: -0.5px; } | |
| header p { font-size: 13px; color: #666; margin-top: 4px; } | |
| #status-bar { | |
| display: flex; align-items: center; gap: 8px; | |
| font-size: 13px; color: #888; | |
| } | |
| #status-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: #444; transition: background 0.3s; | |
| } | |
| #status-dot.live { background: #4CAF50; } | |
| #status-dot.error { background: #f44336; } | |
| #notepad { | |
| width: 660px; max-width: 100%; | |
| background: #141414; border: 1px solid #222; | |
| border-radius: 14px; min-height: 320px; | |
| padding: 20px; display: flex; flex-direction: column; gap: 10px; | |
| } | |
| #empty-hint { | |
| color: #3a3a3a; font-size: 14px; | |
| text-align: center; margin: auto; | |
| line-height: 1.6; | |
| } | |
| .note-item { | |
| display: flex; flex-direction: column; gap: 3px; | |
| padding: 12px 14px; background: #1a1a1a; | |
| border-radius: 8px; border-left: 3px solid #4CAF50; | |
| animation: fadeIn 0.25s ease; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(5px); } | |
| to { opacity: 1; transform: none; } | |
| } | |
| .note-time { font-size: 11px; color: #555; font-family: monospace; } | |
| .note-text { font-size: 14px; color: #e0e0e0; line-height: 1.5; } | |
| #clear-btn { | |
| padding: 10px 24px; border-radius: 8px; border: none; | |
| font-size: 13px; font-weight: 600; cursor: pointer; | |
| background: #2a2a2a; color: #e0e0e0; | |
| transition: background 0.2s; | |
| } | |
| #clear-btn:hover { background: #f44336; color: #fff; } | |
| #note-count { | |
| font-size: 12px; color: #555; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>Live Notepad</h1> | |
| <p>Notes appear here in real time as you speak to the avatar</p> | |
| </header> | |
| <div id="status-bar"> | |
| <div id="status-dot"></div> | |
| <span id="status-text">Connectingβ¦</span> | |
| <span style="margin-left:8px" id="note-count"></span> | |
| </div> | |
| <div id="notepad"> | |
| <div id="notes-list"></div> | |
| <div id="empty-hint"> | |
| No notes yet.<br/> | |
| Tell the avatar: <em>"Note thatβ¦"</em> or <em>"Write down thatβ¦"</em> | |
| </div> | |
| </div> | |
| <button id="clear-btn">Clear All Notes</button> | |
| <script> | |
| const notesList = document.getElementById('notes-list'); | |
| const emptyHint = document.getElementById('empty-hint'); | |
| const statusDot = document.getElementById('status-dot'); | |
| const statusText = document.getElementById('status-text'); | |
| const noteCount = document.getElementById('note-count'); | |
| function esc(s) { | |
| return String(s) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>'); | |
| } | |
| function renderNotes(notes) { | |
| noteCount.textContent = notes.length ? `${notes.length} note${notes.length > 1 ? 's' : ''}` : ''; | |
| if (!notes.length) { | |
| notesList.innerHTML = ''; | |
| emptyHint.style.display = 'block'; | |
| return; | |
| } | |
| emptyHint.style.display = 'none'; | |
| notesList.innerHTML = notes.map(n => | |
| `<div class="note-item"> | |
| <span class="note-time">${esc(n.time)}</span> | |
| <span class="note-text">${esc(n.text)}</span> | |
| </div>` | |
| ).join(''); | |
| notesList.lastElementChild?.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| const es = new EventSource('/notepad/events'); | |
| es.onopen = () => { statusDot.className = 'live'; statusText.textContent = 'Live'; }; | |
| es.onmessage = (e) => { const d = JSON.parse(e.data); if (d.notes !== undefined) renderNotes(d.notes); }; | |
| es.onerror = () => { statusDot.className = 'error'; statusText.textContent = 'Reconnectingβ¦'; }; | |
| document.getElementById('clear-btn').addEventListener('click', () => { | |
| fetch('/notepad/clear', { method: 'POST' }); | |
| }); | |
| </script> | |
| </body> | |
| </html>""" | |
| if __name__ == "__main__": | |
| uvicorn.run("main:app", host="0.0.0.0", port=PORT, reload=True) | |