Explorar o código

fastapi+sse+docker

hjh hai 6 días
pai
achega
aed928e2c6

+ 10 - 0
黄靖淏/fastapi_sse_docker/Dockerfile

@@ -0,0 +1,10 @@
+FROM python:3.11-slim
+
+WORKDIR /code
+
+COPY . .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+
+CMD ["uvicorn", "chat:app", "--host", "0.0.0.0", "--port", "8000"]

+ 85 - 0
黄靖淏/fastapi_sse_docker/chat.py

@@ -0,0 +1,85 @@
+
+import uuid
+from fastapi import FastAPI
+from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
+from fastapi.middleware.cors import CORSMiddleware
+
+from agno.agent import Agent
+from agno.models.openai.like import OpenAILike
+from agno.memory.v2.db.sqlite import SqliteMemoryDb
+from agno.memory.v2.memory import Memory
+from agno.storage.sqlite import SqliteStorage
+from textwrap import dedent
+import os
+
+memory_db = SqliteMemoryDb(db_file="tmp/chat_memory.db", table_name="memory")
+storge_db = SqliteStorage(table_name="agent_sessions", db_file="tmp/chat_memory.db")
+
+memory = Memory(
+    model=OpenAILike(
+        id="qwen3-32b",
+        api_key=os.getenv("BAILIAN_API_KEY"),
+        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
+        request_params={"extra_body": {"enable_thinking": False}},
+    ),
+    db=memory_db,
+)
+
+agent = Agent(
+    model=OpenAILike(
+        id="qwen3-32b",
+        api_key=os.getenv("BAILIAN_API_KEY"),
+        base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
+        request_params={"extra_body": {"enable_thinking": False}},
+    ),
+    instructions=dedent("""\
+        You are a helpful multi-turn information collection assistant. 
+        Your job is to collect the user's:
+        1. name
+        2. age
+        3. industry
+
+        You must follow this strict logic:
+        - Start by asking the user's name.
+        - Then ask for the age.
+        - Then ask for the industry.
+        - Do NOT skip ahead.
+        - If the user gives an irrelevant answer or skips, gently remind them and guide them back.
+        - When all 3 pieces of info are collected, show a summary to the user and tell them they are free to ask anything.
+        - Until all info is collected, don't answer other questions. Just remind them to finish the info collection first.
+
+        Always remember the information already collected and avoid repeating questions.
+        Use concise and friendly tone.
+    """),
+    memory=memory,
+    storage=storge_db,
+    stream=True,
+    add_datetime_to_instructions=True,
+    show_tool_calls=True,
+    markdown=False,
+    add_history_to_messages=True,
+    enable_user_memories=True,
+)
+
+user_id = str(uuid.uuid4())
+
+
+async def ask_agent(message: str):
+    for chunk in agent.run(message=message, user_id=user_id, stream=True):
+        yield f"data: {chunk}\n\n"
+
+
+app = FastAPI()
+
+
+@app.get("/stream_text")
+async def stream_text(message: str = "你好,这是使用FastAPI和SSE实现的打字机效果。"):
+    return StreamingResponse(
+        ask_agent(message),
+        media_type="text/event-stream",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )
+
+@app.get("/", response_class=HTMLResponse)
+async def get_index():
+    return FileResponse("./index.html")

+ 147 - 0
黄靖淏/fastapi_sse_docker/index.html

@@ -0,0 +1,147 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8" />
+    <title>智能体助手 - 流式输出演示</title>
+    <style>
+        body {
+            font-family: 'Segoe UI', sans-serif;
+            background: #f5f5f5;
+            margin: 0;
+            padding: 0;
+            display: flex;
+            flex-direction: column;
+            height: 100vh;
+        }
+
+        header {
+            background-color: #4CAF50;
+            color: white;
+            padding: 1rem;
+            font-size: 1.5rem;
+            text-align: center;
+        }
+
+        #chat-container {
+            flex: 1;
+            overflow-y: auto;
+            padding: 1rem;
+            background-color: #ffffff;
+            box-shadow: inset 0 0 5px #ddd;
+        }
+
+        .message {
+            padding: 0.6rem 1rem;
+            margin-bottom: 0.8rem;
+            border-radius: 10px;
+            max-width: 90%;
+            line-height: 1.6;
+            font-size: 1.1rem;
+        }
+
+        .bot {
+            background-color: #e0f7fa;
+            align-self: flex-start;
+        }
+
+        .typing::after {
+            content: '|';
+            animation: blink 1s infinite;
+            color: gray;
+        }
+
+        @keyframes blink {
+            0% { opacity: 1; }
+            50% { opacity: 0; }
+            100% { opacity: 1; }
+        }
+
+        footer {
+            padding: 1rem;
+            background-color: #fff;
+            display: flex;
+            gap: 0.5rem;
+            border-top: 1px solid #ccc;
+        }
+
+        input[type="text"] {
+            flex: 1;
+            padding: 0.6rem;
+            font-size: 1rem;
+            border: 1px solid #ccc;
+            border-radius: 5px;
+        }
+
+        button {
+            padding: 0.6rem 1.2rem;
+            font-size: 1rem;
+            background-color: #4CAF50;
+            color: white;
+            border: none;
+            border-radius: 5px;
+            cursor: pointer;
+        }
+
+        button:hover {
+            background-color: #45a049;
+        }
+
+    </style>
+</head>
+<body>
+<header>智能体助手 - 流式回复展示</header>
+<div id="chat-container"></div>
+
+<footer>
+    <input id="message-input" type="text" placeholder="请输入您的消息..." />
+    <button onclick="sendMessage()">发送</button>
+</footer>
+
+<script>
+    const chatContainer = document.getElementById('chat-container');
+    const input = document.getElementById('message-input');
+    let source = null;
+    let currentLine = null;
+
+    function sendMessage() {
+        const message = input.value.trim();
+        if (!message) return;
+
+        if (source) {
+            source.close(); // 停止旧的连接
+        }
+
+        // 创建新消息容器
+        currentLine = document.createElement('div');
+        currentLine.className = 'message bot typing';
+        chatContainer.appendChild(currentLine);
+        chatContainer.scrollTop = chatContainer.scrollHeight;
+
+        // 启动 SSE
+        source = new EventSource(`/stream_text?message=${encodeURIComponent(message)}`);
+
+        source.onmessage = function (event) {
+            try {
+                if (!event.data.startsWith('RunResponseContentEvent')) return;
+
+                const contentMatch = event.data.match(/content='(.*?)'/);
+                if (contentMatch && contentMatch[1]) {
+                    const text = contentMatch[1];
+                    currentLine.textContent += text;
+                    chatContainer.scrollTop = chatContainer.scrollHeight;
+                }
+            } catch (e) {
+                console.warn('解析失败:', event.data);
+            }
+        };
+
+        source.onerror = function () {
+            currentLine.classList.remove('typing');
+            source.close();
+        };
+
+        input.value = '';
+    }
+</script>
+</body>
+</html>

+ 105 - 0
黄靖淏/fastapi_sse_docker/requirements.txt

@@ -0,0 +1,105 @@
+agno==1.7.2
+annotated-types==0.7.0
+anthropic==0.57.1
+anyio==4.9.0
+asttokens==3.0.0
+beautifulsoup4==4.13.4
+certifi==2025.7.14
+cffi==1.17.1
+charset-normalizer==3.4.2
+click==8.2.1
+colorama==0.4.6
+comm==0.2.2
+curl_cffi==0.12.0
+debugpy==1.8.14
+decorator==5.2.1
+deepdiff==8.5.0
+distro==1.9.0
+dnspython==2.7.0
+docstring_parser==0.16
+email_validator==2.2.0
+executing==2.2.0
+fastapi==0.116.1
+fastapi-cli==0.0.8
+fastapi-cloud-cli==0.1.4
+frozendict==2.4.6
+gitdb==4.0.12
+GitPython==3.1.44
+greenlet==3.2.3
+h11==0.16.0
+httpcore==1.0.9
+httptools==0.6.4
+httpx==0.28.1
+idna==3.10
+ipykernel==6.29.5
+ipython==9.4.0
+ipython_pygments_lexers==1.1.1
+ipywidgets==8.1.7
+itsdangerous==2.2.0
+jedi==0.19.2
+Jinja2==3.1.6
+jiter==0.10.0
+jupyter_client==8.6.3
+jupyter_core==5.8.1
+jupyterlab_widgets==3.0.15
+markdown-it-py==3.0.0
+MarkupSafe==3.0.2
+matplotlib-inline==0.1.7
+mdurl==0.1.2
+multitasking==0.0.11
+nest-asyncio==1.6.0
+numpy==2.3.1
+openai==1.95.1
+orderly-set==5.5.0
+orjson==3.11.0
+packaging==25.0
+pandas==2.3.1
+parso==0.8.4
+peewee==3.18.2
+platformdirs==4.3.8
+prompt_toolkit==3.0.51
+protobuf==6.31.1
+psutil==7.0.0
+pure_eval==0.2.3
+pycparser==2.22
+pydantic==2.11.7
+pydantic-extra-types==2.10.5
+pydantic-settings==2.10.1
+pydantic_core==2.33.2
+Pygments==2.19.2
+python-dateutil==2.9.0.post0
+python-dotenv==1.1.1
+python-multipart==0.0.20
+pytz==2025.2
+pywin32==310; sys_platform == "win32"
+PyYAML==6.0.2
+pyzmq==27.0.0
+requests==2.32.4
+rich==14.0.0
+rich-toolkit==0.14.8
+rignore==0.6.2
+sentry-sdk==2.33.0
+shellingham==1.5.4
+six==1.17.0
+smmap==5.0.2
+sniffio==1.3.1
+soupsieve==2.7
+SQLAlchemy==2.0.41
+stack-data==0.6.3
+starlette==0.47.1
+tomli==2.2.1
+tornado==6.5.1
+tqdm==4.67.1
+traitlets==5.14.3
+typer==0.16.0
+typing-inspection==0.4.1
+typing_extensions==4.14.1
+tzdata==2025.2
+ujson==5.10.0
+urllib3==2.5.0
+uvicorn==0.35.0
+watchfiles==1.1.0
+wcwidth==0.2.13
+websockets==15.0.1
+widgetsnbextension==4.0.14
+yfinance==0.2.65