Commit
·
82bf89e
0
Parent(s):
reinit repo
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +10 -0
- .gitignore +87 -0
- Dockerfile +90 -0
- README.md +119 -0
- assets/extract1.png +3 -0
- assets/extract2.png +3 -0
- assets/main_pic.png +3 -0
- assets/main_pic_review.png +3 -0
- assets/mcp_playground.png +3 -0
- assets/sidebar_available_tools.png +3 -0
- assets/sidebar_available_tools_list.png +3 -0
- assets/sidebar_providers.png +3 -0
- assets/sidebar_server_management.png +3 -0
- assets/tool_execution_history.png +3 -0
- client/.env-example.txt +2 -0
- client/.streamlit/config.toml +22 -0
- client/.streamlit/style.css +139 -0
- client/Dockerfile +32 -0
- client/README.md +80 -0
- client/app.py +50 -0
- client/apps/__init__.py +0 -0
- client/apps/mcp_playground.py +1353 -0
- client/chat_history/chat_Bio_QA_mcp_agent_20250908_122027.json +37 -0
- client/chat_history/chat_Review_mcp_agent_20250908_121128.json +0 -0
- client/config.py +59 -0
- client/docs/import_export_guide.md +65 -0
- client/docs/logging_config.md +78 -0
- client/icons/playground.png +3 -0
- client/requirements.txt +23 -0
- client/run.sh +13 -0
- client/servers_config.json +18 -0
- client/services/__init__.py +0 -0
- client/services/ai_service.py +149 -0
- client/services/chat_service.py +207 -0
- client/services/export_service.py +166 -0
- client/services/import_service.py +92 -0
- client/services/logging_service.py +212 -0
- client/services/mcp_service.py +104 -0
- client/services/task_monitor.py +160 -0
- client/static/.gitkeep +1 -0
- client/ui_components/__init__.py +0 -0
- client/ui_components/log_viewer.py +183 -0
- client/ui_components/main_components.py +13 -0
- client/ui_components/sidebar_components.py +344 -0
- client/utils/__init__.py +0 -0
- client/utils/ai_prompts.py +93 -0
- client/utils/async_helpers.py +28 -0
- client/utils/tool_schema_parser.py +29 -0
- docker-compose.yml +45 -0
- servers/Retrieve/.gitignore +9 -0
.gitattributes
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.webp filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
assets/*.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.xlsx filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
servers/Retrieve/config/*.xlsx filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# Input/Output files
|
| 4 |
+
data/
|
| 5 |
+
|
| 6 |
+
# Python bytecode
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
*.swp
|
| 12 |
+
|
| 13 |
+
# Jupyter Notebook Checkpoints
|
| 14 |
+
.ipynb_checkpoints/
|
| 15 |
+
|
| 16 |
+
# Virtual environment
|
| 17 |
+
env/
|
| 18 |
+
venv/
|
| 19 |
+
ENV/
|
| 20 |
+
env.bak/
|
| 21 |
+
venv.bak/
|
| 22 |
+
|
| 23 |
+
# Distribution / packaging
|
| 24 |
+
*.egg
|
| 25 |
+
*.egg-info/
|
| 26 |
+
dist/
|
| 27 |
+
build/
|
| 28 |
+
*.tar.gz
|
| 29 |
+
|
| 30 |
+
# IDEs
|
| 31 |
+
.vscode/
|
| 32 |
+
.idea/
|
| 33 |
+
|
| 34 |
+
# MacOS
|
| 35 |
+
.DS_Store
|
| 36 |
+
|
| 37 |
+
# Windows
|
| 38 |
+
Thumbs.db
|
| 39 |
+
|
| 40 |
+
# Python Logs
|
| 41 |
+
*.log
|
| 42 |
+
*.log.*
|
| 43 |
+
|
| 44 |
+
# Pytest
|
| 45 |
+
.cache/
|
| 46 |
+
|
| 47 |
+
# Other
|
| 48 |
+
*.bak
|
| 49 |
+
*.tmp
|
| 50 |
+
*.swp
|
| 51 |
+
|
| 52 |
+
# dependencies
|
| 53 |
+
/node_modules
|
| 54 |
+
/.pnp
|
| 55 |
+
.pnp.js
|
| 56 |
+
.yarn/install-state.gz
|
| 57 |
+
|
| 58 |
+
# testing
|
| 59 |
+
/coverage
|
| 60 |
+
|
| 61 |
+
# next.js
|
| 62 |
+
/.next/
|
| 63 |
+
/out/
|
| 64 |
+
|
| 65 |
+
# production
|
| 66 |
+
/build
|
| 67 |
+
|
| 68 |
+
# misc
|
| 69 |
+
.DS_Store
|
| 70 |
+
*.pem
|
| 71 |
+
|
| 72 |
+
# debug
|
| 73 |
+
npm-debug.log*
|
| 74 |
+
yarn-debug.log*
|
| 75 |
+
yarn-error.log*
|
| 76 |
+
|
| 77 |
+
# local env files
|
| 78 |
+
# .env*.local
|
| 79 |
+
# .env.prod
|
| 80 |
+
|
| 81 |
+
# vercel
|
| 82 |
+
.vercel
|
| 83 |
+
|
| 84 |
+
# typescript
|
| 85 |
+
*.tsbuildinfo
|
| 86 |
+
next-env.d.ts
|
| 87 |
+
.env
|
Dockerfile
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =========================
|
| 2 |
+
# Simple Python 3.12 environment
|
| 3 |
+
# =========================
|
| 4 |
+
FROM python:3.12-slim-bullseye AS final
|
| 5 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 6 |
+
PIP_NO_CACHE_DIR=1
|
| 7 |
+
|
| 8 |
+
# Create user
|
| 9 |
+
RUN useradd -m -u 1000 user
|
| 10 |
+
USER user
|
| 11 |
+
ENV HOME=/home/user \
|
| 12 |
+
PATH=/home/user/.local/bin:$PATH
|
| 13 |
+
WORKDIR $HOME/app
|
| 14 |
+
|
| 15 |
+
# Copy application code
|
| 16 |
+
COPY --chown=user client/ $HOME/app/client/
|
| 17 |
+
COPY --chown=user servers/ $HOME/app/servers/
|
| 18 |
+
|
| 19 |
+
# Create independent virtual environments for each service
|
| 20 |
+
RUN python -m venv $HOME/venvs/client \
|
| 21 |
+
&& python -m venv $HOME/venvs/retrieve \
|
| 22 |
+
&& python -m venv $HOME/venvs/review
|
| 23 |
+
|
| 24 |
+
# Install client dependencies
|
| 25 |
+
RUN $HOME/venvs/client/bin/pip install --no-cache-dir -U pip \
|
| 26 |
+
&& $HOME/venvs/client/bin/pip install --no-cache-dir -r $HOME/app/client/requirements.txt
|
| 27 |
+
|
| 28 |
+
# Install Retrieve dependencies
|
| 29 |
+
RUN $HOME/venvs/retrieve/bin/pip install --no-cache-dir -U pip \
|
| 30 |
+
&& $HOME/venvs/retrieve/bin/pip install --no-cache-dir -r $HOME/app/servers/Retrieve/requirements.txt \
|
| 31 |
+
&& $HOME/venvs/retrieve/bin/pip install --no-cache-dir -U crawl4ai \
|
| 32 |
+
&& $HOME/venvs/retrieve/bin/crawl4ai-setup || true \
|
| 33 |
+
&& $HOME/venvs/retrieve/bin/crawl4ai-doctor || true
|
| 34 |
+
|
| 35 |
+
# Install Review dependencies
|
| 36 |
+
RUN $HOME/venvs/review/bin/pip install --no-cache-dir -U pip \
|
| 37 |
+
&& $HOME/venvs/review/bin/pip install --no-cache-dir -r $HOME/app/servers/Review/requirements.txt
|
| 38 |
+
|
| 39 |
+
# Generate startup script
|
| 40 |
+
RUN mkdir -p $HOME/app \
|
| 41 |
+
&& cat <<'EOF' > $HOME/app/start.sh
|
| 42 |
+
#!/bin/bash
|
| 43 |
+
set -e
|
| 44 |
+
cd "$HOME/app"
|
| 45 |
+
|
| 46 |
+
start_service() {
|
| 47 |
+
local name="$1"
|
| 48 |
+
local dir="$2"
|
| 49 |
+
local py="$3"
|
| 50 |
+
(
|
| 51 |
+
cd "$dir"
|
| 52 |
+
while true; do
|
| 53 |
+
echo "[startup] Starting $name (Python 3.12)…"
|
| 54 |
+
set +e
|
| 55 |
+
"$py" main.py
|
| 56 |
+
exit_code=$?
|
| 57 |
+
set -e
|
| 58 |
+
if [ $exit_code -eq 0 ]; then
|
| 59 |
+
echo "[$name] exited normally"
|
| 60 |
+
break
|
| 61 |
+
else
|
| 62 |
+
echo "[$name] crashed with code $exit_code, restarting in 10s..."
|
| 63 |
+
sleep 10
|
| 64 |
+
fi
|
| 65 |
+
done
|
| 66 |
+
) &
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if [ -d "servers/Retrieve" ]; then
|
| 70 |
+
start_service "Retrieve" "servers/Retrieve" "$HOME/venvs/retrieve/bin/python"
|
| 71 |
+
fi
|
| 72 |
+
|
| 73 |
+
if [ -d "servers/Review" ]; then
|
| 74 |
+
start_service "Review" "servers/Review" "$HOME/venvs/review/bin/python"
|
| 75 |
+
fi
|
| 76 |
+
|
| 77 |
+
# Wait for backend services to start
|
| 78 |
+
sleep 5
|
| 79 |
+
|
| 80 |
+
# Start frontend Streamlit
|
| 81 |
+
echo "[startup] Starting Streamlit client (Python 3.12)…"
|
| 82 |
+
cd "$HOME/app/client"
|
| 83 |
+
export PORT="${PORT:-7860}"
|
| 84 |
+
exec "$HOME/venvs/client/bin/python" -m streamlit run app.py --server.port="$PORT" --server.address=0.0.0.0
|
| 85 |
+
EOF
|
| 86 |
+
|
| 87 |
+
RUN chmod +x $HOME/app/start.sh
|
| 88 |
+
|
| 89 |
+
EXPOSE 7860
|
| 90 |
+
CMD ["/bin/bash", "/home/user/app/start.sh"]
|
README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: ADMP-LS
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
# ADMP-LS
|
| 11 |
+
|
| 12 |
+
A multi-service MCP (Model Context Protocol) application containing independent MCP servers and a Streamlit client.
|
| 13 |
+
|
| 14 |
+
## Description & Citation
|
| 15 |
+
|
| 16 |
+
ADMP-LS is an agent-based platform for life sciences that unifies literature review, evidence-grounded QA, and parameter extraction with transparent provenance.
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+

|
| 20 |
+
|
| 21 |
+

|
| 22 |
+
|
| 23 |
+

|
| 24 |
+

|
| 25 |
+
## Service Architecture
|
| 26 |
+
|
| 27 |
+
- **Streamlit Client** (Port 7860, Python 3.11): Main user interface
|
| 28 |
+
- **Retrieve Service** (Port 9487, Python 3.11): Biomedical RAG service
|
| 29 |
+
- **Review Service** (Port 8880, Python 3.11): Biomedical Review service
|
| 30 |
+
|
| 31 |
+
Related docs:
|
| 32 |
+
- Client details: `client/README.md`
|
| 33 |
+
- Retrieve server: `servers/Retrieve/readme.md`
|
| 34 |
+
- Review server (EN): `servers/Review/readme.md`
|
| 35 |
+
|
| 36 |
+
## Technical Features
|
| 37 |
+
|
| 38 |
+
- ✅ Multi-stage Docker build
|
| 39 |
+
- ✅ Multi-Python version support (3.11 + 3.12)
|
| 40 |
+
- ✅ Virtual environment isolation
|
| 41 |
+
- ✅ HF Spaces compliant
|
| 42 |
+
- ✅ GPU support (optional)
|
| 43 |
+
|
| 44 |
+
## Deployment
|
| 45 |
+
|
| 46 |
+
This Space uses Docker deployment, with all services running in the same container but using independent Python virtual environments to avoid dependency conflicts.
|
| 47 |
+
|
| 48 |
+
## Environment Variables
|
| 49 |
+
|
| 50 |
+
You can set the following environment variables through HF Spaces:
|
| 51 |
+
|
| 52 |
+
### Basic Configuration
|
| 53 |
+
- `PORT`: Streamlit client port (default 7860)
|
| 54 |
+
- `RETRIEVE_PORT`: Retrieve service port (default 9487)
|
| 55 |
+
- `REVIEW_PORT`: Review service port (default 8880)
|
| 56 |
+
|
| 57 |
+
### Retrieve Service LLM Configuration
|
| 58 |
+
- `QA_LLM_MAIN_API_KEY`: QA main model API key
|
| 59 |
+
- `QA_LLM_MAIN_BASE_URL`: QA main model base URL
|
| 60 |
+
- `QA_LLM_BACKUP_API_KEY`: QA backup model API key
|
| 61 |
+
- `QA_LLM_BACKUP_BASE_URL`: QA backup model base URL
|
| 62 |
+
- `REWRITE_LLM_MAIN_API_KEY`: Rewrite main model API key
|
| 63 |
+
- `REWRITE_LLM_MAIN_BASE_URL`: Rewrite main model base URL
|
| 64 |
+
- `REWRITE_LLM_BACKUP_API_KEY`: Rewrite backup model API key
|
| 65 |
+
- `REWRITE_LLM_BACKUP_BASE_URL`: Rewrite backup model base URL
|
| 66 |
+
|
| 67 |
+
### Retrieve Service Web Search Configuration
|
| 68 |
+
- `SERPER_API_KEY`: Serper API key (for web search)
|
| 69 |
+
|
| 70 |
+
### Review Service Configuration
|
| 71 |
+
- `OPENAI_BASE_URL`: OpenAI API base URL
|
| 72 |
+
- `OPENAI_API_KEY`: OpenAI API key
|
| 73 |
+
- `QIANWEN_BASE_URL`: Qianwen API base URL
|
| 74 |
+
- `QIANWEN_API_KEY`: Qianwen API key
|
| 75 |
+
- `SEARCH_URL`: Search service URL
|
| 76 |
+
- `LOG_DIR`: Log directory
|
| 77 |
+
- `LOG_LEVEL`: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
| 78 |
+
- `LOG_MAX_SIZE`: Log file maximum size (bytes)
|
| 79 |
+
- `LOG_BACKUP_COUNT`: Log backup file count
|
| 80 |
+
- `LOG_ENABLE_CONSOLE`: Enable console logging (true/false)
|
| 81 |
+
- `LOG_ENABLE_FILE`: Enable file logging (true/false)
|
| 82 |
+
- `DEBUG_MODE`: Debug mode (true/false)
|
| 83 |
+
|
| 84 |
+
### Client Configuration (providers)
|
| 85 |
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`
|
| 86 |
+
- `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`
|
| 87 |
+
- `GOOGLE_API_KEY`, `GOOGLE_BASE_URL`
|
| 88 |
+
- `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
|
| 89 |
+
- `GROQ_API_KEY`, `GROQ_BASE_URL`
|
| 90 |
+
|
| 91 |
+
Note: place a `.env` file under `client/` (see `client/.env-example.txt`). The client also reads `client/servers_config.json` for MCP/server integration.
|
| 92 |
+
|
| 93 |
+
## Ports
|
| 94 |
+
- Space app port (client UI): `PORT` (default 7860)
|
| 95 |
+
- Internal Retrieve service: 9487 (HTTP APIs: `/retrieve`, `/stream-chat`)
|
| 96 |
+
- Internal Review service: 8880 (HTTP APIs: `/health`, `/review_generate`; MCP mounts: `/review`, `/check`)
|
| 97 |
+
|
| 98 |
+
## Quick Links
|
| 99 |
+
- Start client locally: see `client/README.md`
|
| 100 |
+
- Start Retrieve locally or with Docker: see `servers/Retrieve/readme.md`
|
| 101 |
+
- Start Review locally or with Docker: see `servers/Review/readme.md`
|
| 102 |
+
|
| 103 |
+
## 🔗 Links
|
| 104 |
+
- **GitHub**: [https://github.com/JackKuo666/ADMP-LS](https://github.com/JackKuo666/ADMP-LS)
|
| 105 |
+
- **Hugging Face Spaces**: [https://huggingface.co/spaces/jackkuo/ADMP-LS](https://huggingface.co/spaces/jackkuo/ADMP-LS)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
## 🙏 Acknowledgements
|
| 109 |
+
|
| 110 |
+
- [mcp-playground](https://github.com/Elkhn/mcp-playground)
|
| 111 |
+
- [LangChain](https://github.com/langchain-ai/langchain)
|
| 112 |
+
- [LangGraph](https://github.com/langchain-ai/langgraph)
|
| 113 |
+
- [FastMCP](https://github.com/jlowin/fastmcp)
|
| 114 |
+
- [Streamlit](https://github.com/streamlit/streamlit)
|
| 115 |
+
|
| 116 |
+
## Citation
|
| 117 |
+
Citation: Guo M., Sun Z., Xie S., Hu J., Sun S., Li X., Feng L., Jiang J. "ADMP-LS: Agent-based Dialogue and Mining Platform for Evidence-Grounded QA, Extraction, and Literature Review in Life Science," Zhejiang Lab & Western Carolina University.
|
| 118 |
+
|
| 119 |
+
|
assets/extract1.png
ADDED
|
Git LFS Details
|
assets/extract2.png
ADDED
|
Git LFS Details
|
assets/main_pic.png
ADDED
|
Git LFS Details
|
assets/main_pic_review.png
ADDED
|
Git LFS Details
|
assets/mcp_playground.png
ADDED
|
Git LFS Details
|
assets/sidebar_available_tools.png
ADDED
|
Git LFS Details
|
assets/sidebar_available_tools_list.png
ADDED
|
Git LFS Details
|
assets/sidebar_providers.png
ADDED
|
Git LFS Details
|
assets/sidebar_server_management.png
ADDED
|
Git LFS Details
|
assets/tool_execution_history.png
ADDED
|
Git LFS Details
|
client/.env-example.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=sk-xxx
|
| 2 |
+
OPENAI_BASE_URL=https://xxx
|
client/.streamlit/config.toml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[server]
|
| 2 |
+
port=8502 # change port number. By default streamlit uses 8502 port
|
| 3 |
+
address = "0.0.0.0"
|
| 4 |
+
headless=true # This will eliminate automatically open browser
|
| 5 |
+
enableCORS=false
|
| 6 |
+
enableXsrfProtection=false
|
| 7 |
+
enableWebsocketCompression=false
|
| 8 |
+
enableStaticServing = true
|
| 9 |
+
|
| 10 |
+
[browser] # This ip and port will show in command prompt
|
| 11 |
+
#serverAddress = " "
|
| 12 |
+
serverPort = 8502
|
| 13 |
+
gatherUsageStats = false
|
| 14 |
+
|
| 15 |
+
[theme]
|
| 16 |
+
primaryColor="#2F2E78"
|
| 17 |
+
backgroundColor="#FFFFFF"
|
| 18 |
+
secondaryBackgroundColor="#F0F2F6"
|
| 19 |
+
textColor="#31333F"
|
| 20 |
+
font="sans serif"
|
| 21 |
+
|
| 22 |
+
|
client/.streamlit/style.css
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* section[data-testid="stSidebar"] div[data-testid="stSidebarContent"] {
|
| 2 |
+
overflow: hidden;
|
| 3 |
+
} */
|
| 4 |
+
|
| 5 |
+
section[data-testid="stSidebar"]
|
| 6 |
+
.block-container
|
| 7 |
+
> div[data-testid="stVerticalBlockBorderWrapper"]
|
| 8 |
+
> div {
|
| 9 |
+
height: 100vh;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
section[data-testid="stSidebar"]
|
| 13 |
+
.block-container
|
| 14 |
+
> div[data-testid="stVerticalBlockBorderWrapper"]
|
| 15 |
+
> div
|
| 16 |
+
> div {
|
| 17 |
+
display: flex;
|
| 18 |
+
height: 100vh;
|
| 19 |
+
padding-bottom: 35px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
section[data-testid="stSidebar"]
|
| 23 |
+
.block-container
|
| 24 |
+
> div[data-testid="stVerticalBlockBorderWrapper"]
|
| 25 |
+
> div
|
| 26 |
+
> div
|
| 27 |
+
> div:nth-child(2) {
|
| 28 |
+
flex: 1;
|
| 29 |
+
order: 1;
|
| 30 |
+
overflow: overlay;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
section[data-testid="stSidebar"]
|
| 34 |
+
.block-container
|
| 35 |
+
> div[data-testid="stVerticalBlockBorderWrapper"]
|
| 36 |
+
> div
|
| 37 |
+
> div
|
| 38 |
+
> div:nth-child(n + 3) {
|
| 39 |
+
order: 2;
|
| 40 |
+
flex-grow: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
section[data-testid="stSidebar"] > div > div:nth-child(2) {
|
| 44 |
+
padding-top: 0rem !important;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
section.main > div {
|
| 48 |
+
padding-top: 0px;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
.icon-text-container {
|
| 52 |
+
display: flex;
|
| 53 |
+
justify-content: center;
|
| 54 |
+
text-align: center;
|
| 55 |
+
align-items: center;
|
| 56 |
+
margin-bottom: 8px;
|
| 57 |
+
margin-top: 0px;
|
| 58 |
+
margin-left: -8px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.icon-text-container img {
|
| 62 |
+
width: 60px;
|
| 63 |
+
height: 60px;
|
| 64 |
+
margin-right: 0;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.icon-text-container span {
|
| 68 |
+
text-align: center;
|
| 69 |
+
text-shadow: 1px 1px #ccc;
|
| 70 |
+
font-size: 28px !important;
|
| 71 |
+
font-family: "auto";
|
| 72 |
+
font-weight: 500 !important;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
hr {
|
| 76 |
+
margin-top: 20px;
|
| 77 |
+
margin-bottom: 30px;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.avatar {
|
| 81 |
+
display: flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
gap: 10px;
|
| 84 |
+
pointer-events: none;
|
| 85 |
+
margin: -8px 10px -16px;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.avatar svg {
|
| 89 |
+
width: 30px;
|
| 90 |
+
height: 30px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.avatar h2 {
|
| 94 |
+
font-size: 20px;
|
| 95 |
+
margin: 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.content-div {
|
| 99 |
+
padding: 5px 20px;
|
| 100 |
+
margin: 5px;
|
| 101 |
+
text-align: left;
|
| 102 |
+
border-radius: 10px;
|
| 103 |
+
border: none;
|
| 104 |
+
line-height: 1.6;
|
| 105 |
+
font-size: 17px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.content-div.assistant p {
|
| 109 |
+
padding: 4px;
|
| 110 |
+
margin: 2px;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.content-div.user p {
|
| 114 |
+
padding: 4px;
|
| 115 |
+
margin: -5px 2px -3px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
div[data-testid="stForm"] {
|
| 119 |
+
border: none;
|
| 120 |
+
padding: 0;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
button[kind="primaryFormSubmit"] {
|
| 124 |
+
border: none;
|
| 125 |
+
padding: 0;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
div[data-testid="stForm"]
|
| 129 |
+
+ div[data-testid="stHorizontalBlock"]
|
| 130 |
+
div[data-baseweb="select"]
|
| 131 |
+
> div:nth-child(1) {
|
| 132 |
+
background-color: transparent;
|
| 133 |
+
justify-content: center;
|
| 134 |
+
font-weight: 300;
|
| 135 |
+
border-radius: 0.25rem;
|
| 136 |
+
margin: 0;
|
| 137 |
+
line-height: 1.4;
|
| 138 |
+
border: 1px solid rgba(49, 51, 63, 0.2);
|
| 139 |
+
}
|
client/Dockerfile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim-bullseye
|
| 2 |
+
|
| 3 |
+
# Set up a new user named "user" with user ID 1000
|
| 4 |
+
RUN useradd -m -u 1000 user
|
| 5 |
+
|
| 6 |
+
# Switch to the "user" user
|
| 7 |
+
USER user
|
| 8 |
+
|
| 9 |
+
# Set home to the user's home directory
|
| 10 |
+
ENV HOME=/home/user \
|
| 11 |
+
PATH=/home/user/.local/bin:$PATH
|
| 12 |
+
|
| 13 |
+
# Set the working directory to the user's home directory
|
| 14 |
+
WORKDIR $HOME/app
|
| 15 |
+
|
| 16 |
+
# Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
|
| 17 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 18 |
+
|
| 19 |
+
# Copy the current directory contents into the container at $HOME/app setting the owner to the user
|
| 20 |
+
COPY --chown=user requirements.txt $HOME/app/
|
| 21 |
+
|
| 22 |
+
# Install Python dependencies
|
| 23 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 24 |
+
|
| 25 |
+
# Copy the rest of the application
|
| 26 |
+
COPY --chown=user . $HOME/app/
|
| 27 |
+
|
| 28 |
+
# Expose the port that Streamlit runs on
|
| 29 |
+
EXPOSE 8501
|
| 30 |
+
|
| 31 |
+
# Command to run the application
|
| 32 |
+
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
|
client/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## MCP Playground (Client)
|
| 2 |
+
|
| 3 |
+
A Streamlit-based playground UI for multiple models and providers.
|
| 4 |
+
|
| 5 |
+
### Dependencies
|
| 6 |
+
- Python 3.11
|
| 7 |
+
- See `requirements.txt` (key libs: `streamlit`, `langchain`, `langgraph`, `boto3`, `python-dotenv`, etc.)
|
| 8 |
+
|
| 9 |
+
### Environment Variables
|
| 10 |
+
Create a `.env` file under `client` (use `.env-example.txt` as a reference). Common keys:
|
| 11 |
+
- `OPENAI_API_KEY`, `OPENAI_BASE_URL`
|
| 12 |
+
- `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`
|
| 13 |
+
- `GOOGLE_API_KEY`, `GOOGLE_BASE_URL`
|
| 14 |
+
- `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`
|
| 15 |
+
- `GROQ_API_KEY`, `GROQ_BASE_URL`
|
| 16 |
+
|
| 17 |
+
Optional: `servers_config.json` (MCP server/services config). The app reads `client/servers_config.json`.
|
| 18 |
+
|
| 19 |
+
Notes:
|
| 20 |
+
- `.env-example.txt` only shows OpenAI keys as a sample; add other provider keys as needed.
|
| 21 |
+
- Bedrock (AWS) requires valid AWS credentials and region.
|
| 22 |
+
|
| 23 |
+
### Run Locally
|
| 24 |
+
From the `client` directory:
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
# 1) Create and activate a virtualenv (example: venv)
|
| 28 |
+
python3.11 -m venv .venv
|
| 29 |
+
source .venv/bin/activate
|
| 30 |
+
|
| 31 |
+
# 2) Install dependencies
|
| 32 |
+
pip install --upgrade pip
|
| 33 |
+
pip install -r requirements.txt
|
| 34 |
+
|
| 35 |
+
# 3) Start (pick one)
|
| 36 |
+
streamlit run app.py --server.port=8501 --server.address=0.0.0.0
|
| 37 |
+
# or use the script (port 8502)
|
| 38 |
+
chmod +x run.sh
|
| 39 |
+
./run.sh
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
Default ports:
|
| 43 |
+
- `Dockerfile`: 8501
|
| 44 |
+
- `run.sh`: 8502
|
| 45 |
+
|
| 46 |
+
Logs:
|
| 47 |
+
- Printed to console and saved under `logs/` (handled by the app’s logging system).
|
| 48 |
+
|
| 49 |
+
Tip: You can override ports using Streamlit flags or environment variables, e.g. `STREAMLIT_SERVER_PORT` and `STREAMLIT_SERVER_ADDRESS` (the `run.sh` script sets these before starting).
|
| 50 |
+
|
| 51 |
+
### Run with Docker
|
| 52 |
+
From the `client` directory:
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Build image (example tag)
|
| 56 |
+
docker build -t mcp-playground-client:latest .
|
| 57 |
+
|
| 58 |
+
# Run container (map 8501)
|
| 59 |
+
docker run --rm -it \
|
| 60 |
+
-p 8501:8501 \
|
| 61 |
+
--env-file .env \
|
| 62 |
+
mcp-playground-client:latest
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
The container starts with:
|
| 66 |
+
```bash
|
| 67 |
+
streamlit run app.py --server.port=8501 --server.address=0.0.0.0
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
### Directory Overview (partial)
|
| 71 |
+
- `app.py`: entry point
|
| 72 |
+
- `services/`, `utils/`, `apps/`: business logic and UI
|
| 73 |
+
- `.streamlit/style.css`: styling
|
| 74 |
+
- `servers_config.json`: MCP/services configuration
|
| 75 |
+
- `icons/`, `static/`, `logs/`: assets and logs
|
| 76 |
+
|
| 77 |
+
### FAQ
|
| 78 |
+
- If the port is busy, change `--server.port`.
|
| 79 |
+
- If environment variables are missing, ensure `.env` is in `client` and keys are correct.
|
| 80 |
+
|
client/app.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import asyncio
|
| 3 |
+
import os
|
| 4 |
+
import nest_asyncio
|
| 5 |
+
import atexit
|
| 6 |
+
from services.chat_service import init_session
|
| 7 |
+
from services.logging_service import get_logger
|
| 8 |
+
from services.task_monitor import get_task_monitor
|
| 9 |
+
from utils.async_helpers import on_shutdown
|
| 10 |
+
from apps import mcp_playground
|
| 11 |
+
|
| 12 |
+
# Apply nest_asyncio to allow nested asyncio event loops (needed for Streamlit's execution model)
|
| 13 |
+
nest_asyncio.apply()
|
| 14 |
+
|
| 15 |
+
page_icon_path = os.path.join('.', 'icons', 'playground.png')
|
| 16 |
+
|
| 17 |
+
st.set_page_config(
|
| 18 |
+
page_title="MCP Playground",
|
| 19 |
+
page_icon=(page_icon_path),
|
| 20 |
+
layout='wide',
|
| 21 |
+
initial_sidebar_state="expanded"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Customize css
|
| 25 |
+
with open(os.path.join('.', '.streamlit', 'style.css')) as f:
|
| 26 |
+
st.markdown(f'<style>{f.read()}</style>', unsafe_allow_html=True)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main():
|
| 30 |
+
# Initialize session state for event loop
|
| 31 |
+
if "loop" not in st.session_state:
|
| 32 |
+
st.session_state.loop = asyncio.new_event_loop()
|
| 33 |
+
asyncio.set_event_loop(st.session_state.loop)
|
| 34 |
+
|
| 35 |
+
# Initialize logging system
|
| 36 |
+
logger = get_logger()
|
| 37 |
+
logger.log_system_status("Application started")
|
| 38 |
+
|
| 39 |
+
# Initialize task monitor
|
| 40 |
+
task_monitor = get_task_monitor()
|
| 41 |
+
|
| 42 |
+
# Register shutdown handler
|
| 43 |
+
atexit.register(on_shutdown)
|
| 44 |
+
|
| 45 |
+
# Initialize the primary application
|
| 46 |
+
init_session()
|
| 47 |
+
mcp_playground.main()
|
| 48 |
+
|
| 49 |
+
if __name__ == "__main__":
|
| 50 |
+
main()
|
client/apps/__init__.py
ADDED
|
File without changes
|
client/apps/mcp_playground.py
ADDED
|
@@ -0,0 +1,1353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import streamlit as st
|
| 3 |
+
import base64
|
| 4 |
+
import uuid
|
| 5 |
+
import time
|
| 6 |
+
from langchain_core.messages import HumanMessage, ToolMessage
|
| 7 |
+
from services.ai_service import get_response_stream
|
| 8 |
+
from services.mcp_service import run_agent
|
| 9 |
+
from services.chat_service import get_current_chat, _append_message_to_session
|
| 10 |
+
from services.export_service import export_chat_to_markdown, export_chat_to_json
|
| 11 |
+
from services.logging_service import get_logger
|
| 12 |
+
from services.task_monitor import get_task_monitor
|
| 13 |
+
from utils.async_helpers import run_async
|
| 14 |
+
from utils.ai_prompts import make_system_prompt, make_main_prompt
|
| 15 |
+
import ui_components.sidebar_components as sd_compents
|
| 16 |
+
from ui_components.main_components import display_tool_executions
|
| 17 |
+
from config import DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE
|
| 18 |
+
import traceback
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def replace_citation(match, citation_to_doc, doc_id_to_info):
|
| 22 |
+
"""Replace citation markers with formatted citations"""
|
| 23 |
+
citation_num = int(match.group(1))
|
| 24 |
+
if citation_num in citation_to_doc:
|
| 25 |
+
doc_id = citation_to_doc[citation_num]
|
| 26 |
+
if doc_id in doc_id_to_info:
|
| 27 |
+
doc_info = doc_id_to_info[doc_id]
|
| 28 |
+
title = doc_info.get('title', 'N/A')
|
| 29 |
+
return f"([{citation_num}]({doc_info.get('url', '#')} \"{title}\"))"
|
| 30 |
+
return match.group(0)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def replace_footnote_citation(match, citation_to_doc, doc_id_to_info):
|
| 34 |
+
"""Replace footnote citation markers with formatted citations"""
|
| 35 |
+
citation_num = int(match.group(1))
|
| 36 |
+
if citation_num in citation_to_doc:
|
| 37 |
+
doc_id = citation_to_doc[citation_num]
|
| 38 |
+
if doc_id in doc_id_to_info:
|
| 39 |
+
doc_info = doc_id_to_info[doc_id]
|
| 40 |
+
title = doc_info.get('title', 'N/A')
|
| 41 |
+
return f"([{citation_num}]({doc_info.get('url', '#')} \"{title}\"))"
|
| 42 |
+
return match.group(0)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def replace_document_citation(match, citation_to_doc, doc_id_to_info):
|
| 46 |
+
"""Replace document citation markers with formatted citations"""
|
| 47 |
+
citation_num = int(match.group(1))
|
| 48 |
+
if citation_num in citation_to_doc:
|
| 49 |
+
doc_id = citation_to_doc[citation_num]
|
| 50 |
+
if doc_id in doc_id_to_info:
|
| 51 |
+
doc_info = doc_id_to_info[doc_id]
|
| 52 |
+
title = doc_info.get('title', 'N/A')
|
| 53 |
+
return f"([{citation_num}]({doc_info.get('url', '#')} \"{title}\"))"
|
| 54 |
+
return match.group(0)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def extract_bio_final_answer(raw: str) -> str | None:
|
| 58 |
+
"""
|
| 59 |
+
Extract the final answer from bio_qa_stream_chat ToolMessage text marked with
|
| 60 |
+
'Bio-QA-final-Answer:' (note the Chinese full-width colon).
|
| 61 |
+
Compatible with two scenarios:
|
| 62 |
+
A) SSE stream: Multiple lines containing 'data: {...}' JSON
|
| 63 |
+
B) Plain text/code blocks: First appears ```bio-...``` code block, final answer appears at the end
|
| 64 |
+
Returns plain text answer; returns None if not found.
|
| 65 |
+
"""
|
| 66 |
+
if not raw:
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
marker = "Bio-QA-final-Answer:"
|
| 70 |
+
|
| 71 |
+
# --- Scenario A: SSE line stream (contains 'data:')
|
| 72 |
+
if "data:" in raw:
|
| 73 |
+
final = []
|
| 74 |
+
for line in raw.splitlines():
|
| 75 |
+
line = line.strip()
|
| 76 |
+
if not line.startswith("data: "):
|
| 77 |
+
continue
|
| 78 |
+
# Parse JSON
|
| 79 |
+
try:
|
| 80 |
+
import json
|
| 81 |
+
data = json.loads(line[6:])
|
| 82 |
+
except Exception:
|
| 83 |
+
continue
|
| 84 |
+
if data.get("type") == "result":
|
| 85 |
+
content = str(data.get("content", ""))
|
| 86 |
+
if content.startswith(marker):
|
| 87 |
+
# Remove marker
|
| 88 |
+
final_text = content[len(marker):].strip()
|
| 89 |
+
final.append(final_text)
|
| 90 |
+
elif data.get("type") == "done":
|
| 91 |
+
# End flag, exit directly
|
| 92 |
+
break
|
| 93 |
+
if final:
|
| 94 |
+
# Use the last occurrence (more stable)
|
| 95 |
+
return final[-1].strip()
|
| 96 |
+
|
| 97 |
+
# --- Scenario B: Plain text (does not contain 'data:'), directly find marker
|
| 98 |
+
idx = raw.rfind(marker)
|
| 99 |
+
if idx != -1:
|
| 100 |
+
final_text = raw[idx + len(marker):].strip()
|
| 101 |
+
# Remove possible code fence or extra backticks that might wrap it
|
| 102 |
+
if final_text.startswith("```"):
|
| 103 |
+
# Remove the first code fence
|
| 104 |
+
final_text = final_text.lstrip("`")
|
| 105 |
+
# Also simply remove trailing extra backticks
|
| 106 |
+
final_text = final_text.rstrip("`").strip()
|
| 107 |
+
return final_text or None
|
| 108 |
+
|
| 109 |
+
return None
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def extract_review_final_report(raw: str) -> str | None:
|
| 113 |
+
"""
|
| 114 |
+
Extract the final report content from review_generate ToolMessage text marked with
|
| 115 |
+
'Final_report\n'.
|
| 116 |
+
Compatible with two scenarios:
|
| 117 |
+
A) SSE stream: Multiple lines containing 'data: {...}' JSON
|
| 118 |
+
B) Plain text: Directly find content after Final_report\n marker
|
| 119 |
+
Returns plain text report; returns None if not found.
|
| 120 |
+
"""
|
| 121 |
+
if not raw:
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
marker = "Final_report\n"
|
| 125 |
+
|
| 126 |
+
# --- Scenario A: SSE line stream (contains 'data:')
|
| 127 |
+
if "data:" in raw:
|
| 128 |
+
final_content = []
|
| 129 |
+
found_marker = False
|
| 130 |
+
for line in raw.splitlines():
|
| 131 |
+
line = line.strip()
|
| 132 |
+
if not line.startswith("data: "):
|
| 133 |
+
continue
|
| 134 |
+
# Parse JSON
|
| 135 |
+
try:
|
| 136 |
+
import json
|
| 137 |
+
data = json.loads(line[6:])
|
| 138 |
+
except Exception:
|
| 139 |
+
continue
|
| 140 |
+
if data.get("type") == "result":
|
| 141 |
+
content = str(data.get("content", ""))
|
| 142 |
+
if content == marker:
|
| 143 |
+
found_marker = True
|
| 144 |
+
continue
|
| 145 |
+
elif found_marker:
|
| 146 |
+
# Collect all content after marker
|
| 147 |
+
final_content.append(content)
|
| 148 |
+
elif data.get("type") == "done":
|
| 149 |
+
# End flag, exit directly
|
| 150 |
+
break
|
| 151 |
+
if final_content:
|
| 152 |
+
return "".join(final_content).strip()
|
| 153 |
+
|
| 154 |
+
# --- Scenario B: Plain text (does not contain 'data:'), directly find marker
|
| 155 |
+
idx = raw.find(marker)
|
| 156 |
+
if idx != -1:
|
| 157 |
+
final_text = raw[idx + len(marker):].strip()
|
| 158 |
+
# Remove possible code fence or extra backticks that might wrap it
|
| 159 |
+
if final_text.startswith("```"):
|
| 160 |
+
# Remove the first code fence
|
| 161 |
+
final_text = final_text.lstrip("`")
|
| 162 |
+
# Also simply remove trailing extra backticks
|
| 163 |
+
final_text = final_text.rstrip("`").strip()
|
| 164 |
+
return final_text or None
|
| 165 |
+
|
| 166 |
+
return None
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def create_download_button(content: str, filename: str, file_type: str = "md", tool_type: str = "literature_review"):
|
| 170 |
+
"""
|
| 171 |
+
Create a download button that supports downloading as Markdown or PDF format
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
content: Content to download
|
| 175 |
+
filename: Filename (without extension)
|
| 176 |
+
file_type: File type, 'md' or 'pdf'
|
| 177 |
+
tool_type: Tool type for appropriate filename generation
|
| 178 |
+
"""
|
| 179 |
+
# Ensure a unique key per button instance to avoid duplicate element IDs
|
| 180 |
+
counter = st.session_state.get("download_btn_counter", 0)
|
| 181 |
+
st.session_state["download_btn_counter"] = counter + 1
|
| 182 |
+
base_key = f"download_{tool_type}_{file_type}_{counter}"
|
| 183 |
+
|
| 184 |
+
# Add timestamp to filename
|
| 185 |
+
from datetime import datetime
|
| 186 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 187 |
+
|
| 188 |
+
# Generate appropriate filename based on tool type
|
| 189 |
+
if tool_type == "bio_qa_stream_chat":
|
| 190 |
+
base_filename = "bio_qa_report"
|
| 191 |
+
elif tool_type == "review_generate":
|
| 192 |
+
base_filename = "literature_review"
|
| 193 |
+
else:
|
| 194 |
+
base_filename = filename
|
| 195 |
+
|
| 196 |
+
filename_with_timestamp = f"{base_filename}_{timestamp}"
|
| 197 |
+
|
| 198 |
+
if file_type == "md":
|
| 199 |
+
# Download as Markdown file
|
| 200 |
+
st.download_button(
|
| 201 |
+
label=f"📥 Download as Markdown",
|
| 202 |
+
data=content,
|
| 203 |
+
file_name=f"{filename_with_timestamp}.md",
|
| 204 |
+
mime="text/markdown",
|
| 205 |
+
help="Click to download report as Markdown format",
|
| 206 |
+
key=f"{base_key}_md"
|
| 207 |
+
)
|
| 208 |
+
elif file_type == "pdf":
|
| 209 |
+
try:
|
| 210 |
+
# Use reportlab with markdown parsing (no system dependencies)
|
| 211 |
+
from reportlab.lib.pagesizes import letter, A4
|
| 212 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
| 213 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 214 |
+
from reportlab.lib.units import inch
|
| 215 |
+
from reportlab.lib.enums import TA_JUSTIFY, TA_LEFT
|
| 216 |
+
from io import BytesIO
|
| 217 |
+
import markdown
|
| 218 |
+
|
| 219 |
+
# Convert markdown to HTML first for better parsing
|
| 220 |
+
html_content = markdown.markdown(content, extensions=['tables', 'fenced_code'])
|
| 221 |
+
|
| 222 |
+
# Create PDF document
|
| 223 |
+
buffer = BytesIO()
|
| 224 |
+
doc = SimpleDocTemplate(buffer, pagesize=A4, rightMargin=72, leftMargin=72, topMargin=72, bottomMargin=18)
|
| 225 |
+
|
| 226 |
+
# Get styles
|
| 227 |
+
styles = getSampleStyleSheet()
|
| 228 |
+
|
| 229 |
+
# Create custom styles
|
| 230 |
+
title_style = ParagraphStyle(
|
| 231 |
+
'CustomTitle',
|
| 232 |
+
parent=styles['Heading1'],
|
| 233 |
+
fontSize=16,
|
| 234 |
+
spaceAfter=30,
|
| 235 |
+
alignment=TA_LEFT
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
heading_style = ParagraphStyle(
|
| 239 |
+
'CustomHeading',
|
| 240 |
+
parent=styles['Heading2'],
|
| 241 |
+
fontSize=14,
|
| 242 |
+
spaceAfter=12,
|
| 243 |
+
spaceBefore=20,
|
| 244 |
+
alignment=TA_LEFT
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
body_style = ParagraphStyle(
|
| 248 |
+
'CustomBody',
|
| 249 |
+
parent=styles['Normal'],
|
| 250 |
+
fontSize=11,
|
| 251 |
+
spaceAfter=6,
|
| 252 |
+
alignment=TA_JUSTIFY
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
# Build PDF content
|
| 256 |
+
story = []
|
| 257 |
+
|
| 258 |
+
# Add title based on tool type
|
| 259 |
+
if tool_type == "bio_qa_stream_chat":
|
| 260 |
+
title = "Biological Q&A Report"
|
| 261 |
+
elif tool_type == "review_generate":
|
| 262 |
+
title = "Literature Review Report"
|
| 263 |
+
else:
|
| 264 |
+
title = "Report"
|
| 265 |
+
|
| 266 |
+
story.append(Paragraph(title, title_style))
|
| 267 |
+
story.append(Spacer(1, 12))
|
| 268 |
+
|
| 269 |
+
# Parse HTML content and convert to PDF elements
|
| 270 |
+
from bs4 import BeautifulSoup, NavigableString
|
| 271 |
+
|
| 272 |
+
soup = BeautifulSoup(html_content, 'html.parser')
|
| 273 |
+
|
| 274 |
+
def element_text_with_links(element) -> str:
|
| 275 |
+
parts = []
|
| 276 |
+
for child in element.children:
|
| 277 |
+
if isinstance(child, NavigableString):
|
| 278 |
+
parts.append(str(child))
|
| 279 |
+
elif getattr(child, 'name', None) == 'a':
|
| 280 |
+
href = child.get('href', '#')
|
| 281 |
+
text = child.get_text(strip=True)
|
| 282 |
+
parts.append(f'<link href="{href}">{text}</link>')
|
| 283 |
+
else:
|
| 284 |
+
# Fallback to text for other inline elements
|
| 285 |
+
parts.append(child.get_text(strip=False))
|
| 286 |
+
return ''.join(parts).strip()
|
| 287 |
+
|
| 288 |
+
for element in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li']):
|
| 289 |
+
if element.name in ['h1', 'h2', 'h3']:
|
| 290 |
+
heading_text = element_text_with_links(element)
|
| 291 |
+
story.append(Paragraph(heading_text or element.get_text(), heading_style))
|
| 292 |
+
story.append(Spacer(1, 6))
|
| 293 |
+
elif element.name == 'p':
|
| 294 |
+
text = element_text_with_links(element)
|
| 295 |
+
if text.strip():
|
| 296 |
+
story.append(Paragraph(text, body_style))
|
| 297 |
+
elif element.name == 'code':
|
| 298 |
+
code_style = ParagraphStyle(
|
| 299 |
+
'CodeText',
|
| 300 |
+
parent=body_style,
|
| 301 |
+
fontName='Courier',
|
| 302 |
+
fontSize=10,
|
| 303 |
+
backColor='#f8f9fa'
|
| 304 |
+
)
|
| 305 |
+
story.append(Paragraph(element.get_text(), code_style))
|
| 306 |
+
elif element.name == 'pre':
|
| 307 |
+
pre_style = ParagraphStyle(
|
| 308 |
+
'PreText',
|
| 309 |
+
parent=body_style,
|
| 310 |
+
fontName='Courier',
|
| 311 |
+
fontSize=10,
|
| 312 |
+
backColor='#f8f9fa',
|
| 313 |
+
leftIndent=20
|
| 314 |
+
)
|
| 315 |
+
story.append(Paragraph(element.get_text(), pre_style))
|
| 316 |
+
story.append(Spacer(1, 6))
|
| 317 |
+
elif element.name == 'blockquote':
|
| 318 |
+
quote_style = ParagraphStyle(
|
| 319 |
+
'QuoteText',
|
| 320 |
+
parent=body_style,
|
| 321 |
+
leftIndent=20,
|
| 322 |
+
leftPadding=10,
|
| 323 |
+
borderWidth=1,
|
| 324 |
+
borderColor='#3498db',
|
| 325 |
+
borderPadding=5
|
| 326 |
+
)
|
| 327 |
+
quote_text = element_text_with_links(element)
|
| 328 |
+
story.append(Paragraph(quote_text or element.get_text(), quote_style))
|
| 329 |
+
story.append(Spacer(1, 6))
|
| 330 |
+
elif element.name in ['ul', 'ol']:
|
| 331 |
+
index = 0
|
| 332 |
+
for li in element.find_all('li', recursive=False):
|
| 333 |
+
index += 1
|
| 334 |
+
li_text = element_text_with_links(li)
|
| 335 |
+
bullet = '• ' if element.name == 'ul' else f'{index}. '
|
| 336 |
+
story.append(Paragraph(f'{bullet}{li_text}', body_style))
|
| 337 |
+
story.append(Spacer(1, 6))
|
| 338 |
+
|
| 339 |
+
# Generate PDF
|
| 340 |
+
doc.build(story)
|
| 341 |
+
pdf_bytes = buffer.getvalue()
|
| 342 |
+
buffer.close()
|
| 343 |
+
|
| 344 |
+
# Create download button
|
| 345 |
+
st.download_button(
|
| 346 |
+
label="📥 Download as PDF",
|
| 347 |
+
data=pdf_bytes,
|
| 348 |
+
file_name=f"{filename_with_timestamp}.pdf",
|
| 349 |
+
mime="application/pdf",
|
| 350 |
+
help="Click to download report as PDF format",
|
| 351 |
+
key=f"{base_key}_pdf"
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
except ImportError as e:
|
| 355 |
+
st.warning(f"⚠️ Cannot generate PDF: Missing required libraries. Please install reportlab and beautifulsoup4. Error: {str(e)}")
|
| 356 |
+
except Exception as e:
|
| 357 |
+
st.error(f"❌ Error generating PDF: {str(e)}")
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
def main():
|
| 361 |
+
# Initialize logger
|
| 362 |
+
logger = get_logger()
|
| 363 |
+
task_monitor = get_task_monitor()
|
| 364 |
+
|
| 365 |
+
with st.sidebar:
|
| 366 |
+
st.link_button("🚀 Parameter Extraction", "https://huggingface.co/spaces/jackkuo/Automated-Enzyme-Kinetics-Extractor", type="primary")
|
| 367 |
+
st.subheader("Chat History")
|
| 368 |
+
sd_compents.create_history_chat_container()
|
| 369 |
+
|
| 370 |
+
# ------------------------------------------------------------------ Chat Part
|
| 371 |
+
# Main chat interface
|
| 372 |
+
st.header("Chat with Agent")
|
| 373 |
+
|
| 374 |
+
messages_container = st.container(border=True, height=600)
|
| 375 |
+
# ------------------------------------------------------------------ Chat history
|
| 376 |
+
# Re-render previous messages
|
| 377 |
+
if st.session_state.get('current_chat_id'):
|
| 378 |
+
st.session_state["messages"] = get_current_chat(st.session_state['current_chat_id'])
|
| 379 |
+
tool_count = 0
|
| 380 |
+
|
| 381 |
+
# Debug: log message count
|
| 382 |
+
logger.log_system_status(f"Re-rendering {len(st.session_state['messages'])} messages for chat {st.session_state['current_chat_id']}")
|
| 383 |
+
|
| 384 |
+
# Load bio data for this chat if available
|
| 385 |
+
chat_id = st.session_state['current_chat_id']
|
| 386 |
+
bio_data_key = f"bio_data_{chat_id}"
|
| 387 |
+
bio_data = st.session_state.get(bio_data_key, {})
|
| 388 |
+
|
| 389 |
+
for m in st.session_state["messages"]:
|
| 390 |
+
# Debug: log message structure
|
| 391 |
+
has_tool = "tool" in m and m["tool"]
|
| 392 |
+
has_content = "content" in m and m["content"]
|
| 393 |
+
logger.log_system_status(f"Message: role={m.get('role')}, has_tool={has_tool}, has_content={has_content}")
|
| 394 |
+
|
| 395 |
+
with messages_container.chat_message(m["role"]):
|
| 396 |
+
# 先显示ToolMessage(如果有)
|
| 397 |
+
if "tool" in m and m["tool"]:
|
| 398 |
+
tool_count += 1
|
| 399 |
+
# Display ToolMessage in collapsible format
|
| 400 |
+
with st.expander(f"🔧 ToolMessage - {tool_count}", expanded=False):
|
| 401 |
+
st.code(m["tool"], language='yaml')
|
| 402 |
+
|
| 403 |
+
# 再显示content(如果有)
|
| 404 |
+
if "content" in m and m["content"]:
|
| 405 |
+
content_text = str(m["content"])
|
| 406 |
+
|
| 407 |
+
# Check if this is a bio final answer and restore citations
|
| 408 |
+
if (m["role"] == "assistant" and
|
| 409 |
+
bio_data.get('has_bio_final_answer') and
|
| 410 |
+
bio_data.get('bio_final_answer_content') == content_text):
|
| 411 |
+
|
| 412 |
+
# Restore bio data for citation processing
|
| 413 |
+
bio_search_data = bio_data.get('bio_search_data', [])
|
| 414 |
+
bio_citation_data = bio_data.get('bio_citation_data', [])
|
| 415 |
+
web_search_data = bio_data.get('web_search_data', [])
|
| 416 |
+
|
| 417 |
+
# Display found literature information
|
| 418 |
+
if bio_search_data or web_search_data:
|
| 419 |
+
total_bio_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in bio_search_data)
|
| 420 |
+
total_web_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in web_search_data)
|
| 421 |
+
if total_bio_docs > 0 and total_web_docs > 0:
|
| 422 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers and {total_web_docs} web pages")
|
| 423 |
+
elif total_bio_docs > 0:
|
| 424 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers")
|
| 425 |
+
else:
|
| 426 |
+
st.markdown(f"### 🌐 Analysis based on {total_web_docs} web pages")
|
| 427 |
+
|
| 428 |
+
st.markdown("### 🎯 Final Answer")
|
| 429 |
+
|
| 430 |
+
# Process citation markers in final answer
|
| 431 |
+
processed_answer = content_text
|
| 432 |
+
if bio_citation_data and (bio_search_data or web_search_data):
|
| 433 |
+
# Create docId to literature info mapping
|
| 434 |
+
doc_id_to_info = {}
|
| 435 |
+
# Add PubMed data
|
| 436 |
+
for search_data in bio_search_data:
|
| 437 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 438 |
+
for doc in bio_docs:
|
| 439 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 440 |
+
# Add web search data
|
| 441 |
+
for search_data in web_search_data:
|
| 442 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 443 |
+
for doc in web_docs:
|
| 444 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 445 |
+
|
| 446 |
+
# Create citation number to docId mapping
|
| 447 |
+
citation_to_doc = {}
|
| 448 |
+
for citation in bio_citation_data:
|
| 449 |
+
citation_num = citation.get('citation')
|
| 450 |
+
doc_id = citation.get('docId')
|
| 451 |
+
citation_to_doc[citation_num] = doc_id
|
| 452 |
+
|
| 453 |
+
# Replace citation markers
|
| 454 |
+
import re
|
| 455 |
+
|
| 456 |
+
# First replace single citations
|
| 457 |
+
def replace_citation_local(match):
|
| 458 |
+
return replace_citation(match, citation_to_doc, doc_id_to_info)
|
| 459 |
+
processed_answer = re.sub(r'\[bio-rag-citation:(\d+)\]', replace_citation_local, processed_answer)
|
| 460 |
+
|
| 461 |
+
def replace_footnote_citation_local(match):
|
| 462 |
+
return replace_footnote_citation(match, citation_to_doc, doc_id_to_info)
|
| 463 |
+
processed_answer = re.sub(r'\[\^(\d+)\]', replace_footnote_citation_local, processed_answer)
|
| 464 |
+
|
| 465 |
+
def replace_document_citation_local(match):
|
| 466 |
+
return replace_document_citation(match, citation_to_doc, doc_id_to_info)
|
| 467 |
+
processed_answer = re.sub(r'\[document (\d+)\]', replace_document_citation_local, processed_answer)
|
| 468 |
+
|
| 469 |
+
# Remove bottom references section (since we display complete reference list below)
|
| 470 |
+
processed_answer = re.sub(r'\n\nReferences:.*$', '', processed_answer, flags=re.DOTALL)
|
| 471 |
+
|
| 472 |
+
# Then process consecutive citations, add separators
|
| 473 |
+
processed_answer = re.sub(r'\](\[)', r'], \1', processed_answer)
|
| 474 |
+
|
| 475 |
+
st.markdown(processed_answer)
|
| 476 |
+
|
| 477 |
+
# Display citation information
|
| 478 |
+
if bio_citation_data:
|
| 479 |
+
st.markdown(f"### 📖 References ({len(bio_citation_data)} citations)")
|
| 480 |
+
|
| 481 |
+
# Create docId to literature info mapping
|
| 482 |
+
doc_id_to_info = {}
|
| 483 |
+
# Add PubMed data
|
| 484 |
+
for search_data in bio_search_data:
|
| 485 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 486 |
+
for doc in bio_docs:
|
| 487 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 488 |
+
# Add web search data
|
| 489 |
+
for search_data in web_search_data:
|
| 490 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 491 |
+
for doc in web_docs:
|
| 492 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 493 |
+
|
| 494 |
+
# Display citation list
|
| 495 |
+
for citation in bio_citation_data:
|
| 496 |
+
doc_id = citation.get('docId')
|
| 497 |
+
citation_num = citation.get('citation')
|
| 498 |
+
source = citation.get('source', '')
|
| 499 |
+
|
| 500 |
+
if doc_id in doc_id_to_info:
|
| 501 |
+
doc_info = doc_id_to_info[doc_id]
|
| 502 |
+
title = doc_info.get('title', 'N/A')
|
| 503 |
+
url = doc_info.get('url', '#')
|
| 504 |
+
|
| 505 |
+
if source == 'webSearch':
|
| 506 |
+
st.markdown(f"[{citation_num}] {title}. [Link]({url})")
|
| 507 |
+
else:
|
| 508 |
+
author = doc_info.get('author', 'N/A')
|
| 509 |
+
journal = doc_info.get('JournalInfo', 'N/A')
|
| 510 |
+
|
| 511 |
+
authors = author.split(', ')
|
| 512 |
+
if len(authors) > 3:
|
| 513 |
+
display_author = ', '.join(authors[:3]) + ' et al.'
|
| 514 |
+
else:
|
| 515 |
+
display_author = author
|
| 516 |
+
|
| 517 |
+
st.markdown(f"[{citation_num}] {display_author}. {title}. {journal}. [Link]({url})")
|
| 518 |
+
else:
|
| 519 |
+
st.markdown(f"[{citation_num}] Document ID: {doc_id}")
|
| 520 |
+
else:
|
| 521 |
+
# Normal content display
|
| 522 |
+
st.markdown(content_text)
|
| 523 |
+
|
| 524 |
+
# Check if this is a review report and add download buttons
|
| 525 |
+
if m["role"] == "assistant" and m["content"]:
|
| 526 |
+
# Try to detect if this is a literature review report
|
| 527 |
+
content_text = str(m["content"])
|
| 528 |
+
if ("Literature Review Report" in content_text or
|
| 529 |
+
"📚 Literature Review Report" in content_text or
|
| 530 |
+
len(content_text) > 500): # Assume long content might be a review report
|
| 531 |
+
# Add download buttons for review reports
|
| 532 |
+
st.markdown("---")
|
| 533 |
+
st.markdown("### 📥 Download Options")
|
| 534 |
+
col1, col2 = st.columns(2)
|
| 535 |
+
with col1:
|
| 536 |
+
create_download_button(content_text, "literature_review", "md", "bio_qa_stream_chat")
|
| 537 |
+
with col2:
|
| 538 |
+
create_download_button(content_text, "literature_review", "pdf", "bio_qa_stream_chat")
|
| 539 |
+
|
| 540 |
+
# ------------------------------------------------------------------ Chat input
|
| 541 |
+
user_text = st.chat_input("Ask a question or explore available MCP tools")
|
| 542 |
+
|
| 543 |
+
# ------------------------------------------------------------------ SideBar widgets
|
| 544 |
+
# Main sidebar widgets
|
| 545 |
+
sd_compents.create_sidebar_chat_buttons()
|
| 546 |
+
sd_compents.create_provider_select_widget()
|
| 547 |
+
sd_compents.create_advanced_configuration_widget()
|
| 548 |
+
sd_compents.create_mcp_connection_widget()
|
| 549 |
+
sd_compents.create_mcp_tools_widget()
|
| 550 |
+
|
| 551 |
+
# ------------------------------------------------------------------ Main Logic
|
| 552 |
+
if user_text is None: # nothing submitted yet
|
| 553 |
+
st.stop()
|
| 554 |
+
|
| 555 |
+
params = st.session_state.get('params')
|
| 556 |
+
if not (
|
| 557 |
+
params.get('api_key') or
|
| 558 |
+
( params.get('model_id') == 'Bedrock' and
|
| 559 |
+
params.get('region_name') and
|
| 560 |
+
params.get('aws_access_key') and
|
| 561 |
+
params.get('aws_secret_key')
|
| 562 |
+
)
|
| 563 |
+
):
|
| 564 |
+
err_mesg = "❌ Missing credentials: provide either an API key or complete AWS credentials."
|
| 565 |
+
_append_message_to_session({"role": "assistant", "content": err_mesg})
|
| 566 |
+
with messages_container.chat_message("assistant"):
|
| 567 |
+
st.markdown(err_mesg)
|
| 568 |
+
st.rerun()
|
| 569 |
+
|
| 570 |
+
# ------------------------------------------------------------------ handle question (if any text)
|
| 571 |
+
if user_text:
|
| 572 |
+
# Log user message
|
| 573 |
+
logger.log_chat_message("user", user_text, st.session_state.get('current_chat_id'))
|
| 574 |
+
|
| 575 |
+
user_text_dct = {"role": "user", "content": user_text}
|
| 576 |
+
_append_message_to_session(user_text_dct)
|
| 577 |
+
with messages_container.chat_message("user"):
|
| 578 |
+
st.markdown(user_text)
|
| 579 |
+
|
| 580 |
+
with st.spinner("Thinking…", show_time=True):
|
| 581 |
+
# Start monitoring long-running task
|
| 582 |
+
task_id = str(uuid.uuid4())
|
| 583 |
+
task_monitor.start_monitoring(
|
| 584 |
+
task_id,
|
| 585 |
+
f"MCP_Agent_Response_{st.session_state.get('current_chat_id', 'unknown')}",
|
| 586 |
+
st.session_state.get('current_chat_id')
|
| 587 |
+
)
|
| 588 |
+
|
| 589 |
+
start_time = time.time()
|
| 590 |
+
system_prompt = make_system_prompt()
|
| 591 |
+
main_prompt = make_main_prompt(user_text)
|
| 592 |
+
try:
|
| 593 |
+
# If agent is available, use it
|
| 594 |
+
if st.session_state.agent:
|
| 595 |
+
logger.log_system_status("Using MCP agent for response")
|
| 596 |
+
|
| 597 |
+
# 记录可用的MCP工具
|
| 598 |
+
available_tools = [tool.name for tool in st.session_state.tools]
|
| 599 |
+
logger.log_mcp_agent_usage("ReactAgent", available_tools, st.session_state.get('current_chat_id'))
|
| 600 |
+
|
| 601 |
+
response = run_async(run_agent(st.session_state.agent, user_text))
|
| 602 |
+
tool_output = None
|
| 603 |
+
tools_used_in_response = []
|
| 604 |
+
|
| 605 |
+
# Extract tool executions if available
|
| 606 |
+
if "messages" in response:
|
| 607 |
+
logger.log_system_status(f"Processing {len(response['messages'])} messages from agent response")
|
| 608 |
+
for msg in response["messages"]:
|
| 609 |
+
# Debug: log message type
|
| 610 |
+
msg_type = type(msg).__name__
|
| 611 |
+
logger.log_system_status(f"Processing message type: {msg_type}")
|
| 612 |
+
|
| 613 |
+
# Look for AIMessage with tool calls
|
| 614 |
+
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
| 615 |
+
logger.log_system_status(f"Found tool calls: {msg.tool_calls}")
|
| 616 |
+
for tool_call in msg.tool_calls:
|
| 617 |
+
tools_used_in_response.append(tool_call['name'])
|
| 618 |
+
|
| 619 |
+
# Log tool call
|
| 620 |
+
logger.log_mcp_tool_call(
|
| 621 |
+
tool_call['name'],
|
| 622 |
+
tool_call['args'],
|
| 623 |
+
st.session_state.get('current_chat_id')
|
| 624 |
+
)
|
| 625 |
+
|
| 626 |
+
# Find corresponding ToolMessage
|
| 627 |
+
tool_output = next(
|
| 628 |
+
(m.content for m in response["messages"]
|
| 629 |
+
if isinstance(m, ToolMessage) and
|
| 630 |
+
m.tool_call_id == tool_call['id']),
|
| 631 |
+
None
|
| 632 |
+
)
|
| 633 |
+
if tool_output:
|
| 634 |
+
# Log tool response
|
| 635 |
+
logger.log_mcp_tool_response(
|
| 636 |
+
tool_call['name'],
|
| 637 |
+
tool_output,
|
| 638 |
+
st.session_state.get('current_chat_id')
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
st.session_state.tool_executions.append({
|
| 642 |
+
"tool_name": tool_call['name'],
|
| 643 |
+
"input": tool_call['args'],
|
| 644 |
+
"output": tool_output,
|
| 645 |
+
"timestamp": datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 646 |
+
})
|
| 647 |
+
elif hasattr(msg, 'name') and msg.name:
|
| 648 |
+
logger.log_system_status(f"Found ToolMessage: {msg.name}")
|
| 649 |
+
else:
|
| 650 |
+
logger.log_system_status(f"Message has no tool calls or name: {msg}")
|
| 651 |
+
|
| 652 |
+
# 记录实际使用的工具
|
| 653 |
+
if tools_used_in_response:
|
| 654 |
+
logger.log_mcp_agent_usage("Response", tools_used_in_response, st.session_state.get('current_chat_id'))
|
| 655 |
+
else:
|
| 656 |
+
logger.log_system_status("No MCP tools used in this response")
|
| 657 |
+
# Extract and display the response
|
| 658 |
+
output = ""
|
| 659 |
+
tool_count = 0
|
| 660 |
+
# Initialize bio QA data from session state or create new
|
| 661 |
+
chat_id = st.session_state.get('current_chat_id')
|
| 662 |
+
bio_data_key = f"bio_data_{chat_id}" if chat_id else "bio_data_default"
|
| 663 |
+
|
| 664 |
+
if bio_data_key not in st.session_state:
|
| 665 |
+
st.session_state[bio_data_key] = {
|
| 666 |
+
'bio_final_answer_content': "",
|
| 667 |
+
'has_bio_final_answer': False,
|
| 668 |
+
'review_final_report_content': "",
|
| 669 |
+
'has_review_final_report': False,
|
| 670 |
+
'bio_search_data': [],
|
| 671 |
+
'bio_citation_data': [],
|
| 672 |
+
'web_search_data': []
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
# Load existing data or initialize new
|
| 676 |
+
bio_data = st.session_state[bio_data_key]
|
| 677 |
+
bio_final_answer_content = bio_data['bio_final_answer_content']
|
| 678 |
+
has_bio_final_answer = bio_data['has_bio_final_answer']
|
| 679 |
+
review_final_report_content = bio_data['review_final_report_content']
|
| 680 |
+
has_review_final_report = bio_data['has_review_final_report']
|
| 681 |
+
bio_search_data = bio_data['bio_search_data']
|
| 682 |
+
bio_citation_data = bio_data['bio_citation_data']
|
| 683 |
+
web_search_data = bio_data['web_search_data']
|
| 684 |
+
|
| 685 |
+
if "messages" in response:
|
| 686 |
+
for msg in response["messages"]:
|
| 687 |
+
if isinstance(msg, HumanMessage):
|
| 688 |
+
continue # Skip human messages
|
| 689 |
+
elif hasattr(msg, 'name') and msg.name: # ToolMessage
|
| 690 |
+
tool_count += 1
|
| 691 |
+
with messages_container.chat_message("assistant"):
|
| 692 |
+
# Parse SSE stream data if it's a streaming tool response
|
| 693 |
+
if (msg.name == "bio_qa_stream_chat" or msg.name == "review_generate" or msg.name == "health_check") and "data:" in msg.content:
|
| 694 |
+
if msg.name == "bio_qa_stream_chat":
|
| 695 |
+
st.write("**🔬 Biological Q&A Results:**")
|
| 696 |
+
elif msg.name == "review_generate":
|
| 697 |
+
st.write("**📚 Literature Review Generation:**")
|
| 698 |
+
elif msg.name == "health_check":
|
| 699 |
+
st.write("**🏥 Health Check Results:**")
|
| 700 |
+
|
| 701 |
+
# Parse and display streaming content
|
| 702 |
+
lines = msg.content.split('\n')
|
| 703 |
+
handled_final_answer = False
|
| 704 |
+
handled_final_report = False
|
| 705 |
+
final_report_content = []
|
| 706 |
+
for line in lines:
|
| 707 |
+
if line.startswith('data: '):
|
| 708 |
+
try:
|
| 709 |
+
import json
|
| 710 |
+
data = json.loads(line[6:]) # Remove 'data: ' prefix
|
| 711 |
+
if data.get('type') == 'result':
|
| 712 |
+
content = data.get('content', '')
|
| 713 |
+
# Check if this is a final answer
|
| 714 |
+
if content.startswith("Bio-QA-final-Answer:") and not handled_final_answer:
|
| 715 |
+
# Extract final answer content
|
| 716 |
+
bio_final_answer_content = content.replace("Bio-QA-final-Answer:", "").strip()
|
| 717 |
+
# Save to session state
|
| 718 |
+
bio_data['bio_final_answer_content'] = bio_final_answer_content
|
| 719 |
+
bio_data['has_bio_final_answer'] = True
|
| 720 |
+
st.session_state[bio_data_key] = bio_data
|
| 721 |
+
|
| 722 |
+
# Set as main output
|
| 723 |
+
output = bio_final_answer_content
|
| 724 |
+
# Set flag to skip LLM processing
|
| 725 |
+
has_bio_final_answer = True
|
| 726 |
+
# Display final answer immediately in main conversation area
|
| 727 |
+
st.markdown("---")
|
| 728 |
+
# Display found literature information
|
| 729 |
+
if bio_search_data or web_search_data:
|
| 730 |
+
total_bio_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in bio_search_data)
|
| 731 |
+
total_web_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in web_search_data)
|
| 732 |
+
# total_docs = total_bio_docs + total_web_docs
|
| 733 |
+
if total_bio_docs > 0 and total_web_docs > 0:
|
| 734 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers and {total_web_docs} web pages")
|
| 735 |
+
elif total_bio_docs > 0:
|
| 736 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers")
|
| 737 |
+
else:
|
| 738 |
+
st.markdown(f"### 🌐 Analysis based on {total_web_docs} web pages")
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
st.markdown("### 🎯 Final Answer")
|
| 743 |
+
|
| 744 |
+
# Process citation markers in final answer
|
| 745 |
+
processed_answer = bio_final_answer_content
|
| 746 |
+
if bio_citation_data and (bio_search_data or web_search_data):
|
| 747 |
+
# Create docId to literature info mapping
|
| 748 |
+
doc_id_to_info = {}
|
| 749 |
+
# Add PubMed data
|
| 750 |
+
for search_data in bio_search_data:
|
| 751 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 752 |
+
for doc in bio_docs:
|
| 753 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 754 |
+
# Add web search data
|
| 755 |
+
for search_data in web_search_data:
|
| 756 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 757 |
+
for doc in web_docs:
|
| 758 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 759 |
+
|
| 760 |
+
# Create citation number to docId mapping
|
| 761 |
+
citation_to_doc = {}
|
| 762 |
+
for citation in bio_citation_data:
|
| 763 |
+
citation_num = citation.get('citation')
|
| 764 |
+
doc_id = citation.get('docId')
|
| 765 |
+
citation_to_doc[citation_num] = doc_id
|
| 766 |
+
|
| 767 |
+
# Replace citation markers
|
| 768 |
+
import re
|
| 769 |
+
# First replace single citations
|
| 770 |
+
processed_answer = re.sub(r'\[bio-rag-citation:(\d+)\]', replace_citation, processed_answer)
|
| 771 |
+
|
| 772 |
+
processed_answer = re.sub(r'\[\^(\d+)\]', replace_footnote_citation, processed_answer)
|
| 773 |
+
|
| 774 |
+
processed_answer = re.sub(r'\[document (\d+)\]', replace_document_citation, processed_answer)
|
| 775 |
+
|
| 776 |
+
# Remove bottom references section (since we display complete reference list below)
|
| 777 |
+
processed_answer = re.sub(r'\n\nReferences:.*$', '', processed_answer, flags=re.DOTALL)
|
| 778 |
+
|
| 779 |
+
# Then process consecutive citations, add separators
|
| 780 |
+
processed_answer = re.sub(r'\](\[)', r'], \1', processed_answer)
|
| 781 |
+
|
| 782 |
+
st.markdown(processed_answer)
|
| 783 |
+
|
| 784 |
+
# Display citation information (moved below final answer)
|
| 785 |
+
if bio_citation_data:
|
| 786 |
+
st.markdown(f"### 📖 References ({len(bio_citation_data)} citations)")
|
| 787 |
+
|
| 788 |
+
# Create docId to literature info mapping
|
| 789 |
+
doc_id_to_info = {}
|
| 790 |
+
# Add PubMed data
|
| 791 |
+
for search_data in bio_search_data:
|
| 792 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 793 |
+
for doc in bio_docs:
|
| 794 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 795 |
+
# Add web search data
|
| 796 |
+
for search_data in web_search_data:
|
| 797 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 798 |
+
for doc in web_docs:
|
| 799 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 800 |
+
|
| 801 |
+
# Display citation list, associate with literature info (standard reference format)
|
| 802 |
+
for citation in bio_citation_data:
|
| 803 |
+
doc_id = citation.get('docId')
|
| 804 |
+
citation_num = citation.get('citation')
|
| 805 |
+
source = citation.get('source', '')
|
| 806 |
+
|
| 807 |
+
if doc_id in doc_id_to_info:
|
| 808 |
+
doc_info = doc_id_to_info[doc_id]
|
| 809 |
+
title = doc_info.get('title', 'N/A')
|
| 810 |
+
url = doc_info.get('url', '#')
|
| 811 |
+
|
| 812 |
+
if source == 'webSearch':
|
| 813 |
+
# Web citation format: [number] title. [link](URL)
|
| 814 |
+
st.markdown(f"[{citation_num}] {title}. [Link]({url})")
|
| 815 |
+
else:
|
| 816 |
+
# PubMed literature citation format: [number] author. title. journal info. [link](URL)
|
| 817 |
+
author = doc_info.get('author', 'N/A')
|
| 818 |
+
journal = doc_info.get('JournalInfo', 'N/A')
|
| 819 |
+
|
| 820 |
+
# Process author info, only show first 3
|
| 821 |
+
authors = author.split(', ')
|
| 822 |
+
if len(authors) > 3:
|
| 823 |
+
display_author = ', '.join(authors[:3]) + ' et al.'
|
| 824 |
+
else:
|
| 825 |
+
display_author = author
|
| 826 |
+
|
| 827 |
+
st.markdown(f"[{citation_num}] {display_author}. {title}. {journal}. [Link]({url})")
|
| 828 |
+
else:
|
| 829 |
+
st.markdown(f"[{citation_num}] Document ID: {doc_id}")
|
| 830 |
+
|
| 831 |
+
# Build complete content for download (including references)
|
| 832 |
+
complete_content = ""
|
| 833 |
+
|
| 834 |
+
# Add analysis information
|
| 835 |
+
if bio_search_data or web_search_data:
|
| 836 |
+
total_bio_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in bio_search_data)
|
| 837 |
+
total_web_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in web_search_data)
|
| 838 |
+
if total_bio_docs > 0 and total_web_docs > 0:
|
| 839 |
+
complete_content += f"### 📚 Analysis based on {total_bio_docs} scientific papers and {total_web_docs} web pages\n\n"
|
| 840 |
+
elif total_bio_docs > 0:
|
| 841 |
+
complete_content += f"### 📚 Analysis based on {total_bio_docs} scientific papers\n\n"
|
| 842 |
+
else:
|
| 843 |
+
complete_content += f"### 🌐 Analysis based on {total_web_docs} web pages\n\n"
|
| 844 |
+
|
| 845 |
+
# Add final answer
|
| 846 |
+
complete_content += "### 🎯 Final Answer\n\n"
|
| 847 |
+
complete_content += processed_answer + "\n\n"
|
| 848 |
+
|
| 849 |
+
# Add references
|
| 850 |
+
if bio_citation_data:
|
| 851 |
+
complete_content += f"### 📖 References ({len(bio_citation_data)} citations)\n\n"
|
| 852 |
+
|
| 853 |
+
# Create docId to literature info mapping
|
| 854 |
+
doc_id_to_info = {}
|
| 855 |
+
# Add PubMed data
|
| 856 |
+
for search_data in bio_search_data:
|
| 857 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 858 |
+
for doc in bio_docs:
|
| 859 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 860 |
+
# Add web search data
|
| 861 |
+
for search_data in web_search_data:
|
| 862 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 863 |
+
for doc in web_docs:
|
| 864 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 865 |
+
|
| 866 |
+
# Add citation list to complete content
|
| 867 |
+
for citation in bio_citation_data:
|
| 868 |
+
doc_id = citation.get('docId')
|
| 869 |
+
citation_num = citation.get('citation')
|
| 870 |
+
source = citation.get('source', '')
|
| 871 |
+
|
| 872 |
+
if doc_id in doc_id_to_info:
|
| 873 |
+
doc_info = doc_id_to_info[doc_id]
|
| 874 |
+
title = doc_info.get('title', 'N/A')
|
| 875 |
+
url = doc_info.get('url', '#')
|
| 876 |
+
|
| 877 |
+
if source == 'webSearch':
|
| 878 |
+
complete_content += f"[{citation_num}] {title}. [Link]({url})\n\n"
|
| 879 |
+
else:
|
| 880 |
+
author = doc_info.get('author', 'N/A')
|
| 881 |
+
journal = doc_info.get('JournalInfo', 'N/A')
|
| 882 |
+
|
| 883 |
+
authors = author.split(', ')
|
| 884 |
+
if len(authors) > 3:
|
| 885 |
+
display_author = ', '.join(authors[:3]) + ' et al.'
|
| 886 |
+
else:
|
| 887 |
+
display_author = author
|
| 888 |
+
|
| 889 |
+
complete_content += f"[{citation_num}] {display_author}. {title}. {journal}. [Link]({url})\n\n"
|
| 890 |
+
else:
|
| 891 |
+
complete_content += f"[{citation_num}] Document ID: {doc_id}\n\n"
|
| 892 |
+
|
| 893 |
+
# Add download buttons for Bio QA final answer (with complete content)
|
| 894 |
+
st.markdown("---")
|
| 895 |
+
st.markdown("### 📥 Download Options")
|
| 896 |
+
col1, col2 = st.columns(2)
|
| 897 |
+
with col1:
|
| 898 |
+
create_download_button(complete_content, "bio_qa_report", "md", "bio_qa_stream_chat")
|
| 899 |
+
with col2:
|
| 900 |
+
create_download_button(complete_content, "bio_qa_report", "pdf", "bio_qa_stream_chat")
|
| 901 |
+
|
| 902 |
+
# Save complete content to session history
|
| 903 |
+
_append_message_to_session({'role': 'assistant', 'content': complete_content})
|
| 904 |
+
|
| 905 |
+
handled_final_answer = True
|
| 906 |
+
# Check if this is a final report marker
|
| 907 |
+
elif content == "Final_report\n" and not handled_final_report:
|
| 908 |
+
handled_final_report = True
|
| 909 |
+
# Start collecting final report content
|
| 910 |
+
continue
|
| 911 |
+
elif handled_final_report:
|
| 912 |
+
# Collect final report content
|
| 913 |
+
final_report_content.append(content)
|
| 914 |
+
else:
|
| 915 |
+
# Try to parse JSON data and store
|
| 916 |
+
try:
|
| 917 |
+
import json
|
| 918 |
+
json_data = json.loads(content)
|
| 919 |
+
if json_data.get("type") == "search" and json_data.get("handler") == "QASearch":
|
| 920 |
+
handler_param = json_data.get('handlerParam', {})
|
| 921 |
+
source = handler_param.get('source', '')
|
| 922 |
+
if source == 'pubmed':
|
| 923 |
+
bio_search_data.append(json_data)
|
| 924 |
+
# Save to session state
|
| 925 |
+
bio_data['bio_search_data'] = bio_search_data
|
| 926 |
+
st.session_state[bio_data_key] = bio_data
|
| 927 |
+
st.write(f"🔍 Found {len(handler_param.get('bioDocs', []))} relevant papers")
|
| 928 |
+
elif source == 'webSearch':
|
| 929 |
+
web_search_data.append(json_data)
|
| 930 |
+
# Save to session state
|
| 931 |
+
bio_data['web_search_data'] = web_search_data
|
| 932 |
+
st.session_state[bio_data_key] = bio_data
|
| 933 |
+
st.write(f"🌐 Found {len(handler_param.get('bioDocs', []))} relevant web pages")
|
| 934 |
+
elif isinstance(json_data, list) and len(json_data) > 0 and "source" in json_data[0] and "citation" in json_data[0]:
|
| 935 |
+
# This is citation data
|
| 936 |
+
bio_citation_data.extend(json_data)
|
| 937 |
+
# Save to session state
|
| 938 |
+
bio_data['bio_citation_data'] = bio_citation_data
|
| 939 |
+
st.session_state[bio_data_key] = bio_data
|
| 940 |
+
st.write(f"📝 Generated citation information, {len(json_data)} citations total")
|
| 941 |
+
else:
|
| 942 |
+
st.write(content)
|
| 943 |
+
except json.JSONDecodeError:
|
| 944 |
+
# If not JSON, display content normally
|
| 945 |
+
st.write(content)
|
| 946 |
+
elif data.get('type') == 'done':
|
| 947 |
+
st.success("✅ Answer completed")
|
| 948 |
+
except json.JSONDecodeError:
|
| 949 |
+
continue
|
| 950 |
+
|
| 951 |
+
# Process collected final report content
|
| 952 |
+
if handled_final_report and final_report_content:
|
| 953 |
+
review_final_report_content = "".join(final_report_content).strip()
|
| 954 |
+
|
| 955 |
+
# Always display ToolMessage (collapsible)
|
| 956 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 957 |
+
st.code(msg.content, language='yaml')
|
| 958 |
+
|
| 959 |
+
# Display final report in main conversation area
|
| 960 |
+
with messages_container.chat_message("assistant"):
|
| 961 |
+
st.markdown("---")
|
| 962 |
+
st.markdown("### 📚 Literature Review Report")
|
| 963 |
+
st.markdown(review_final_report_content)
|
| 964 |
+
|
| 965 |
+
# Add download buttons to main conversation area (persistent)
|
| 966 |
+
st.markdown("---")
|
| 967 |
+
st.markdown("### 📥 Download Options")
|
| 968 |
+
col1, col2 = st.columns(2)
|
| 969 |
+
with col1:
|
| 970 |
+
create_download_button(review_final_report_content, "literature_review", "md", "review_generate")
|
| 971 |
+
with col2:
|
| 972 |
+
create_download_button(review_final_report_content, "literature_review", "pdf", "review_generate")
|
| 973 |
+
|
| 974 |
+
# Set flags and output
|
| 975 |
+
has_review_final_report = True
|
| 976 |
+
output = review_final_report_content
|
| 977 |
+
|
| 978 |
+
# Save final report to session history with download buttons info
|
| 979 |
+
_append_message_to_session({'role': 'assistant', 'content': review_final_report_content})
|
| 980 |
+
# Also save the original ToolMessage for reference
|
| 981 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 982 |
+
else:
|
| 983 |
+
# Save tool message to session history
|
| 984 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 985 |
+
st.code(msg.content, language='yaml')
|
| 986 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 987 |
+
else:
|
| 988 |
+
# For non-streaming or non-SSE returned tool messages, prioritize parsing bio_qa_stream_chat final answer
|
| 989 |
+
if msg.name == "bio_qa_stream_chat":
|
| 990 |
+
# Try to extract search data
|
| 991 |
+
try:
|
| 992 |
+
import json
|
| 993 |
+
import re
|
| 994 |
+
# Find JSON data blocks
|
| 995 |
+
json_matches = re.findall(r'```bio-chat-agent-task\n(.*?)\n```', msg.content, re.DOTALL)
|
| 996 |
+
for json_str in json_matches:
|
| 997 |
+
try:
|
| 998 |
+
json_data = json.loads(json_str)
|
| 999 |
+
if json_data.get("type") == "search" and json_data.get("handler") == "QASearch":
|
| 1000 |
+
handler_param = json_data.get('handlerParam', {})
|
| 1001 |
+
source = handler_param.get('source', '')
|
| 1002 |
+
if source == 'pubmed':
|
| 1003 |
+
bio_search_data.append(json_data)
|
| 1004 |
+
# Save to session state
|
| 1005 |
+
bio_data['bio_search_data'] = bio_search_data
|
| 1006 |
+
st.session_state[bio_data_key] = bio_data
|
| 1007 |
+
elif source == 'webSearch':
|
| 1008 |
+
web_search_data.append(json_data)
|
| 1009 |
+
# Save to session state
|
| 1010 |
+
bio_data['web_search_data'] = web_search_data
|
| 1011 |
+
st.session_state[bio_data_key] = bio_data
|
| 1012 |
+
except json.JSONDecodeError:
|
| 1013 |
+
continue
|
| 1014 |
+
|
| 1015 |
+
# Find citation data blocks
|
| 1016 |
+
citation_matches = re.findall(r'```bio-resource-lookup\n(.*?)\n```', msg.content, re.DOTALL)
|
| 1017 |
+
for citation_str in citation_matches:
|
| 1018 |
+
try:
|
| 1019 |
+
citation_data = json.loads(citation_str)
|
| 1020 |
+
if isinstance(citation_data, list) and len(citation_data) > 0 and "source" in citation_data[0] and "citation" in citation_data[0]:
|
| 1021 |
+
bio_citation_data.extend(citation_data)
|
| 1022 |
+
# Save to session state
|
| 1023 |
+
bio_data['bio_citation_data'] = bio_citation_data
|
| 1024 |
+
st.session_state[bio_data_key] = bio_data
|
| 1025 |
+
except json.JSONDecodeError:
|
| 1026 |
+
continue
|
| 1027 |
+
except Exception:
|
| 1028 |
+
pass
|
| 1029 |
+
|
| 1030 |
+
extracted = extract_bio_final_answer(msg.content)
|
| 1031 |
+
if extracted:
|
| 1032 |
+
# Always display ToolMessage (collapsible)
|
| 1033 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 1034 |
+
st.code(msg.content, language='yaml')
|
| 1035 |
+
|
| 1036 |
+
# Then display final answer in main conversation area
|
| 1037 |
+
with messages_container.chat_message("assistant"):
|
| 1038 |
+
# Display found literature information
|
| 1039 |
+
if bio_search_data or web_search_data:
|
| 1040 |
+
total_bio_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in bio_search_data)
|
| 1041 |
+
total_web_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in web_search_data)
|
| 1042 |
+
total_docs = total_bio_docs + total_web_docs
|
| 1043 |
+
if total_bio_docs > 0 and total_web_docs > 0:
|
| 1044 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers and {total_web_docs} web pages")
|
| 1045 |
+
elif total_bio_docs > 0:
|
| 1046 |
+
st.markdown(f"### 📚 Analysis based on {total_bio_docs} scientific papers")
|
| 1047 |
+
else:
|
| 1048 |
+
st.markdown(f"### 🌐 Analysis based on {total_web_docs} web pages")
|
| 1049 |
+
|
| 1050 |
+
|
| 1051 |
+
|
| 1052 |
+
st.markdown("### 🎯 Final Answer")
|
| 1053 |
+
|
| 1054 |
+
# Process citation markers in final answer
|
| 1055 |
+
processed_answer = extracted
|
| 1056 |
+
if bio_citation_data and (bio_search_data or web_search_data):
|
| 1057 |
+
# Create docId to literature info mapping
|
| 1058 |
+
doc_id_to_info = {}
|
| 1059 |
+
# Add PubMed data
|
| 1060 |
+
for search_data in bio_search_data:
|
| 1061 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1062 |
+
for doc in bio_docs:
|
| 1063 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1064 |
+
# Add web search data
|
| 1065 |
+
for search_data in web_search_data:
|
| 1066 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1067 |
+
for doc in web_docs:
|
| 1068 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1069 |
+
|
| 1070 |
+
# Create citation number to docId mapping
|
| 1071 |
+
citation_to_doc = {}
|
| 1072 |
+
for citation in bio_citation_data:
|
| 1073 |
+
citation_num = citation.get('citation')
|
| 1074 |
+
doc_id = citation.get('docId')
|
| 1075 |
+
citation_to_doc[citation_num] = doc_id
|
| 1076 |
+
|
| 1077 |
+
# Replace citation markers
|
| 1078 |
+
import re
|
| 1079 |
+
# First replace single citations
|
| 1080 |
+
def replace_citation_local2(match):
|
| 1081 |
+
return replace_citation(match, citation_to_doc, doc_id_to_info)
|
| 1082 |
+
processed_answer = re.sub(r'\[bio-rag-citation:(\d+)\]', replace_citation_local2, processed_answer)
|
| 1083 |
+
|
| 1084 |
+
def replace_footnote_citation_local2(match):
|
| 1085 |
+
return replace_footnote_citation(match, citation_to_doc, doc_id_to_info)
|
| 1086 |
+
processed_answer = re.sub(r'\[\^(\d+)\]', replace_footnote_citation_local2, processed_answer)
|
| 1087 |
+
|
| 1088 |
+
def replace_document_citation_local2(match):
|
| 1089 |
+
return replace_document_citation(match, citation_to_doc, doc_id_to_info)
|
| 1090 |
+
processed_answer = re.sub(r'\[document (\d+)\]', replace_document_citation_local2, processed_answer)
|
| 1091 |
+
|
| 1092 |
+
# Remove bottom references section (since we display complete reference list below)
|
| 1093 |
+
processed_answer = re.sub(r'\n\nReferences:.*$', '', processed_answer, flags=re.DOTALL)
|
| 1094 |
+
|
| 1095 |
+
# Then process consecutive citations, add separators
|
| 1096 |
+
processed_answer = re.sub(r'\](\[)', r'], \1', processed_answer)
|
| 1097 |
+
|
| 1098 |
+
st.markdown(processed_answer)
|
| 1099 |
+
|
| 1100 |
+
# Display citation information (moved below final answer)
|
| 1101 |
+
if bio_citation_data:
|
| 1102 |
+
st.markdown(f"### 📖 References ({len(bio_citation_data)} citations)")
|
| 1103 |
+
|
| 1104 |
+
# Create docId to literature info mapping
|
| 1105 |
+
doc_id_to_info = {}
|
| 1106 |
+
# Add PubMed data
|
| 1107 |
+
for search_data in bio_search_data:
|
| 1108 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1109 |
+
for doc in bio_docs:
|
| 1110 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1111 |
+
# Add web search data
|
| 1112 |
+
for search_data in web_search_data:
|
| 1113 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1114 |
+
for doc in web_docs:
|
| 1115 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1116 |
+
|
| 1117 |
+
# Display citation list, associate with literature info (standard reference format)
|
| 1118 |
+
for citation in bio_citation_data:
|
| 1119 |
+
doc_id = citation.get('docId')
|
| 1120 |
+
citation_num = citation.get('citation')
|
| 1121 |
+
source = citation.get('source', '')
|
| 1122 |
+
|
| 1123 |
+
if doc_id in doc_id_to_info:
|
| 1124 |
+
doc_info = doc_id_to_info[doc_id]
|
| 1125 |
+
title = doc_info.get('title', 'N/A')
|
| 1126 |
+
url = doc_info.get('url', '#')
|
| 1127 |
+
|
| 1128 |
+
if source == 'webSearch':
|
| 1129 |
+
# Web citation format: [number] title. [link](URL)
|
| 1130 |
+
st.markdown(f"[{citation_num}] {title}. [Link]({url})")
|
| 1131 |
+
else:
|
| 1132 |
+
# PubMed literature citation format: [number] author. title. journal info. [link](URL)
|
| 1133 |
+
author = doc_info.get('author', 'N/A')
|
| 1134 |
+
journal = doc_info.get('JournalInfo', 'N/A')
|
| 1135 |
+
|
| 1136 |
+
# Process author info, only show first 3
|
| 1137 |
+
authors = author.split(', ')
|
| 1138 |
+
if len(authors) > 3:
|
| 1139 |
+
display_author = ', '.join(authors[:3]) + ' et al.'
|
| 1140 |
+
else:
|
| 1141 |
+
display_author = author
|
| 1142 |
+
|
| 1143 |
+
st.markdown(f"[{citation_num}] {display_author}. {title}. {journal}. [Link]({url})")
|
| 1144 |
+
else:
|
| 1145 |
+
st.markdown(f"[{citation_num}] Document ID: {doc_id}")
|
| 1146 |
+
|
| 1147 |
+
# Build complete formatted content for saving
|
| 1148 |
+
complete_content = ""
|
| 1149 |
+
|
| 1150 |
+
# Add analysis information
|
| 1151 |
+
if bio_search_data or web_search_data:
|
| 1152 |
+
total_bio_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in bio_search_data)
|
| 1153 |
+
total_web_docs = sum(len(data.get('handlerParam', {}).get('bioDocs', [])) for data in web_search_data)
|
| 1154 |
+
if total_bio_docs > 0 and total_web_docs > 0:
|
| 1155 |
+
complete_content += f"### 📚 Analysis based on {total_bio_docs} scientific papers and {total_web_docs} web pages\n\n"
|
| 1156 |
+
elif total_bio_docs > 0:
|
| 1157 |
+
complete_content += f"### 📚 Analysis based on {total_bio_docs} scientific papers\n\n"
|
| 1158 |
+
else:
|
| 1159 |
+
complete_content += f"### 🌐 Analysis based on {total_web_docs} web pages\n\n"
|
| 1160 |
+
|
| 1161 |
+
# Add final answer
|
| 1162 |
+
complete_content += "### 🎯 Final Answer\n\n"
|
| 1163 |
+
complete_content += processed_answer + "\n\n"
|
| 1164 |
+
|
| 1165 |
+
# Add references
|
| 1166 |
+
if bio_citation_data:
|
| 1167 |
+
complete_content += f"### 📖 References ({len(bio_citation_data)} citations)\n\n"
|
| 1168 |
+
|
| 1169 |
+
# Create docId to literature info mapping
|
| 1170 |
+
doc_id_to_info = {}
|
| 1171 |
+
# Add PubMed data
|
| 1172 |
+
for search_data in bio_search_data:
|
| 1173 |
+
bio_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1174 |
+
for doc in bio_docs:
|
| 1175 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1176 |
+
# Add web search data
|
| 1177 |
+
for search_data in web_search_data:
|
| 1178 |
+
web_docs = search_data.get('handlerParam', {}).get('bioDocs', [])
|
| 1179 |
+
for doc in web_docs:
|
| 1180 |
+
doc_id_to_info[doc.get('docId')] = doc
|
| 1181 |
+
|
| 1182 |
+
# Add citation list to complete content
|
| 1183 |
+
for citation in bio_citation_data:
|
| 1184 |
+
doc_id = citation.get('docId')
|
| 1185 |
+
citation_num = citation.get('citation')
|
| 1186 |
+
source = citation.get('source', '')
|
| 1187 |
+
|
| 1188 |
+
if doc_id in doc_id_to_info:
|
| 1189 |
+
doc_info = doc_id_to_info[doc_id]
|
| 1190 |
+
title = doc_info.get('title', 'N/A')
|
| 1191 |
+
url = doc_info.get('url', '#')
|
| 1192 |
+
|
| 1193 |
+
if source == 'webSearch':
|
| 1194 |
+
complete_content += f"[{citation_num}] {title}. [Link]({url})\n\n"
|
| 1195 |
+
else:
|
| 1196 |
+
author = doc_info.get('author', 'N/A')
|
| 1197 |
+
journal = doc_info.get('JournalInfo', 'N/A')
|
| 1198 |
+
|
| 1199 |
+
authors = author.split(', ')
|
| 1200 |
+
if len(authors) > 3:
|
| 1201 |
+
display_author = ', '.join(authors[:3]) + ' et al.'
|
| 1202 |
+
else:
|
| 1203 |
+
display_author = author
|
| 1204 |
+
|
| 1205 |
+
complete_content += f"[{citation_num}] {display_author}. {title}. {journal}. [Link]({url})\n\n"
|
| 1206 |
+
else:
|
| 1207 |
+
complete_content += f"[{citation_num}] Document ID: {doc_id}\n\n"
|
| 1208 |
+
|
| 1209 |
+
# Override output and bio_final_answer_content for session recording
|
| 1210 |
+
output = complete_content
|
| 1211 |
+
bio_final_answer_content = complete_content
|
| 1212 |
+
# Set flag to skip LLM processing
|
| 1213 |
+
has_bio_final_answer = True
|
| 1214 |
+
|
| 1215 |
+
# Add download buttons for Bio QA final answer (with complete content)
|
| 1216 |
+
st.markdown("---")
|
| 1217 |
+
st.markdown("### 📥 Download Options")
|
| 1218 |
+
col1, col2 = st.columns(2)
|
| 1219 |
+
with col1:
|
| 1220 |
+
create_download_button(complete_content, "bio_qa_report", "md", "bio_qa_stream_chat")
|
| 1221 |
+
with col2:
|
| 1222 |
+
create_download_button(complete_content, "bio_qa_report", "pdf", "bio_qa_stream_chat")
|
| 1223 |
+
|
| 1224 |
+
# Save ToolMessage first, then complete formatted content
|
| 1225 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 1226 |
+
_append_message_to_session({'role': 'assistant', 'content': complete_content})
|
| 1227 |
+
|
| 1228 |
+
# Debug: log ToolMessage save
|
| 1229 |
+
logger.log_system_status(f"Saved ToolMessage for bio_qa_stream_chat: {len(msg.content)} characters")
|
| 1230 |
+
logger.log_system_status(f"Current chat has {len(st.session_state.get('messages', []))} messages")
|
| 1231 |
+
else:
|
| 1232 |
+
# Fallback: if final answer not parsed, display tool message in original way
|
| 1233 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 1234 |
+
st.code(msg.content, language='yaml')
|
| 1235 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 1236 |
+
elif msg.name == "review_generate":
|
| 1237 |
+
# Try to extract final report
|
| 1238 |
+
extracted_report = extract_review_final_report(msg.content)
|
| 1239 |
+
if extracted_report:
|
| 1240 |
+
# Always display ToolMessage (collapsible)
|
| 1241 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 1242 |
+
st.code(msg.content, language='yaml')
|
| 1243 |
+
|
| 1244 |
+
# Display final report in main conversation area
|
| 1245 |
+
with messages_container.chat_message("assistant"):
|
| 1246 |
+
st.markdown("---")
|
| 1247 |
+
st.markdown("### 📚 Literature Review Report")
|
| 1248 |
+
st.markdown(extracted_report)
|
| 1249 |
+
|
| 1250 |
+
# Add download buttons to main conversation area (persistent)
|
| 1251 |
+
st.markdown("---")
|
| 1252 |
+
st.markdown("### 📥 Download Options")
|
| 1253 |
+
col1, col2 = st.columns(2)
|
| 1254 |
+
with col1:
|
| 1255 |
+
create_download_button(extracted_report, "literature_review", "md", "review_generate")
|
| 1256 |
+
with col2:
|
| 1257 |
+
create_download_button(extracted_report, "literature_review", "pdf", "review_generate")
|
| 1258 |
+
|
| 1259 |
+
# Override output and review_final_report_content for session recording
|
| 1260 |
+
output = extracted_report
|
| 1261 |
+
review_final_report_content = extracted_report
|
| 1262 |
+
# Set flag to skip LLM processing
|
| 1263 |
+
has_review_final_report = True
|
| 1264 |
+
|
| 1265 |
+
# Save "assistant final report" to session history (instead of writing tool original text to tool field)
|
| 1266 |
+
_append_message_to_session({'role': 'assistant', 'content': extracted_report})
|
| 1267 |
+
# Also save the original ToolMessage for reference
|
| 1268 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 1269 |
+
else:
|
| 1270 |
+
# Fallback: if final report not parsed, display tool message in original way
|
| 1271 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 1272 |
+
st.code(msg.content, language='yaml')
|
| 1273 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 1274 |
+
else:
|
| 1275 |
+
# Other tools remain the same, but use collapsible display
|
| 1276 |
+
with st.expander(f"🔧 ToolMessage - {tool_count} ({msg.name})", expanded=False):
|
| 1277 |
+
st.code(msg.content, language='yaml')
|
| 1278 |
+
_append_message_to_session({'role': 'assistant', 'content': '', 'tool': msg.content})
|
| 1279 |
+
else: # AIMessage
|
| 1280 |
+
# If there's a final answer or final report, skip LLM response
|
| 1281 |
+
if not has_bio_final_answer and not has_review_final_report and hasattr(msg, "content") and msg.content:
|
| 1282 |
+
with messages_container.chat_message("assistant"):
|
| 1283 |
+
output = str(msg.content)
|
| 1284 |
+
st.markdown(output)
|
| 1285 |
+
|
| 1286 |
+
# Ensure final answer or final report is correctly saved
|
| 1287 |
+
if not output and bio_final_answer_content:
|
| 1288 |
+
output = bio_final_answer_content
|
| 1289 |
+
if not output and review_final_report_content:
|
| 1290 |
+
output = review_final_report_content
|
| 1291 |
+
|
| 1292 |
+
# Initialize response_dct
|
| 1293 |
+
response_dct = None
|
| 1294 |
+
|
| 1295 |
+
# If there's a final answer or final report, use it directly as response, no need to save additional assistant message
|
| 1296 |
+
if has_bio_final_answer or has_review_final_report:
|
| 1297 |
+
# Final answer or final report has already been saved to session history during processing
|
| 1298 |
+
# But we need to trigger UI re-render to show the saved content
|
| 1299 |
+
if has_bio_final_answer:
|
| 1300 |
+
response_dct = {"role": "assistant", "content": bio_final_answer_content}
|
| 1301 |
+
logger.log_chat_message("assistant", bio_final_answer_content, st.session_state.get('current_chat_id'), has_tool=True)
|
| 1302 |
+
elif has_review_final_report:
|
| 1303 |
+
response_dct = {"role": "assistant", "content": review_final_report_content}
|
| 1304 |
+
logger.log_chat_message("assistant", review_final_report_content, st.session_state.get('current_chat_id'), has_tool=True)
|
| 1305 |
+
else:
|
| 1306 |
+
response_dct = {"role": "assistant", "content": output}
|
| 1307 |
+
# Log assistant message
|
| 1308 |
+
logger.log_chat_message("assistant", output, st.session_state.get('current_chat_id'))
|
| 1309 |
+
# Fall back to regular stream response if agent not available
|
| 1310 |
+
else:
|
| 1311 |
+
st.warning("You are not connect to MCP servers!")
|
| 1312 |
+
response_stream = get_response_stream(
|
| 1313 |
+
main_prompt,
|
| 1314 |
+
llm_provider=st.session_state['params']['model_id'],
|
| 1315 |
+
system=system_prompt,
|
| 1316 |
+
temperature=st.session_state['params'].get('temperature', DEFAULT_TEMPERATURE),
|
| 1317 |
+
max_tokens=st.session_state['params'].get('max_tokens', DEFAULT_MAX_TOKENS),
|
| 1318 |
+
)
|
| 1319 |
+
with messages_container.chat_message("assistant"):
|
| 1320 |
+
response = st.write_stream(response_stream)
|
| 1321 |
+
response_dct = {"role": "assistant", "content": response}
|
| 1322 |
+
except Exception as e:
|
| 1323 |
+
# Stop monitoring and log error
|
| 1324 |
+
task_monitor.stop_monitoring(task_id)
|
| 1325 |
+
duration = time.time() - start_time
|
| 1326 |
+
|
| 1327 |
+
response = f"⚠️ Something went wrong: {str(e)}"
|
| 1328 |
+
logger.log_error(
|
| 1329 |
+
"MCP_Agent_Error",
|
| 1330 |
+
str(e),
|
| 1331 |
+
{
|
| 1332 |
+
'chat_id': st.session_state.get('current_chat_id'),
|
| 1333 |
+
'duration_seconds': duration,
|
| 1334 |
+
'user_text': user_text
|
| 1335 |
+
}
|
| 1336 |
+
)
|
| 1337 |
+
|
| 1338 |
+
st.error(response)
|
| 1339 |
+
st.code(traceback.format_exc(), language="python")
|
| 1340 |
+
st.stop()
|
| 1341 |
+
finally:
|
| 1342 |
+
# Stop monitoring
|
| 1343 |
+
task_monitor.stop_monitoring(task_id)
|
| 1344 |
+
# Add assistant message to chat history (only if not already saved)
|
| 1345 |
+
if response_dct is not None:
|
| 1346 |
+
# Check if this is a final answer/report that was already saved during processing
|
| 1347 |
+
if (has_bio_final_answer or has_review_final_report) and response_dct.get('content'):
|
| 1348 |
+
# Don't save again, just trigger UI re-render
|
| 1349 |
+
pass
|
| 1350 |
+
else:
|
| 1351 |
+
_append_message_to_session(response_dct)
|
| 1352 |
+
|
| 1353 |
+
display_tool_executions()
|
client/chat_history/chat_Bio_QA_mcp_agent_20250908_122027.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"chat_id": "f357a4d4-358c-429d-b63e-9a57f6036989",
|
| 3 |
+
"chat_name": "Which mcp agents do you",
|
| 4 |
+
"export_date": "2025-09-08T12:20:27.120509",
|
| 5 |
+
"total_messages": 7,
|
| 6 |
+
"messages": [
|
| 7 |
+
{
|
| 8 |
+
"role": "user",
|
| 9 |
+
"content": "Which mcp agents do you have?\n\n"
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"role": "assistant",
|
| 13 |
+
"content": "I have access to the following MCP agents:\n\n1. **Bio Qa**: This tool provides answers to biomedical and medical questions using a Retrieval-Augmented Generation system. It searches scientific literature databases such as PubMed and conducts web searches for recent developments and guidelines to provide comprehensive, evidence-based responses.\n\n2. **Review Generate**: This tool generates detailed literature reviews on biomedical topics. It performs extensive research and writing processes, including comprehensive literature searches, section-by-section writing, and quality control, resulting in a complete literature review.\n\nThese tools are specifically optimized for handling questions related to biomedical and healthcare topics."
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"role": "user",
|
| 17 |
+
"content": "Bio QA: What is DNA?"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"role": "assistant",
|
| 21 |
+
"content": "",
|
| 22 |
+
"tool": "\n```bio-chat-agent-task\n{\"type\": \"search\", \"label\": \"Retrieving content from PubMed\", \"hoverable\": true, \"handler\": \"QASearch\", \"status\": \"running\", \"handlerParam\": {\"source\": \"pubmed\", \"bioDocs\": [{\"docId\": \"355567327917510656\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/21698376\", \"title\": \"Sequencing technologies and genome sequencing.\", \"description\": \"The high-throughput - next generation sequencing (HT-NGS) technologies are currently the hottest topic in the field of human and animals genomics researches, which can produce over 100 times more data compared to the most sophisticated capillary sequencers based on the Sanger method. With the ongoing developments of high throughput sequencing machines and advancement of modern bioinformatics tools at unprecedented pace, the target goal of sequencing individual genomes of living organism at a cost of $1,000 each is seemed to be realistically feasible in the near future. In the relatively short time frame since 2005, the HT-NGS technologies are revolutionizing the human and animal genome researches by analysis of chromatin immunoprecipitation coupled to DNA microarray (ChIP-chip) or sequencing (ChIP-seq), RNA sequencing (RNA-seq), whole genome genotyping, genome wide structural variation, de novo assembling and re-assembling of genome, mutation detection and carrier screening, detection of inherited disorders and complex human diseases, DNA library preparation, paired ends and genomic captures, sequencing of mitochondrial genome and personal genomics. In this review, we addressed the important features of HT-NGS like, first generation DNA sequencers, birth of HT-NGS, second generation HT-NGS platforms, third generation HT-NGS platforms: including single molecule Heliscope\\u2122, SMRT\\u2122 and RNAP sequencers, Nanopore, Archon Genomics X PRIZE foundation, comparison of second and third HT-NGS platforms, applications, advances and future perspectives of sequencing technologies on human and animal genome research.\", \"author\": \"Chandra Shekhar Pareek, Rafal Smoczynski, Andrzej Tretyn\", \"JournalInfo\": \"Journal of applied genetics.2011.doi:10.1007/s13353-011-0057-x\", \"PMID\": \"21698376\"}, {\"docId\": \"355567327917510657\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/33911273\", \"title\": \"Towards complete and error-free genome assemblies of all vertebrate species.\", \"description\": \"High-quality and complete reference genome assemblies are fundamental for the application of genomics to biology, disease, and biodiversity conservation. However, such assemblies are available for only a few non-microbial species\", \"author\": \"Arang Rhie, Shane A McCarthy, Olivier Fedrigo, Joana Damas, Giulio Formenti, Sergey Koren, Marcela Uliano-Silva, William Chow, Arkarachai Fungtammasan, Juwan Kim, Chul Lee, Byung June Ko, Mark Chaisson, Gregory L Gedman, Lindsey J Cantin, Francoise Thibaud-Nissen, Leanne Haggerty, Iliana Bista, Michelle Smith, Bettina Haase, Jacquelyn Mountcastle, Sylke Winkler, Sadye Paez, Jason Howard, Sonja C Vernes, Tanya M Lama, Frank Grutzner, Wesley C Warren, Christopher N Balakrishnan, Dave Burt, Julia M George, Matthew T Biegler, David Iorns, Andrew Digby, Daryl Eason, Bruce Robertson, Taylor Edwards, Mark Wilkinson, George Turner, Axel Meyer, Andreas F Kautt, Paolo Franchini, H William Detrich, Hannes Svardal, Maximilian Wagner, Gavin J P Naylor, Martin Pippel, Milan Malinsky, Mark Mooney, Maria Simbirsky, Brett T Hannigan, Trevor Pesout, Marlys Houck, Ann Misuraca, Sarah B Kingan, Richard Hall, Zev Kronenberg, Ivan Sovi\\u0107, Christopher Dunn, Zemin Ning, Alex Hastie, Joyce Lee, Siddarth Selvaraj, Richard E Green, Nicholas H Putnam, Ivo Gut, Jay Ghurye, Erik Garrison, Ying Sims, Joanna Collins, Sarah Pelan, James Torrance, Alan Tracey, Jonathan Wood, Robel E Dagnew, Dengfeng Guan, Sarah E London, David F Clayton, Claudio V Mello, Samantha R Friedrich, Peter V Lovell, Ekaterina Osipova, Farooq O Al-Ajli, Simona Secomandi, Heebal Kim, Constantina Theofanopoulou, Michael Hiller, Yang Zhou, Robert S Harris, Kateryna D Makova, Paul Medvedev, Jinna Hoffman, Patrick Masterson, Karen Clark, Fergal Martin, Kevin Howe, Paul Flicek, Brian P Walenz, Woori Kwak, Hiram Clawson, Mark Diekhans, Luis Nassar, Benedict Paten, Robert H S Kraus, Andrew J Crawford, M Thomas P Gilbert, Guojie Zhang, Byrappa Venkatesh, Robert W Murphy, Klaus-Peter Koepfli, Beth Shapiro, Warren E Johnson, Federica Di Palma, Tomas Marques-Bonet, Emma C Teeling, Tandy Warnow, Jennifer Marshall Graves, Oliver A Ryder, David Haussler, Stephen J O'Brien, Jonas Korlach, Harris A Lewin, Kerstin Howe, Eugene W Myers, Richard Durbin, Adam M Phillippy, Erich D Jarvis\", \"JournalInfo\": \"Nature.2021.doi:10.1038/s41586-021-03451-0\", \"PMID\": \"33911273\"}, {\"docId\": \"355567327917510658\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/39541441\", \"title\": \"Sequence modeling and design from molecular to genome scale with Evo.\", \"description\": \"The genome is a sequence that encodes the DNA, RNA, and proteins that orchestrate an organism's function. We present Evo, a long-context genomic foundation model with a frontier architecture trained on millions of prokaryotic and phage genomes, and report scaling laws on DNA to complement observations in language and vision. Evo generalizes across DNA, RNA, and proteins, enabling zero-shot function prediction competitive with domain-specific language models and the generation of functional CRISPR-Cas and transposon systems, representing the first examples of protein-RNA and protein-DNA codesign with a language model. Evo also learns how small mutations affect whole-organism fitness and generates megabase-scale sequences with plausible genomic architecture. These prediction and generation capabilities span molecular to genomic scales of complexity, advancing our understanding and control of biology.\", \"author\": \"Eric Nguyen, Michael Poli, Matthew G Durrant, Brian Kang, Dhruva Katrekar, David B Li, Liam J Bartie, Armin W Thomas, Samuel H King, Garyk Brixi, Jeremy Sullivan, Madelena Y Ng, Ashley Lewis, Aaron Lou, Stefano Ermon, Stephen A Baccus, Tina Hernandez-Boussard, Christopher R\\u00e9, Patrick D Hsu, Brian L Hie\", \"JournalInfo\": \"Science (New York, N.Y.).2024.doi:10.1126/science.ado9336\", \"PMID\": \"39541441\"}, {\"docId\": \"355567327917510659\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/28419628\", \"title\": \"HLA-HD: An accurate HLA typing algorithm for next-generation sequencing data.\", \"description\": \"The accurate typing of human leukocyte antigen (HLA) alleles is critical for a variety of medical applications, such as genomic studies of multifactorial diseases, including immune system and inflammation-related disorders, and donor selection in organ transplantation and regenerative medicine. Here, we developed a new algorithm for determining HLA alleles using next-generation sequencing (NGS) results. The method consists of constructing an extensive dictionary of HLA alleles, precise mapping of the NGS reads, and calculating a score based on weighted read counts to select the most suitable pair of alleles. The developed algorithm compares the score of all allele pairs, taking into account variation not only in the domain for antigen presentation (G-DOMAIN), but also outside this domain. Using this method, HLA alleles could be determined with 6-digit precision. We showed that our method was more accurate than other NGS-based methods and revealed limitations of the conventional HLA typing technologies. Furthermore, we determined the complete genomic sequence of an HLA-A-like-pseudogene when we assembled NGS reads that had caused arguable typing, and found its identity with HLA-Y*02:01. The accuracy of the HLA-A allele typing was improved after the HLA-Y*02:01 sequence was included in the HLA allele dictionary.\", \"author\": \"Shuji Kawaguchi, Koichiro Higasa, Masakazu Shimizu, Ryo Yamada, Fumihiko Matsuda\", \"JournalInfo\": \"Human mutation.2017.doi:10.1002/humu.23230\", \"PMID\": \"28419628\"}, {\"docId\": \"355567327917510660\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/32025019\", \"title\": \"Genomic basis for RNA alterations in cancer.\", \"description\": \"Transcript alterations often result from somatic changes in cancer genomes\", \"author\": \" , Claudia Calabrese, Natalie R Davidson, Deniz Demircio\\u011flu, Nuno A Fonseca, Yao He, Andr\\u00e9 Kahles, Kjong-Van Lehmann, Fenglin Liu, Yuichi Shiraishi, Cameron M Soulette, Lara Urban, Liliana Greger, Siliang Li, Dongbing Liu, Marc D Perry, Qian Xiang, Fan Zhang, Junjun Zhang, Peter Bailey, Serap Erkek, Katherine A Hoadley, Yong Hou, Matthew R Huska, Helena Kilpinen, Jan O Korbel, Maximillian G Marin, Julia Markowski, Tannistha Nandi, Qiang Pan-Hammarstr\\u00f6m, Chandra Sekhar Pedamallu, Reiner Siebert, Stefan G Stark, Hong Su, Patrick Tan, Sebastian M Waszak, Christina Yung, Shida Zhu, Philip Awadalla, Chad J Creighton, Matthew Meyerson, B F Francis Ouellette, Kui Wu, Huanming Yang, , Alvis Brazma, Angela N Brooks, Jonathan G\\u00f6ke, Gunnar R\\u00e4tsch, Roland F Schwarz, Oliver Stegle, Zemin Zhang, \", \"JournalInfo\": \"Nature.2020.doi:10.1038/s41586-020-1970-0\", \"PMID\": \"32025019\"}, {\"docId\": \"355567327917510661\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/36747096\", \"title\": \"Simultaneous sequencing of genetic and epigenetic bases in DNA.\", \"description\": \"DNA comprises molecular information stored in genetic and epigenetic bases, both of which are vital to our understanding of biology. Most DNA sequencing approaches address either genetics or epigenetics and thus capture incomplete information. Methods widely used to detect epigenetic DNA bases fail to capture common C-to-T mutations or distinguish 5-methylcytosine from 5-hydroxymethylcytosine. We present a single base-resolution sequencing methodology that sequences complete genetics and the two most common cytosine modifications in a single workflow. DNA is copied and bases are enzymatically converted. Coupled decoding of bases across the original and copy strand provides a phased digital readout. Methods are demonstrated on human genomic DNA and cell-free DNA from a blood sample of a patient with cancer. The approach is accurate, requires low DNA input and has a simple workflow and analysis pipeline. Simultaneous, phased reading of genetic and epigenetic bases provides a more complete picture of the information stored in genomes and has applications throughout biomedicine.\", \"author\": \"Jens F\\u00fcllgrabe, Walraj S Gosal, P\\u00e1id\\u00ed Creed, Sidong Liu, Casper K Lumby, David J Morley, Tobias W B Ost, Albert J Vilella, Shirong Yu, Helen Bignell, Philippa Burns, Tom Charlesworth, Beiyuan Fu, Howerd Fordham, Nicolas J Harding, Olga Gandelman, Paula Golder, Christopher Hodson, Mengjie Li, Marjana Lila, Yang Liu, Joanne Mason, Jason Mellad, Jack M Monahan, Oliver Nentwich, Alexandra Palmer, Michael Steward, Minna Taipale, Audrey Vandomme, Rita Santo San-Bento, Ankita Singhal, Julia Vivian, Natalia W\\u00f3jtowicz, Nathan Williams, Nicolas J Walker, Nicola C H Wong, Gary N Yalloway, Joanna D Holbrook, Shankar Balasubramanian\", \"JournalInfo\": \"Nature biotechnology.2023.doi:10.1038/s41587-022-01652-0\", \"PMID\": \"36747096\"}, {\"docId\": \"355567327917510662\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/23560931\", \"title\": \"Next-generation sequencing platforms.\", \"description\": \"Automated DNA sequencing instruments embody an elegant interplay among chemistry, engineering, software, and molecular biology and have built upon Sanger's founding discovery of dideoxynucleotide sequencing to perform once-unfathomable tasks. Combined with innovative physical mapping approaches that helped to establish long-range relationships between cloned stretches of genomic DNA, fluorescent DNA sequencers produced reference genome sequences for model organisms and for the reference human genome. New types of sequencing instruments that permit amazing acceleration of data-collection rates for DNA sequencing have been developed. The ability to generate genome-scale data sets is now transforming the nature of biological inquiry. Here, I provide an historical perspective of the field, focusing on the fundamental developments that predated the advent of next-generation sequencing instruments and providing information about how these instruments work, their application to biological research, and the newest types of sequencers that can extract data from single DNA molecules.\", \"author\": \"Elaine R Mardis\", \"JournalInfo\": \"Annual review of analytical chemistry (Palo Alto, Calif.).2013.doi:10.1146/annurev-anchem-062012-092628\", \"PMID\": \"23560931\"}, {\"docId\": \"355567327917510663\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/11181995\", \"title\": \"The sequence of the human genome.\", \"description\": \"A 2.91-billion base pair (bp) consensus sequence of the euchromatic portion of the human genome was generated by the whole-genome shotgun sequencing method. The 14.8-billion bp DNA sequence was generated over 9 months from 27,271,853 high-quality sequence reads (5.11-fold coverage of the genome) from both ends of plasmid clones made from the DNA of five individuals. Two assembly strategies-a whole-genome assembly and a regional chromosome assembly-were used, each combining sequence data from Celera and the publicly funded genome effort. The public data were shredded into 550-bp segments to create a 2.9-fold coverage of those genome regions that had been sequenced, without including biases inherent in the cloning and assembly procedure used by the publicly funded group. This brought the effective coverage in the assemblies to eightfold, reducing the number and size of gaps in the final assembly over what would be obtained with 5.11-fold coverage. The two assembly strategies yielded very similar results that largely agree with independent mapping data. The assemblies effectively cover the euchromatic regions of the human chromosomes. More than 90% of the genome is in scaffold assemblies of 100,000 bp or more, and 25% of the genome is in scaffolds of 10 million bp or larger. Analysis of the genome sequence revealed 26,588 protein-encoding transcripts for which there was strong corroborating evidence and an additional approximately 12,000 computationally derived genes with mouse matches or other weak supporting evidence. Although gene-dense clusters are obvious, almost half the genes are dispersed in low G+C sequence separated by large tracts of apparently noncoding sequence. Only 1.1% of the genome is spanned by exons, whereas 24% is in introns, with 75% of the genome being intergenic DNA. Duplications of segmental blocks, ranging in size up to chromosomal lengths, are abundant throughout the genome and reveal a complex evolutionary history. Comparative genomic analysis indicates vertebrate expansions of genes associated with neuronal function, with tissue-specific developmental regulation, and with the hemostasis and immune systems. DNA sequence comparisons between the consensus sequence and publicly funded genome data provided locations of 2.1 million single-nucleotide polymorphisms (SNPs). A random pair of human haploid genomes differed at a rate of 1 bp per 1250 on average, but there was marked heterogeneity in the level of polymorphism across the genome. Less than 1% of all SNPs resulted in variation in proteins, but the task of determining which SNPs have functional consequences remains an open challenge.\", \"author\": \"J C Venter, M D Adams, E W Myers, P W Li, R J Mural, G G Sutton, H O Smith, M Yandell, C A Evans, R A Holt, J D Gocayne, P Amanatides, R M Ballew, D H Huson, J R Wortman, Q Zhang, C D Kodira, X H Zheng, L Chen, M Skupski, G Subramanian, P D Thomas, J Zhang, G L Gabor Miklos, C Nelson, S Broder, A G Clark, J Nadeau, V A McKusick, N Zinder, A J Levine, R J Roberts, M Simon, C Slayman, M Hunkapiller, R Bolanos, A Delcher, I Dew, D Fasulo, M Flanigan, L Florea, A Halpern, S Hannenhalli, S Kravitz, S Levy, C Mobarry, K Reinert, K Remington, J Abu-Threideh, E Beasley, K Biddick, V Bonazzi, R Brandon, M Cargill, I Chandramouliswaran, R Charlab, K Chaturvedi, Z Deng, V Di Francesco, P Dunn, K Eilbeck, C Evangelista, A E Gabrielian, W Gan, W Ge, F Gong, Z Gu, P Guan, T J Heiman, M E Higgins, R R Ji, Z Ke, K A Ketchum, Z Lai, Y Lei, Z Li, J Li, Y Liang, X Lin, F Lu, G V Merkulov, N Milshina, H M Moore, A K Naik, V A Narayan, B Neelam, D Nusskern, D B Rusch, S Salzberg, W Shao, B Shue, J Sun, Z Wang, A Wang, X Wang, J Wang, M Wei, R Wides, C Xiao, C Yan, A Yao, J Ye, M Zhan, W Zhang, H Zhang, Q Zhao, L Zheng, F Zhong, W Zhong, S Zhu, S Zhao, D Gilbert, S Baumhueter, G Spier, C Carter, A Cravchik, T Woodage, F Ali, H An, A Awe, D Baldwin, H Baden, M Barnstead, I Barrow, K Beeson, D Busam, A Carver, A Center, M L Cheng, L Curry, S Danaher, L Davenport, R Desilets, S Dietz, K Dodson, L Doup, S Ferriera, N Garg, A Gluecksmann, B Hart, J Haynes, C Haynes, C Heiner, S Hladun, D Hostin, J Houck, T Howland, C Ibegwam, J Johnson, F Kalush, L Kline, S Koduru, A Love, F Mann, D May, S McCawley, T McIntosh, I McMullen, M Moy, L Moy, B Murphy, K Nelson, C Pfannkoch, E Pratts, V Puri, H Qureshi, M Reardon, R Rodriguez, Y H Rogers, D Romblad, B Ruhfel, R Scott, C Sitter, M Smallwood, E Stewart, R Strong, E Suh, R Thomas, N N Tint, S Tse, C Vech, G Wang, J Wetter, S Williams, M Williams, S Windsor, E Winn-Deen, K Wolfe, J Zaveri, K Zaveri, J F Abril, R Guig\\u00f3, M J Campbell, K V Sjolander, B Karlak, A Kejariwal, H Mi, B Lazareva, T Hatton, A Narechania, K Diemer, A Muruganujan, N Guo, S Sato, V Bafna, S Istrail, R Lippert, R Schwartz, B Walenz, S Yooseph, D Allen, A Basu, J Baxendale, L Blick, M Caminha, J Carnes-Stine, P Caulk, Y H Chiang, M Coyne, C Dahlke, A Deslattes Mays, M Dombroski, M Donnelly, D Ely, S Esparham, C Fosler, H Gire, S Glanowski, K Glasser, A Glodek, M Gorokhov, K Graham, B Gropman, M Harris, J Heil, S Henderson, J Hoover, D Jennings, C Jordan, J Jordan, J Kasha, L Kagan, C Kraft, A Levitsky, M Lewis, X Liu, J Lopez, D Ma, W Majoros, J McDaniel, S Murphy, M Newman, T Nguyen, N Nguyen, M Nodell, S Pan, J Peck, M Peterson, W Rowe, R Sanders, J Scott, M Simpson, T Smith, A Sprague, T Stockwell, R Turner, E Venter, M Wang, M Wen, D Wu, M Wu, A Xia, A Zandieh, X Zhu\", \"JournalInfo\": \"Science (New York, N.Y.).2001.doi:10.1126/science.1058040\", \"PMID\": \"11181995\"}, {\"docId\": \"355567327917510664\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/35444284\", \"title\": \"Somatic genomic changes in single Alzheimer's disease neurons.\", \"description\": \"Dementia in Alzheimer's disease progresses alongside neurodegeneration\", \"author\": \"Michael B Miller, August Yue Huang, Junho Kim, Zinan Zhou, Samantha L Kirkham, Eduardo A Maury, Jennifer S Ziegenfuss, Hannah C Reed, Jennifer E Neil, Lariza Rento, Steven C Ryu, Chanthia C Ma, Lovelace J Luquette, Heather M Ames, Derek H Oakley, Matthew P Frosch, Bradley T Hyman, Michael A Lodato, Eunjung Alice Lee, Christopher A Walsh\", \"JournalInfo\": \"Nature.2022.doi:10.1038/s41586-022-04640-1\", \"PMID\": \"35444284\"}, {\"docId\": \"355567327917510665\", \"url\": \"https://pubmed.ncbi.nlm.nih.gov/19997069\", \"title\": \"Sequencing technologies - the next generation.\", \"description\": \"Demand has never been greater for revolutionary technologies that deliver fast, inexpensive and accurate genome information. This challenge has catalysed the development of next-generation sequencing (NGS) technologies. The inexpensive production of large volumes of sequence data is the primary advantage over conventional methods. Here, I present a technical review of template preparation, sequencing and imaging, genome alignment and assembly approaches, and recent advances in current and near-term commercially available NGS instruments. I also outline the broad range of applications for NGS technologies, in addition to providing guidelines for platform selection to address biological questions of interest.\", \"author\": \"Michael L Metzker\", \"JournalInfo\": \"Nature reviews. Genetics.2010.doi:10.1038/nrg2626\", \"PMID\": \"19997069\"}]}}\n``` \n\n```bio-chat-agent-task\n{\"type\": \"search\", \"label\": \"Retrieving content from Browser\", \"hoverable\": true, \"handler\": \"QASearch\", \"status\": \"running\", \"handlerParam\": {\"source\": \"webSearch\", \"bioDocs\": [{\"docId\": \"355567578724306944\", \"url\": \"https://medlineplus.gov/genetics/understanding/basics/dna/\", \"title\": \"What is DNA?: MedlinePlus Genetics\", \"description\": null}, {\"docId\": \"355567578724306945\", \"url\": \"https://www.khanacademy.org/test-prep/mcat/biomolecules/dna/a/dna-structure-and-function\", \"title\": \"DNA function & structure (with diagram) (article) - Khan Academy\", \"description\": null}, {\"docId\": \"355567578724306946\", \"url\": \"https://www.cd-genomics.com/blog/dna-definition-molecular-structure-transcription/\", \"title\": \"DNA Fundamentals and Structural Dissection - CD Genomics\", \"description\": null}, {\"docId\": \"355567578724306947\", \"url\": \"https://www.genome.gov/genetics-glossary/Genetic-Code\", \"title\": \"Genetic Code - National Human Genome Research Institute\", \"description\": null}, {\"docId\": \"355567578724306948\", \"url\": \"https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material\", \"title\": \"1.2: DNA- The Genetic Material - Biology LibreTexts\", \"description\": null}]}}\n``` \n\n```bio-resource-lookup\n[{\"source\": \"pubmed\", \"docId\": \"355567327917510656\", \"citation\": 1}, {\"source\": \"pubmed\", \"docId\": \"355567327917510657\", \"citation\": 2}, {\"source\": \"pubmed\", \"docId\": \"355567327917510658\", \"citation\": 3}, {\"source\": \"pubmed\", \"docId\": \"355567327917510659\", \"citation\": 4}, {\"source\": \"pubmed\", \"docId\": \"355567327917510660\", \"citation\": 5}, {\"source\": \"pubmed\", \"docId\": \"355567327917510661\", \"citation\": 6}, {\"source\": \"pubmed\", \"docId\": \"355567327917510662\", \"citation\": 7}, {\"source\": \"pubmed\", \"docId\": \"355567327917510663\", \"citation\": 8}, {\"source\": \"pubmed\", \"docId\": \"355567327917510664\", \"citation\": 9}, {\"source\": \"pubmed\", \"docId\": \"355567327917510665\", \"citation\": 10}, {\"source\": \"webSearch\", \"docId\": \"355567578724306944\", \"citation\": 11}, {\"source\": \"webSearch\", \"docId\": \"355567578724306945\", \"citation\": 12}, {\"source\": \"webSearch\", \"docId\": \"355567578724306946\", \"citation\": 13}, {\"source\": \"webSearch\", \"docId\": \"355567578724306947\", \"citation\": 14}, {\"source\": \"webSearch\", \"docId\": \"355567578724306948\", \"citation\": 15}]\n```\n Bio-QA-final-Answer:Based on the search results, DNA (deoxyribonucleic acid) is defined as follows:\n\n### 🧬 1. **Definition and Function** \nDNA is the hereditary material in humans and almost all other organisms, responsible for storing genetic information that determines an organism's development and function. It carries instructions for building and maintaining living organisms [bio-rag-citation:11][bio-rag-citation:15].\n\n### 🧪 2. **Chemical Composition** \n- DNA is composed of **nucleotide subunits**, each consisting of: \n - A **deoxyribose sugar** molecule. \n - A **phosphate group**. \n - One of four **nitrogenous bases**: adenine (A), thymine (T), cytosine (C), or guanine (G) [bio-rag-citation:15]. \n- Nucleotides form two antiparallel strands linked by hydrogen bonds between complementary bases: **A pairs with T**, and **C pairs with G** [bio-rag-citation:15].\n\n### 🧬 3. **Structure** \n- DNA adopts a **double helix** configuration, resembling a twisted ladder. The sugar-phosphate backbones form the \"side rails,\" while base pairs form the \"rungs\" [bio-rag-citation:11][bio-rag-citation:15]. \n- The sequence of bases (e.g., ATGCC) encodes genetic instructions, similar to letters forming words [bio-rag-citation:11][bio-rag-citation:14].\n\n### 📦 4. **Cellular Location and Packaging** \n- In eukaryotic cells, DNA resides primarily in the **nucleus** (nuclear DNA), with a small amount in **mitochondria** (mtDNA) [bio-rag-citation:11]. \n- To fit inside the nucleus, DNA is packaged into **chromosomes** via histone proteins, forming condensed structures called **chromatin** [bio-rag-citation:15].\n\n### ♻️ 5. **Replication** \nDNA can self-replicate during cell division. Each strand serves as a template for synthesizing a complementary strand, ensuring accurate transmission of genetic information to daughter cells [bio-rag-citation:11][bio-rag-citation:15].\n\n### 💎 Key Citation \nThese points synthesize information from documents describing DNA's biological role, chemical structure, and genetic significance [bio-rag-citation:11][bio-rag-citation:14][bio-rag-citation:15]."
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"role": "assistant",
|
| 26 |
+
"content": "### 📚 Analysis based on 10 scientific papers and 5 web pages\n\n### 🎯 Final Answer\n\nBased on the search results, DNA (deoxyribonucleic acid) is defined as follows:\n\n### 🧬 1. **Definition and Function** \nDNA is the hereditary material in humans and almost all other organisms, responsible for storing genetic information that determines an organism's development and function. It carries instructions for building and maintaining living organisms ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\"))([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")).\n\n### 🧪 2. **Chemical Composition** \n- DNA is composed of **nucleotide subunits**, each consisting of: \n - A **deoxyribose sugar** molecule. \n - A **phosphate group**. \n - One of four **nitrogenous bases**: adenine (A), thymine (T), cytosine (C), or guanine (G) ([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")). \n- Nucleotides form two antiparallel strands linked by hydrogen bonds between complementary bases: **A pairs with T**, and **C pairs with G** ([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")).\n\n### 🧬 3. **Structure** \n- DNA adopts a **double helix** configuration, resembling a twisted ladder. The sugar-phosphate backbones form the \"side rails,\" while base pairs form the \"rungs\" ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\"))([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")). \n- The sequence of bases (e.g., ATGCC) encodes genetic instructions, similar to letters forming words ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\"))([14](https://www.genome.gov/genetics-glossary/Genetic-Code \"Genetic Code - National Human Genome Research Institute\")).\n\n### 📦 4. **Cellular Location and Packaging** \n- In eukaryotic cells, DNA resides primarily in the **nucleus** (nuclear DNA), with a small amount in **mitochondria** (mtDNA) ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\")). \n- To fit inside the nucleus, DNA is packaged into **chromosomes** via histone proteins, forming condensed structures called **chromatin** ([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")).\n\n### ♻️ 5. **Replication** \nDNA can self-replicate during cell division. Each strand serves as a template for synthesizing a complementary strand, ensuring accurate transmission of genetic information to daughter cells ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\"))([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")).\n\n### 💎 Key Citation \nThese points synthesize information from documents describing DNA's biological role, chemical structure, and genetic significance ([11](https://medlineplus.gov/genetics/understanding/basics/dna/ \"What is DNA?: MedlinePlus Genetics\"))([14](https://www.genome.gov/genetics-glossary/Genetic-Code \"Genetic Code - National Human Genome Research Institute\"))([15](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material \"1.2: DNA- The Genetic Material - Biology LibreTexts\")).\n\n### 📖 References (15 citations)\n\n[1] Chandra Shekhar Pareek, Rafal Smoczynski, Andrzej Tretyn. Sequencing technologies and genome sequencing.. Journal of applied genetics.2011.doi:10.1007/s13353-011-0057-x. [Link](https://pubmed.ncbi.nlm.nih.gov/21698376)\n\n[2] Arang Rhie, Shane A McCarthy, Olivier Fedrigo et al.. Towards complete and error-free genome assemblies of all vertebrate species.. Nature.2021.doi:10.1038/s41586-021-03451-0. [Link](https://pubmed.ncbi.nlm.nih.gov/33911273)\n\n[3] Eric Nguyen, Michael Poli, Matthew G Durrant et al.. Sequence modeling and design from molecular to genome scale with Evo.. Science (New York, N.Y.).2024.doi:10.1126/science.ado9336. [Link](https://pubmed.ncbi.nlm.nih.gov/39541441)\n\n[4] Shuji Kawaguchi, Koichiro Higasa, Masakazu Shimizu et al.. HLA-HD: An accurate HLA typing algorithm for next-generation sequencing data.. Human mutation.2017.doi:10.1002/humu.23230. [Link](https://pubmed.ncbi.nlm.nih.gov/28419628)\n\n[5] , Claudia Calabrese, Natalie R Davidson et al.. Genomic basis for RNA alterations in cancer.. Nature.2020.doi:10.1038/s41586-020-1970-0. [Link](https://pubmed.ncbi.nlm.nih.gov/32025019)\n\n[6] Jens Füllgrabe, Walraj S Gosal, Páidí Creed et al.. Simultaneous sequencing of genetic and epigenetic bases in DNA.. Nature biotechnology.2023.doi:10.1038/s41587-022-01652-0. [Link](https://pubmed.ncbi.nlm.nih.gov/36747096)\n\n[7] Elaine R Mardis. Next-generation sequencing platforms.. Annual review of analytical chemistry (Palo Alto, Calif.).2013.doi:10.1146/annurev-anchem-062012-092628. [Link](https://pubmed.ncbi.nlm.nih.gov/23560931)\n\n[8] J C Venter, M D Adams, E W Myers et al.. The sequence of the human genome.. Science (New York, N.Y.).2001.doi:10.1126/science.1058040. [Link](https://pubmed.ncbi.nlm.nih.gov/11181995)\n\n[9] Michael B Miller, August Yue Huang, Junho Kim et al.. Somatic genomic changes in single Alzheimer's disease neurons.. Nature.2022.doi:10.1038/s41586-022-04640-1. [Link](https://pubmed.ncbi.nlm.nih.gov/35444284)\n\n[10] Michael L Metzker. Sequencing technologies - the next generation.. Nature reviews. Genetics.2010.doi:10.1038/nrg2626. [Link](https://pubmed.ncbi.nlm.nih.gov/19997069)\n\n[11] What is DNA?: MedlinePlus Genetics. [Link](https://medlineplus.gov/genetics/understanding/basics/dna/)\n\n[12] DNA function & structure (with diagram) (article) - Khan Academy. [Link](https://www.khanacademy.org/test-prep/mcat/biomolecules/dna/a/dna-structure-and-function)\n\n[13] DNA Fundamentals and Structural Dissection - CD Genomics. [Link](https://www.cd-genomics.com/blog/dna-definition-molecular-structure-transcription/)\n\n[14] Genetic Code - National Human Genome Research Institute. [Link](https://www.genome.gov/genetics-glossary/Genetic-Code)\n\n[15] 1.2: DNA- The Genetic Material - Biology LibreTexts. [Link](https://bio.libretexts.org/Bookshelves/Genetics/Genetics_Agriculture_and_Biotechnology_(Suza_and_Lee)/01%3A_Chapters/1.02%3A_DNA-_The_Genetic_Material)\n\n"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"role": "user",
|
| 30 |
+
"content": "good"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"role": "assistant",
|
| 34 |
+
"content": "Hello! How can I assist you today?"
|
| 35 |
+
}
|
| 36 |
+
]
|
| 37 |
+
}
|
client/chat_history/chat_Review_mcp_agent_20250908_121128.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
client/config.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
env = os.getenv
|
| 7 |
+
|
| 8 |
+
# Model mapping
|
| 9 |
+
MODEL_OPTIONS = {
|
| 10 |
+
'OpenAI': 'gpt-4o',
|
| 11 |
+
'Antropic': 'claude-3-5-sonnet-20240620',
|
| 12 |
+
'Google': 'gemini-2.0-flash-001',
|
| 13 |
+
'Bedrock': 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
|
| 14 |
+
'Groq' : 'meta-llama/llama-4-scout-17b-16e-instruct'
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# Streamlit defaults
|
| 18 |
+
DEFAULT_MAX_TOKENS = 4096
|
| 19 |
+
DEFAULT_TEMPERATURE = 1.0
|
| 20 |
+
|
| 21 |
+
# Environment variable configurations for default settings
|
| 22 |
+
DEFAULT_ENV_CONFIG = {
|
| 23 |
+
'OpenAI': {
|
| 24 |
+
'api_key': env('OPENAI_API_KEY'),
|
| 25 |
+
'base_url': env('OPENAI_BASE_URL', 'https://api.openai.com/v1')
|
| 26 |
+
},
|
| 27 |
+
'Antropic': {
|
| 28 |
+
'api_key': env('ANTHROPIC_API_KEY'),
|
| 29 |
+
'base_url': env('ANTHROPIC_BASE_URL', 'https://api.anthropic.com')
|
| 30 |
+
},
|
| 31 |
+
'Google': {
|
| 32 |
+
'api_key': env('GOOGLE_API_KEY'),
|
| 33 |
+
'base_url': env('GOOGLE_BASE_URL', 'https://generativelanguage.googleapis.com/v1beta')
|
| 34 |
+
},
|
| 35 |
+
'Bedrock': {
|
| 36 |
+
'region_name': env('AWS_REGION', 'us-east-1'),
|
| 37 |
+
'aws_access_key': env('AWS_ACCESS_KEY_ID'),
|
| 38 |
+
'aws_secret_key': env('AWS_SECRET_ACCESS_KEY')
|
| 39 |
+
},
|
| 40 |
+
'Groq': {
|
| 41 |
+
'api_key': env('GROQ_API_KEY'),
|
| 42 |
+
'base_url': env('GROQ_BASE_URL', 'https://api.groq.com/openai/v1')
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
# Load server configuration
|
| 47 |
+
config_path = os.path.join(os.path.dirname(__file__), 'servers_config.json')
|
| 48 |
+
if os.path.exists(config_path):
|
| 49 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 50 |
+
SERVER_CONFIG = json.load(f)
|
| 51 |
+
else:
|
| 52 |
+
# Fallback: try relative to current working directory
|
| 53 |
+
config_path = os.path.join('.', 'servers_config.json')
|
| 54 |
+
if os.path.exists(config_path):
|
| 55 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 56 |
+
SERVER_CONFIG = json.load(f)
|
| 57 |
+
else:
|
| 58 |
+
# Default empty configuration if file not found
|
| 59 |
+
SERVER_CONFIG = {"mcpServers": {}}
|
client/docs/import_export_guide.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 对话历史导入导出功能
|
| 2 |
+
|
| 3 |
+
## 功能概述
|
| 4 |
+
|
| 5 |
+
本应用现在支持完整的对话历史导入导出功能,包括:
|
| 6 |
+
|
| 7 |
+
### 📤 导出功能
|
| 8 |
+
- **Markdown格式**: 导出为可读性强的Markdown文件
|
| 9 |
+
- **JSON格式**: 导出为结构化数据,便于程序处理
|
| 10 |
+
- **包含ToolMessage**: 完整保存所有工具执行信息
|
| 11 |
+
- **统计信息**: 包含消息类型统计和元数据
|
| 12 |
+
|
| 13 |
+
### 📥 导入功能
|
| 14 |
+
- **支持Markdown文件**: 可以导入之前导出的Markdown格式对话历史
|
| 15 |
+
- **支持JSON文件**: 可以导入JSON格式的对话历史
|
| 16 |
+
- **自动重命名**: 避免重复名称冲突
|
| 17 |
+
- **完整恢复**: 包括ToolMessage在内的所有信息
|
| 18 |
+
|
| 19 |
+
## 使用方法
|
| 20 |
+
|
| 21 |
+
### 导出对话历史
|
| 22 |
+
1. 在左侧边栏选择要导出的对话
|
| 23 |
+
2. 点击"📥 Export Chat History"区域
|
| 24 |
+
3. 选择导出格式(Markdown或JSON)
|
| 25 |
+
4. 点击下载按钮保存文件
|
| 26 |
+
|
| 27 |
+
### 导入对话历史
|
| 28 |
+
1. 在左侧边栏的"📁 Import Chat History"区域
|
| 29 |
+
2. 点击"Choose a chat history file"上传文件
|
| 30 |
+
3. 支持.md和.json格式
|
| 31 |
+
4. 点击"📥 Import Chat"按钮导入
|
| 32 |
+
|
| 33 |
+
## 文件格式说明
|
| 34 |
+
|
| 35 |
+
### Markdown格式特点
|
| 36 |
+
- 包含完整的对话内容
|
| 37 |
+
- 使用表情符号区分消息类型
|
| 38 |
+
- 包含ToolMessage的YAML格式显示
|
| 39 |
+
- 包含统计信息和元数据
|
| 40 |
+
- 适合人类阅读和分享
|
| 41 |
+
|
| 42 |
+
### JSON格式特点
|
| 43 |
+
- 结构化数据格式
|
| 44 |
+
- 包含所有原始数据
|
| 45 |
+
- 便于程序处理和API集成
|
| 46 |
+
- 支持完整的数据恢复
|
| 47 |
+
|
| 48 |
+
## 特殊功能
|
| 49 |
+
|
| 50 |
+
### Review工具支持
|
| 51 |
+
- 自动识别Literature Review Report
|
| 52 |
+
- 在导出文件中特殊标记
|
| 53 |
+
- 保持下载按钮功能
|
| 54 |
+
|
| 55 |
+
### ToolMessage处理
|
| 56 |
+
- 完整保存所有工具执行信息
|
| 57 |
+
- 支持YAML格式显示
|
| 58 |
+
- 在导入时正确恢复
|
| 59 |
+
|
| 60 |
+
## 注意事项
|
| 61 |
+
|
| 62 |
+
1. 导入的对话会生成新的Chat ID,避免冲突
|
| 63 |
+
2. 重复名称会自动添加时间戳后缀
|
| 64 |
+
3. 导入后会自动切换到导入的对话
|
| 65 |
+
4. 支持大文件导入,但建议单个文件不超过10MB
|
client/docs/logging_config.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 日志系统配置说明
|
| 2 |
+
|
| 3 |
+
## 概述
|
| 4 |
+
|
| 5 |
+
本应用现在集成了完整的日志记录系统,用于监控用户行为、MCP服务调用、系统状态和错误信息。
|
| 6 |
+
|
| 7 |
+
## 日志文件类型
|
| 8 |
+
|
| 9 |
+
### 1. 用户行为日志 (`user_actions.log`)
|
| 10 |
+
记录用户的各种操作:
|
| 11 |
+
- 创建/删除对话
|
| 12 |
+
- 发送消息
|
| 13 |
+
- 导入/导出对话历史
|
| 14 |
+
- 其他用户交互
|
| 15 |
+
|
| 16 |
+
### 2. MCP服务日志 (`mcp_services.log`)
|
| 17 |
+
记录MCP相关的操作:
|
| 18 |
+
- MCP服务器连接/断开
|
| 19 |
+
- 工具调用和响应
|
| 20 |
+
- MCP错误信息
|
| 21 |
+
|
| 22 |
+
### 3. 系统状态日志 (`system_status.log`)
|
| 23 |
+
记录系统运行状态:
|
| 24 |
+
- 应用启动/关闭
|
| 25 |
+
- 长时间运行任务
|
| 26 |
+
- 系统状态变化
|
| 27 |
+
|
| 28 |
+
### 4. 错误日志 (`errors.log`)
|
| 29 |
+
记录所有错误信息:
|
| 30 |
+
- 连接错误
|
| 31 |
+
- 工具执行错误
|
| 32 |
+
- 系统异常
|
| 33 |
+
|
| 34 |
+
## 长时间运行任务监控
|
| 35 |
+
|
| 36 |
+
### 心跳机制
|
| 37 |
+
- 每5分钟发送一次心跳信号
|
| 38 |
+
- 记录任务执行时间
|
| 39 |
+
- 监控任务状态
|
| 40 |
+
|
| 41 |
+
### 监控的任务类型
|
| 42 |
+
- MCP工具调用
|
| 43 |
+
- 大型响应生成
|
| 44 |
+
- 文件处理任务
|
| 45 |
+
|
| 46 |
+
## 日志查看
|
| 47 |
+
|
| 48 |
+
### 界面查看
|
| 49 |
+
- 在应用界面中可以查看实时日志
|
| 50 |
+
- 支持按时间范围过滤
|
| 51 |
+
- 支持关键词搜索
|
| 52 |
+
|
| 53 |
+
### 文件位置
|
| 54 |
+
所有日志文件保存在 `logs/` 目录下
|
| 55 |
+
|
| 56 |
+
## 配置选项
|
| 57 |
+
|
| 58 |
+
### 日志级别
|
| 59 |
+
- INFO: 一般信息
|
| 60 |
+
- ERROR: 错误信息
|
| 61 |
+
|
| 62 |
+
### 日志轮转
|
| 63 |
+
- 建议定期清理旧日志文件
|
| 64 |
+
- 避免日志文件过大影响性能
|
| 65 |
+
|
| 66 |
+
## 使用建议
|
| 67 |
+
|
| 68 |
+
1. **定期检查错误日志**:及时发现和解决问题
|
| 69 |
+
2. **监控长时间运行任务**:确保系统正常运行
|
| 70 |
+
3. **分析用户行为**:了解用户使用模式
|
| 71 |
+
4. **性能监控**:通过日志分析系统性能
|
| 72 |
+
|
| 73 |
+
## 注意事项
|
| 74 |
+
|
| 75 |
+
1. 日志文件会持续增长,需要定期清理
|
| 76 |
+
2. 敏感信息会被自动脱敏处理
|
| 77 |
+
3. 大型响应会被截断显示
|
| 78 |
+
4. 日志记录不会影响应用性能
|
client/icons/playground.png
ADDED
|
|
Git LFS Details
|
client/requirements.txt
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.44
|
| 2 |
+
streamlit-aggrid==0.3.4.post3
|
| 3 |
+
streamlit-option-menu==0.3.6
|
| 4 |
+
boto3==1.38.18
|
| 5 |
+
pandas==2.2.3
|
| 6 |
+
python-docx==1.1.2
|
| 7 |
+
openpyxl==3.1.5
|
| 8 |
+
PyPDF2==3.0.1
|
| 9 |
+
python-dotenv==1.1.0
|
| 10 |
+
nest-asyncio==1.6.0
|
| 11 |
+
markdown==3.5.2
|
| 12 |
+
reportlab==4.0.7
|
| 13 |
+
langchain==0.3.20
|
| 14 |
+
langchain-aws==0.2.12
|
| 15 |
+
langchain-openai>=0.0.3
|
| 16 |
+
langchain-anthropic>=0.1.1
|
| 17 |
+
langchain-google-genai>=2.1.2
|
| 18 |
+
langchain-mcp-adapters>=0.0.7
|
| 19 |
+
langchain_groq>=0.3.6
|
| 20 |
+
langgraph==0.3.30
|
| 21 |
+
beautifulsoup4>=4.12.0
|
| 22 |
+
mcp>=1.13.0
|
| 23 |
+
# force rebuild: 2025-09-08-1
|
client/run.sh
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# 设置环境变量以确保日志输出到控制台
|
| 3 |
+
export PYTHONUNBUFFERED=1
|
| 4 |
+
export STREAMLIT_SERVER_PORT=8502
|
| 5 |
+
export STREAMLIT_SERVER_ADDRESS=0.0.0.0
|
| 6 |
+
|
| 7 |
+
echo "🚀 Starting MCP Playground with logging enabled..."
|
| 8 |
+
echo "📊 Logs will be displayed in this terminal"
|
| 9 |
+
echo "📁 Log files will be saved in logs/ directory"
|
| 10 |
+
echo ""
|
| 11 |
+
|
| 12 |
+
# 运行应用
|
| 13 |
+
streamlit run app.py --server.port=8502 --server.address=0.0.0.0
|
client/servers_config.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"mcpServers": {
|
| 3 |
+
"Bio_QA": {
|
| 4 |
+
"transport": "sse",
|
| 5 |
+
"url": "http://127.0.0.1:9487/sse",
|
| 6 |
+
"timeout": 60000,
|
| 7 |
+
"headers": null,
|
| 8 |
+
"sse_read_timeout": 3600000
|
| 9 |
+
},
|
| 10 |
+
"Bio_Review": {
|
| 11 |
+
"transport": "sse",
|
| 12 |
+
"url": "http://127.0.0.1:8880/review",
|
| 13 |
+
"timeout": 60000,
|
| 14 |
+
"headers": null,
|
| 15 |
+
"sse_read_timeout": 3600000
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
}
|
client/services/__init__.py
ADDED
|
File without changes
|
client/services/ai_service.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
from langchain_core.messages import HumanMessage, SystemMessage
|
| 4 |
+
from langchain_aws import ChatBedrock
|
| 5 |
+
from langchain_openai import ChatOpenAI
|
| 6 |
+
from langchain_groq import ChatGroq
|
| 7 |
+
from langchain_anthropic import ChatAnthropic
|
| 8 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 9 |
+
|
| 10 |
+
from typing import Optional
|
| 11 |
+
from config import MODEL_OPTIONS
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def create_llm_model(llm_provider: str, **kwargs):
|
| 15 |
+
"""Create a language model based on the selected provider."""
|
| 16 |
+
params = st.session_state.get('params')
|
| 17 |
+
|
| 18 |
+
if llm_provider == "OpenAI":
|
| 19 |
+
return ChatOpenAI(
|
| 20 |
+
base_url=params.get("base_url"),
|
| 21 |
+
openai_api_key=params.get("api_key"),
|
| 22 |
+
model=MODEL_OPTIONS['OpenAI'],
|
| 23 |
+
temperature=kwargs.get('temperature', 0.7),
|
| 24 |
+
)
|
| 25 |
+
elif llm_provider == "Antropic":
|
| 26 |
+
return ChatAnthropic(
|
| 27 |
+
base_url=params.get("base_url"),
|
| 28 |
+
anthropic_api_key=params.get("api_key"),
|
| 29 |
+
model=MODEL_OPTIONS['Antropic'],
|
| 30 |
+
temperature=kwargs.get('temperature', 0.7),
|
| 31 |
+
)
|
| 32 |
+
elif llm_provider == "Bedrock":
|
| 33 |
+
import boto3
|
| 34 |
+
# Initialize Bedrock client
|
| 35 |
+
_bedrock = boto3.client(
|
| 36 |
+
'bedrock-runtime',
|
| 37 |
+
region_name=params.get("region_name"),
|
| 38 |
+
aws_access_key_id=params.get("aws_access_key"),
|
| 39 |
+
aws_secret_access_key=params.get("aws_secret_key"),
|
| 40 |
+
)
|
| 41 |
+
return ChatBedrock(
|
| 42 |
+
client=_bedrock,
|
| 43 |
+
model_id=MODEL_OPTIONS['Bedrock'],
|
| 44 |
+
**kwargs
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
elif llm_provider == "Google":
|
| 48 |
+
return ChatGoogleGenerativeAI(
|
| 49 |
+
google_api_key=params.get("api_key"),
|
| 50 |
+
model=MODEL_OPTIONS['Google'],
|
| 51 |
+
temperature=kwargs.get('temperature', 0.7),
|
| 52 |
+
max_tokens=kwargs.get('max_tokens', 4096),
|
| 53 |
+
max_retries=2,
|
| 54 |
+
)
|
| 55 |
+
elif llm_provider == "Groq":
|
| 56 |
+
return ChatGroq(
|
| 57 |
+
api_key=params.get("api_key"), # groq_api_key expected here
|
| 58 |
+
model=MODEL_OPTIONS['Groq'],
|
| 59 |
+
temperature=kwargs.get("temperature", 0.7),
|
| 60 |
+
streaming=kwargs.get("streaming", False)
|
| 61 |
+
)
|
| 62 |
+
else:
|
| 63 |
+
raise ValueError(f"Unsupported LLM provider: {llm_provider}")
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def get_response(prompt: str, llm_provider: str):
|
| 67 |
+
"""Get a response from the LLM using the standard LangChain interface."""
|
| 68 |
+
try:
|
| 69 |
+
# Create the LLM instance dynamically
|
| 70 |
+
llm = create_llm_model(llm_provider)
|
| 71 |
+
|
| 72 |
+
# Wrap prompt in a HumanMessage
|
| 73 |
+
message = HumanMessage(content=prompt)
|
| 74 |
+
|
| 75 |
+
# Invoke model and return the output content
|
| 76 |
+
response = llm.invoke([message])
|
| 77 |
+
return response.content
|
| 78 |
+
|
| 79 |
+
except Exception as e:
|
| 80 |
+
return f"Error during LLM invocation: {str(e)}"
|
| 81 |
+
|
| 82 |
+
def get_response_stream(
|
| 83 |
+
prompt: str,
|
| 84 |
+
llm_provider: str,
|
| 85 |
+
system: Optional[str] = '',
|
| 86 |
+
temperature: float = 1.0,
|
| 87 |
+
max_tokens: int = 4096,
|
| 88 |
+
**kwargs,
|
| 89 |
+
):
|
| 90 |
+
"""
|
| 91 |
+
Get a streaming response from the selected LLM provider.
|
| 92 |
+
All provider-specific connection/auth should be handled via kwargs.
|
| 93 |
+
"""
|
| 94 |
+
try:
|
| 95 |
+
# Add streaming and generation params to kwargs
|
| 96 |
+
kwargs.update({
|
| 97 |
+
"temperature": temperature,
|
| 98 |
+
"max_tokens": max_tokens,
|
| 99 |
+
"streaming": True
|
| 100 |
+
})
|
| 101 |
+
|
| 102 |
+
# Create the LLM with streaming enabled
|
| 103 |
+
llm = create_llm_model(llm_provider, **kwargs)
|
| 104 |
+
|
| 105 |
+
# Compose messages
|
| 106 |
+
messages = []
|
| 107 |
+
if system:
|
| 108 |
+
messages.append(SystemMessage(content=system))
|
| 109 |
+
messages.append(HumanMessage(content=prompt))
|
| 110 |
+
|
| 111 |
+
# Stream the response
|
| 112 |
+
stream_response = llm.stream(messages)
|
| 113 |
+
return stream_response
|
| 114 |
+
except Exception as e:
|
| 115 |
+
st.error(f"[Error during streaming: {str(e)}]")
|
| 116 |
+
st.stop()
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def test_llm_connection(llm_provider: str, test_params: dict = None):
|
| 120 |
+
"""
|
| 121 |
+
Test the connection to the specified LLM provider.
|
| 122 |
+
Returns a tuple of (success: bool, message: str)
|
| 123 |
+
"""
|
| 124 |
+
try:
|
| 125 |
+
# Use test_params if provided, otherwise use session state
|
| 126 |
+
if test_params:
|
| 127 |
+
# Temporarily store original params
|
| 128 |
+
original_params = st.session_state.get('params', {})
|
| 129 |
+
# Set test params
|
| 130 |
+
st.session_state['params'] = test_params
|
| 131 |
+
|
| 132 |
+
# Create LLM instance
|
| 133 |
+
llm = create_llm_model(llm_provider)
|
| 134 |
+
|
| 135 |
+
# Test with a simple message
|
| 136 |
+
test_message = HumanMessage(content="Hello, this is a connection test. Please respond with 'OK'.")
|
| 137 |
+
response = llm.invoke([test_message])
|
| 138 |
+
|
| 139 |
+
# Restore original params if we used test_params
|
| 140 |
+
if test_params:
|
| 141 |
+
st.session_state['params'] = original_params
|
| 142 |
+
|
| 143 |
+
return True, f"✅ Connection successful! Model response: {response.content[:100]}..."
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
# Restore original params if we used test_params
|
| 147 |
+
if test_params:
|
| 148 |
+
st.session_state['params'] = original_params
|
| 149 |
+
return False, f"❌ Connection failed: {str(e)}"
|
client/services/chat_service.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from config import SERVER_CONFIG
|
| 3 |
+
import uuid
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
from services.logging_service import get_logger
|
| 7 |
+
from services.mcp_service import connect_to_mcp_servers
|
| 8 |
+
|
| 9 |
+
# Session state initialization
|
| 10 |
+
def init_session():
|
| 11 |
+
defaults = {
|
| 12 |
+
"params": {},
|
| 13 |
+
"current_chat_id": None,
|
| 14 |
+
"current_chat_index": 0,
|
| 15 |
+
"history_chats": get_history(),
|
| 16 |
+
"messages": [],
|
| 17 |
+
"client": None,
|
| 18 |
+
"agent": None,
|
| 19 |
+
"tools": [],
|
| 20 |
+
"tool_executions": [],
|
| 21 |
+
"servers": SERVER_CONFIG['mcpServers'],
|
| 22 |
+
"auto_connect_attempted": False
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
for key, val in defaults.items():
|
| 26 |
+
if key not in st.session_state:
|
| 27 |
+
st.session_state[key] = val
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def auto_connect_to_mcp():
|
| 31 |
+
"""Automatically connect to MCP servers on first page load"""
|
| 32 |
+
try:
|
| 33 |
+
logger = get_logger()
|
| 34 |
+
logger.log_system_status("Auto-connecting to MCP servers on first load")
|
| 35 |
+
|
| 36 |
+
# Check if params are configured before attempting connection
|
| 37 |
+
params = st.session_state.get('params', {})
|
| 38 |
+
if not params.get('model_id') or not params.get('temperature'):
|
| 39 |
+
logger.log_system_status("Skipping auto-connect: LLM parameters not configured yet")
|
| 40 |
+
st.session_state["auto_connect_attempted"] = True
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
# Attempt to connect to MCP servers
|
| 44 |
+
connect_to_mcp_servers()
|
| 45 |
+
|
| 46 |
+
# Mark auto-connect as attempted
|
| 47 |
+
st.session_state["auto_connect_attempted"] = True
|
| 48 |
+
|
| 49 |
+
# Log successful connection
|
| 50 |
+
if st.session_state.get("agent"):
|
| 51 |
+
logger.log_system_status("Successfully auto-connected to MCP servers", {
|
| 52 |
+
'servers_count': len(st.session_state.servers),
|
| 53 |
+
'tools_count': len(st.session_state.tools)
|
| 54 |
+
})
|
| 55 |
+
else:
|
| 56 |
+
logger.log_system_status("Auto-connect attempted but no agent available")
|
| 57 |
+
|
| 58 |
+
except Exception as e:
|
| 59 |
+
# Log connection error but don't fail the app
|
| 60 |
+
logger = get_logger()
|
| 61 |
+
logger.log_error(
|
| 62 |
+
"Auto_MCP_Connection_Error",
|
| 63 |
+
str(e),
|
| 64 |
+
{'servers': list(st.session_state.servers.keys())}
|
| 65 |
+
)
|
| 66 |
+
logger.log_system_status(f"Auto-connect failed: {str(e)}")
|
| 67 |
+
|
| 68 |
+
# Mark as attempted even if failed
|
| 69 |
+
st.session_state["auto_connect_attempted"] = True
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def load_example_chats():
|
| 73 |
+
"""Load example chat histories from JSON files"""
|
| 74 |
+
example_chats = []
|
| 75 |
+
|
| 76 |
+
# Define example chat files
|
| 77 |
+
example_files = [
|
| 78 |
+
{
|
| 79 |
+
"file": "chat_Bio_QA_mcp_agent_20250908_122027.json",
|
| 80 |
+
"display_name": "Bio QA Example: What is DNA?"
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"file": "chat_Review_mcp_agent_20250908_121128.json",
|
| 84 |
+
"display_name": "Review Example: AML Risk Stratification"
|
| 85 |
+
}
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
# Get the directory of this file
|
| 89 |
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
| 90 |
+
chat_history_dir = os.path.join(current_dir, "..", "chat_history")
|
| 91 |
+
|
| 92 |
+
for example in example_files:
|
| 93 |
+
file_path = os.path.join(chat_history_dir, example["file"])
|
| 94 |
+
if os.path.exists(file_path):
|
| 95 |
+
try:
|
| 96 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 97 |
+
chat_data = json.load(f)
|
| 98 |
+
# Update the chat name for display
|
| 99 |
+
chat_data['chat_name'] = example["display_name"]
|
| 100 |
+
example_chats.append(chat_data)
|
| 101 |
+
except Exception as e:
|
| 102 |
+
logger = get_logger()
|
| 103 |
+
logger.log_error("LoadExampleChat", str(e), {"file": example["file"]})
|
| 104 |
+
|
| 105 |
+
return example_chats
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def get_history():
|
| 109 |
+
if "history_chats" in st.session_state and st.session_state["history_chats"]:
|
| 110 |
+
return st.session_state["history_chats"]
|
| 111 |
+
else:
|
| 112 |
+
# Load example chats first
|
| 113 |
+
example_chats = load_example_chats()
|
| 114 |
+
|
| 115 |
+
# Create a new chat
|
| 116 |
+
chat_id = str(uuid.uuid4())
|
| 117 |
+
new_chat = {'chat_id': chat_id,
|
| 118 |
+
'chat_name': 'New chat',
|
| 119 |
+
'messages': []}
|
| 120 |
+
|
| 121 |
+
# Combine example chats with new chat
|
| 122 |
+
all_chats = example_chats + [new_chat]
|
| 123 |
+
|
| 124 |
+
st.session_state["current_chat_index"] = 0 # Select the new chat (after reversal in sidebar)
|
| 125 |
+
st.session_state["current_chat_id"] = chat_id
|
| 126 |
+
|
| 127 |
+
return all_chats
|
| 128 |
+
|
| 129 |
+
def get_current_chat(chat_id):
|
| 130 |
+
"""Get messages for the current chat."""
|
| 131 |
+
for chat in st.session_state["history_chats"]:
|
| 132 |
+
if chat['chat_id'] == chat_id:
|
| 133 |
+
return chat['messages']
|
| 134 |
+
return []
|
| 135 |
+
|
| 136 |
+
def _append_message_to_session(msg: dict) -> None:
|
| 137 |
+
"""
|
| 138 |
+
Append *msg* to the current chat’s message list **and**
|
| 139 |
+
keep history_chats in-sync.
|
| 140 |
+
"""
|
| 141 |
+
chat_id = st.session_state["current_chat_id"]
|
| 142 |
+
st.session_state["messages"].append(msg)
|
| 143 |
+
for chat in st.session_state["history_chats"]:
|
| 144 |
+
if chat["chat_id"] == chat_id:
|
| 145 |
+
chat["messages"] = st.session_state["messages"] # same list
|
| 146 |
+
if chat["chat_name"] == "New chat": # rename once
|
| 147 |
+
chat["chat_name"] = " ".join(msg["content"].split()[:5]) or "Empty"
|
| 148 |
+
break
|
| 149 |
+
|
| 150 |
+
def create_chat():
|
| 151 |
+
"""Create a new chat session."""
|
| 152 |
+
logger = get_logger()
|
| 153 |
+
chat_id = str(uuid.uuid4())
|
| 154 |
+
new_chat = {'chat_id': chat_id,
|
| 155 |
+
'chat_name': 'New chat',
|
| 156 |
+
'messages': []}
|
| 157 |
+
|
| 158 |
+
st.session_state["history_chats"].append(new_chat)
|
| 159 |
+
st.session_state["current_chat_index"] = 0
|
| 160 |
+
st.session_state["current_chat_id"] = chat_id
|
| 161 |
+
|
| 162 |
+
# Log chat creation
|
| 163 |
+
logger.log_user_action("create_chat", {
|
| 164 |
+
'chat_id': chat_id,
|
| 165 |
+
'total_chats': len(st.session_state["history_chats"])
|
| 166 |
+
})
|
| 167 |
+
|
| 168 |
+
return new_chat
|
| 169 |
+
|
| 170 |
+
def delete_chat(chat_id: str):
|
| 171 |
+
"""Delete a chat from history."""
|
| 172 |
+
if not chat_id: # protection against accidental call
|
| 173 |
+
return
|
| 174 |
+
|
| 175 |
+
logger = get_logger()
|
| 176 |
+
|
| 177 |
+
# Log chat deletion
|
| 178 |
+
chat_to_delete = None
|
| 179 |
+
for chat in st.session_state["history_chats"]:
|
| 180 |
+
if chat["chat_id"] == chat_id:
|
| 181 |
+
chat_to_delete = chat
|
| 182 |
+
break
|
| 183 |
+
|
| 184 |
+
if chat_to_delete:
|
| 185 |
+
logger.log_user_action("delete_chat", {
|
| 186 |
+
'chat_id': chat_id,
|
| 187 |
+
'chat_name': chat_to_delete.get('chat_name'),
|
| 188 |
+
'messages_count': len(chat_to_delete.get('messages', []))
|
| 189 |
+
})
|
| 190 |
+
|
| 191 |
+
# 1) Remove from session_state.history_chats
|
| 192 |
+
st.session_state["history_chats"] = [
|
| 193 |
+
c for c in st.session_state["history_chats"]
|
| 194 |
+
if c["chat_id"] != chat_id
|
| 195 |
+
]
|
| 196 |
+
|
| 197 |
+
# 2) Switch current_chat to another one or create new
|
| 198 |
+
if st.session_state["current_chat_id"] == chat_id:
|
| 199 |
+
if st.session_state["history_chats"]: # if chats still exist
|
| 200 |
+
first = st.session_state["history_chats"][0]
|
| 201 |
+
st.session_state["current_chat_id"] = first["chat_id"]
|
| 202 |
+
st.session_state["current_chat_index"] = 0
|
| 203 |
+
st.session_state["messages"] = first["messages"]
|
| 204 |
+
else: # if all deleted → new empty
|
| 205 |
+
new_chat = create_chat()
|
| 206 |
+
st.session_state["messages"] = new_chat["messages"]
|
| 207 |
+
return
|
client/services/export_service.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import json
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import List, Dict, Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def format_message_for_export(message: Dict[str, Any]) -> str:
|
| 8 |
+
"""
|
| 9 |
+
Format a single message for export to Markdown
|
| 10 |
+
"""
|
| 11 |
+
role = message.get("role", "unknown")
|
| 12 |
+
content = message.get("content", "")
|
| 13 |
+
tool = message.get("tool", "")
|
| 14 |
+
|
| 15 |
+
formatted = f"## {role.title()}\n\n"
|
| 16 |
+
|
| 17 |
+
if content:
|
| 18 |
+
# Handle different content types
|
| 19 |
+
if isinstance(content, str):
|
| 20 |
+
# Check if this is a review report
|
| 21 |
+
if "Literature Review Report" in content or "📚 Literature Review Report" in content:
|
| 22 |
+
formatted += f"### 📚 Literature Review Report\n\n{content}\n\n"
|
| 23 |
+
# Add download note for review reports
|
| 24 |
+
formatted += "> **Note:** This review report can be downloaded as Markdown or PDF from the main interface.\n\n"
|
| 25 |
+
else:
|
| 26 |
+
formatted += f"{content}\n\n"
|
| 27 |
+
else:
|
| 28 |
+
formatted += f"```\n{content}\n```\n\n"
|
| 29 |
+
|
| 30 |
+
if tool:
|
| 31 |
+
formatted += f"### 🔧 Tool Message\n\n```yaml\n{tool}\n```\n\n"
|
| 32 |
+
|
| 33 |
+
return formatted
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def export_chat_to_markdown(chat_data: Dict[str, Any]) -> str:
|
| 37 |
+
"""
|
| 38 |
+
Export a complete chat conversation to Markdown format
|
| 39 |
+
"""
|
| 40 |
+
chat_id = chat_data.get("chat_id", "unknown")
|
| 41 |
+
chat_name = chat_data.get("chat_name", "Unknown Chat")
|
| 42 |
+
messages = chat_data.get("messages", [])
|
| 43 |
+
|
| 44 |
+
# Count message types
|
| 45 |
+
user_messages = sum(1 for msg in messages if msg.get("role") == "user")
|
| 46 |
+
assistant_messages = sum(1 for msg in messages if msg.get("role") == "assistant")
|
| 47 |
+
tool_messages = sum(1 for msg in messages if msg.get("tool"))
|
| 48 |
+
review_reports = sum(1 for msg in messages if msg.get("role") == "assistant" and
|
| 49 |
+
msg.get("content") and
|
| 50 |
+
("Literature Review Report" in str(msg.get("content")) or "📚 Literature Review Report" in str(msg.get("content"))))
|
| 51 |
+
|
| 52 |
+
# Create markdown content
|
| 53 |
+
markdown_content = f"# 💬 Chat: {chat_name}\n\n"
|
| 54 |
+
markdown_content += f"**Chat ID:** `{chat_id}`\n"
|
| 55 |
+
markdown_content += f"**Export Date:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
| 56 |
+
markdown_content += f"**Total Messages:** {len(messages)}\n"
|
| 57 |
+
markdown_content += f"**Message Breakdown:**\n"
|
| 58 |
+
markdown_content += f"- 👤 User Messages: {user_messages}\n"
|
| 59 |
+
markdown_content += f"- 🤖 Assistant Messages: {assistant_messages}\n"
|
| 60 |
+
markdown_content += f"- 🔧 Tool Messages: {tool_messages}\n"
|
| 61 |
+
markdown_content += f"- 📚 Review Reports: {review_reports}\n\n"
|
| 62 |
+
markdown_content += "---\n\n"
|
| 63 |
+
|
| 64 |
+
# Add each message
|
| 65 |
+
for i, message in enumerate(messages, 1):
|
| 66 |
+
role = message.get("role", "unknown")
|
| 67 |
+
role_emoji = "👤" if role == "user" else "🤖" if role == "assistant" else "🔧"
|
| 68 |
+
|
| 69 |
+
markdown_content += f"## {role_emoji} Message {i} ({role.title()})\n\n"
|
| 70 |
+
markdown_content += format_message_for_export(message)
|
| 71 |
+
markdown_content += "---\n\n"
|
| 72 |
+
|
| 73 |
+
return markdown_content
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def export_chat_to_json(chat_data: Dict[str, Any]) -> str:
|
| 77 |
+
"""
|
| 78 |
+
Export a complete chat conversation to JSON format
|
| 79 |
+
"""
|
| 80 |
+
chat_id = chat_data.get("chat_id")
|
| 81 |
+
chat_name = chat_data.get("chat_name")
|
| 82 |
+
messages = chat_data.get("messages", [])
|
| 83 |
+
|
| 84 |
+
# 重新组织消息,确保ToolMessage正确保存
|
| 85 |
+
processed_messages = []
|
| 86 |
+
|
| 87 |
+
for i, message in enumerate(messages):
|
| 88 |
+
processed_message = {
|
| 89 |
+
"role": message.get("role"),
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
# 如果有content字段,添加到消息中
|
| 93 |
+
if "content" in message and message["content"]:
|
| 94 |
+
processed_message["content"] = message["content"]
|
| 95 |
+
else:
|
| 96 |
+
processed_message["content"] = ""
|
| 97 |
+
|
| 98 |
+
# 如果有tool字段,添加到消息中
|
| 99 |
+
if "tool" in message and message["tool"]:
|
| 100 |
+
processed_message["tool"] = message["tool"]
|
| 101 |
+
|
| 102 |
+
processed_messages.append(processed_message)
|
| 103 |
+
|
| 104 |
+
export_data = {
|
| 105 |
+
"chat_id": chat_id,
|
| 106 |
+
"chat_name": chat_name,
|
| 107 |
+
"export_date": datetime.now().isoformat(),
|
| 108 |
+
"total_messages": len(processed_messages),
|
| 109 |
+
"messages": processed_messages
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
return json.dumps(export_data, indent=2, ensure_ascii=False)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def get_chat_by_id(chat_id: str) -> Dict[str, Any]:
|
| 116 |
+
"""
|
| 117 |
+
Get a specific chat by its ID from session state
|
| 118 |
+
"""
|
| 119 |
+
# If it's the current chat, always build from live session messages to avoid staleness
|
| 120 |
+
current_chat_id = st.session_state.get("current_chat_id")
|
| 121 |
+
if current_chat_id == chat_id:
|
| 122 |
+
current_messages = st.session_state.get("messages", [])
|
| 123 |
+
# Prefer the name from history if available
|
| 124 |
+
chat_name = st.session_state.get("current_chat_name", "Current Chat")
|
| 125 |
+
for chat in st.session_state.get("history_chats", []):
|
| 126 |
+
if chat.get("chat_id") == chat_id:
|
| 127 |
+
chat_name = chat.get("chat_name", chat_name)
|
| 128 |
+
break
|
| 129 |
+
return {
|
| 130 |
+
"chat_id": chat_id,
|
| 131 |
+
"chat_name": chat_name,
|
| 132 |
+
"messages": current_messages
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
# Otherwise, return from history if present
|
| 136 |
+
for chat in st.session_state.get("history_chats", []):
|
| 137 |
+
if chat.get("chat_id") == chat_id:
|
| 138 |
+
return chat
|
| 139 |
+
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def create_download_button_for_chat(chat_id: str, file_format: str = "json"):
|
| 144 |
+
"""
|
| 145 |
+
Create a download button for a specific chat
|
| 146 |
+
"""
|
| 147 |
+
chat_data = get_chat_by_id(chat_id)
|
| 148 |
+
if not chat_data:
|
| 149 |
+
st.error("Chat not found")
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
if file_format == "json":
|
| 153 |
+
content = export_chat_to_json(chat_data)
|
| 154 |
+
filename = f"chat_{chat_data['chat_name'].replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
| 155 |
+
mime_type = "application/json"
|
| 156 |
+
else:
|
| 157 |
+
st.error("Unsupported file format")
|
| 158 |
+
return
|
| 159 |
+
|
| 160 |
+
st.download_button(
|
| 161 |
+
label="📥 Download JSON",
|
| 162 |
+
data=content,
|
| 163 |
+
file_name=filename,
|
| 164 |
+
mime=mime_type,
|
| 165 |
+
help="Download complete chat history as JSON file"
|
| 166 |
+
)
|
client/services/import_service.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import json
|
| 3 |
+
import uuid
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Dict, Any
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def parse_json_chat(file_content: str) -> Dict[str, Any]:
|
| 9 |
+
"""
|
| 10 |
+
Parse a JSON chat file and extract chat data
|
| 11 |
+
"""
|
| 12 |
+
try:
|
| 13 |
+
data = json.loads(file_content)
|
| 14 |
+
return {
|
| 15 |
+
'chat_id': str(uuid.uuid4()), # Generate new ID for imported chat
|
| 16 |
+
'chat_name': data.get('chat_name', 'Imported Chat'),
|
| 17 |
+
'messages': data.get('messages', [])
|
| 18 |
+
}
|
| 19 |
+
except json.JSONDecodeError as e:
|
| 20 |
+
st.error(f"Invalid JSON format: {str(e)}")
|
| 21 |
+
return None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def import_chat_from_file(uploaded_file) -> Dict[str, Any]:
|
| 25 |
+
"""
|
| 26 |
+
Import chat data from uploaded file (JSON format only)
|
| 27 |
+
"""
|
| 28 |
+
if uploaded_file is None:
|
| 29 |
+
return None
|
| 30 |
+
|
| 31 |
+
file_content = uploaded_file.read().decode('utf-8')
|
| 32 |
+
file_name = uploaded_file.name.lower()
|
| 33 |
+
|
| 34 |
+
if file_name.endswith('.json'):
|
| 35 |
+
return parse_json_chat(file_content)
|
| 36 |
+
else:
|
| 37 |
+
st.error("Unsupported file format. Please upload a .json file.")
|
| 38 |
+
return None
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def add_imported_chat_to_history(chat_data: Dict[str, Any]):
|
| 42 |
+
"""
|
| 43 |
+
Add imported chat to session state history
|
| 44 |
+
"""
|
| 45 |
+
if not chat_data or not chat_data.get('messages'):
|
| 46 |
+
st.error("No valid chat data to import")
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
# Add timestamp to chat name if it's a duplicate
|
| 50 |
+
original_name = chat_data['chat_name']
|
| 51 |
+
existing_names = [chat['chat_name'] for chat in st.session_state.get('history_chats', [])]
|
| 52 |
+
|
| 53 |
+
if original_name in existing_names:
|
| 54 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 55 |
+
chat_data['chat_name'] = f"{original_name} (Imported {timestamp})"
|
| 56 |
+
|
| 57 |
+
# Add to history
|
| 58 |
+
st.session_state['history_chats'].append(chat_data)
|
| 59 |
+
|
| 60 |
+
# Switch to the imported chat
|
| 61 |
+
st.session_state['current_chat_index'] = 0
|
| 62 |
+
st.session_state['current_chat_id'] = chat_data['chat_id']
|
| 63 |
+
st.session_state['messages'] = chat_data['messages']
|
| 64 |
+
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def create_import_widget():
|
| 69 |
+
"""
|
| 70 |
+
Create a file upload widget for importing chat history (JSON format only)
|
| 71 |
+
"""
|
| 72 |
+
st.markdown("**📁 Import Chat History:**")
|
| 73 |
+
|
| 74 |
+
uploaded_file = st.file_uploader(
|
| 75 |
+
"Choose a chat history file",
|
| 76 |
+
type=['json'],
|
| 77 |
+
help="Upload a previously exported chat history file (.json format only)"
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
if uploaded_file:
|
| 81 |
+
if st.button("📥 Import Chat"):
|
| 82 |
+
with st.spinner("Importing chat history..."):
|
| 83 |
+
chat_data = import_chat_from_file(uploaded_file)
|
| 84 |
+
if chat_data:
|
| 85 |
+
success = add_imported_chat_to_history(chat_data)
|
| 86 |
+
if success:
|
| 87 |
+
st.success(f"✅ Successfully imported chat: {chat_data['chat_name']}")
|
| 88 |
+
st.rerun()
|
| 89 |
+
else:
|
| 90 |
+
st.error("❌ Failed to import chat")
|
| 91 |
+
else:
|
| 92 |
+
st.error("❌ Invalid file format or content")
|
client/services/logging_service.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Optional, Dict, Any, List
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ChatLogger:
|
| 9 |
+
"""
|
| 10 |
+
聊天应用的关键日志记录器
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, log_dir: str = "logs"):
|
| 14 |
+
self.log_dir = log_dir
|
| 15 |
+
self._ensure_log_dir()
|
| 16 |
+
self._setup_loggers()
|
| 17 |
+
|
| 18 |
+
def _ensure_log_dir(self):
|
| 19 |
+
"""确保日志目录存在"""
|
| 20 |
+
if not os.path.exists(self.log_dir):
|
| 21 |
+
os.makedirs(self.log_dir)
|
| 22 |
+
|
| 23 |
+
def _setup_loggers(self):
|
| 24 |
+
"""设置不同类型的日志记录器"""
|
| 25 |
+
# 用户行为日志
|
| 26 |
+
self.user_logger = logging.getLogger('user_actions')
|
| 27 |
+
self.user_logger.setLevel(logging.INFO)
|
| 28 |
+
# 防止重复日志
|
| 29 |
+
self.user_logger.handlers.clear()
|
| 30 |
+
|
| 31 |
+
user_handler = logging.FileHandler(
|
| 32 |
+
os.path.join(self.log_dir, 'user_actions.log'),
|
| 33 |
+
encoding='utf-8'
|
| 34 |
+
)
|
| 35 |
+
user_formatter = logging.Formatter(
|
| 36 |
+
'📝 %(asctime)s - %(levelname)s - %(message)s'
|
| 37 |
+
)
|
| 38 |
+
user_handler.setFormatter(user_formatter)
|
| 39 |
+
self.user_logger.addHandler(user_handler)
|
| 40 |
+
|
| 41 |
+
# 添加控制台输出
|
| 42 |
+
console_handler = logging.StreamHandler()
|
| 43 |
+
console_handler.setFormatter(user_formatter)
|
| 44 |
+
self.user_logger.addHandler(console_handler)
|
| 45 |
+
|
| 46 |
+
# MCP服务日志
|
| 47 |
+
self.mcp_logger = logging.getLogger('mcp_services')
|
| 48 |
+
self.mcp_logger.setLevel(logging.INFO)
|
| 49 |
+
# 防止重复日志
|
| 50 |
+
self.mcp_logger.handlers.clear()
|
| 51 |
+
|
| 52 |
+
mcp_handler = logging.FileHandler(
|
| 53 |
+
os.path.join(self.log_dir, 'mcp_services.log'),
|
| 54 |
+
encoding='utf-8'
|
| 55 |
+
)
|
| 56 |
+
mcp_formatter = logging.Formatter(
|
| 57 |
+
'🔧 %(asctime)s - %(levelname)s - %(message)s'
|
| 58 |
+
)
|
| 59 |
+
mcp_handler.setFormatter(mcp_formatter)
|
| 60 |
+
self.mcp_logger.addHandler(mcp_handler)
|
| 61 |
+
|
| 62 |
+
# 添加控制台输出
|
| 63 |
+
mcp_console_handler = logging.StreamHandler()
|
| 64 |
+
mcp_console_handler.setFormatter(mcp_formatter)
|
| 65 |
+
self.mcp_logger.addHandler(mcp_console_handler)
|
| 66 |
+
|
| 67 |
+
# 系统状态日志
|
| 68 |
+
self.system_logger = logging.getLogger('system_status')
|
| 69 |
+
self.system_logger.setLevel(logging.INFO)
|
| 70 |
+
# 防止重复日志
|
| 71 |
+
self.system_logger.handlers.clear()
|
| 72 |
+
|
| 73 |
+
system_handler = logging.FileHandler(
|
| 74 |
+
os.path.join(self.log_dir, 'system_status.log'),
|
| 75 |
+
encoding='utf-8'
|
| 76 |
+
)
|
| 77 |
+
system_formatter = logging.Formatter(
|
| 78 |
+
'🏥 %(asctime)s - %(levelname)s - %(message)s'
|
| 79 |
+
)
|
| 80 |
+
system_handler.setFormatter(system_formatter)
|
| 81 |
+
self.system_logger.addHandler(system_handler)
|
| 82 |
+
|
| 83 |
+
# 添加控制台输出
|
| 84 |
+
system_console_handler = logging.StreamHandler()
|
| 85 |
+
system_console_handler.setFormatter(system_formatter)
|
| 86 |
+
self.system_logger.addHandler(system_console_handler)
|
| 87 |
+
|
| 88 |
+
# 错误日志
|
| 89 |
+
self.error_logger = logging.getLogger('errors')
|
| 90 |
+
self.error_logger.setLevel(logging.ERROR)
|
| 91 |
+
# 防止重复日志
|
| 92 |
+
self.error_logger.handlers.clear()
|
| 93 |
+
|
| 94 |
+
error_handler = logging.FileHandler(
|
| 95 |
+
os.path.join(self.log_dir, 'errors.log'),
|
| 96 |
+
encoding='utf-8'
|
| 97 |
+
)
|
| 98 |
+
error_formatter = logging.Formatter(
|
| 99 |
+
'❌ %(asctime)s - %(levelname)s - %(message)s'
|
| 100 |
+
)
|
| 101 |
+
error_handler.setFormatter(error_formatter)
|
| 102 |
+
self.error_logger.addHandler(error_handler)
|
| 103 |
+
|
| 104 |
+
# 添加控制台输出
|
| 105 |
+
error_console_handler = logging.StreamHandler()
|
| 106 |
+
error_console_handler.setFormatter(error_formatter)
|
| 107 |
+
self.error_logger.addHandler(error_console_handler)
|
| 108 |
+
|
| 109 |
+
def log_user_action(self, action: str, details: Optional[Dict[str, Any]] = None):
|
| 110 |
+
"""记录用户行为"""
|
| 111 |
+
message = f"USER_ACTION: {action}"
|
| 112 |
+
if details:
|
| 113 |
+
message += f" - Details: {json.dumps(details, ensure_ascii=False)}"
|
| 114 |
+
self.user_logger.info(message)
|
| 115 |
+
|
| 116 |
+
def log_mcp_connection(self, server_name: str, server_url: str, success: bool, error: Optional[str] = None):
|
| 117 |
+
"""记录MCP服务器连接"""
|
| 118 |
+
status = "SUCCESS" if success else "FAILED"
|
| 119 |
+
message = f"MCP_CONNECTION: {server_name} ({server_url}) - {status}"
|
| 120 |
+
if error:
|
| 121 |
+
message += f" - Error: {error}"
|
| 122 |
+
self.mcp_logger.info(message)
|
| 123 |
+
|
| 124 |
+
def log_mcp_tool_call(self, tool_name: str, input_data: Dict[str, Any], chat_id: Optional[str] = None):
|
| 125 |
+
"""记录MCP工具调用"""
|
| 126 |
+
message = f"MCP_TOOL_CALL: {tool_name}"
|
| 127 |
+
if chat_id:
|
| 128 |
+
message += f" - ChatID: {chat_id}"
|
| 129 |
+
message += f" - Input: {json.dumps(input_data, ensure_ascii=False)}"
|
| 130 |
+
self.mcp_logger.info(message)
|
| 131 |
+
|
| 132 |
+
def log_mcp_tool_response(self, tool_name: str, response_data: Any, chat_id: Optional[str] = None):
|
| 133 |
+
"""记录MCP工具响应"""
|
| 134 |
+
message = f"MCP_TOOL_RESPONSE: {tool_name}"
|
| 135 |
+
if chat_id:
|
| 136 |
+
message += f" - ChatID: {chat_id}"
|
| 137 |
+
|
| 138 |
+
# 对于大型响应,只记录摘要
|
| 139 |
+
if isinstance(response_data, str) and len(response_data) > 1000:
|
| 140 |
+
message += f" - Response: {response_data[:500]}... (truncated, total length: {len(response_data)})"
|
| 141 |
+
else:
|
| 142 |
+
message += f" - Response: {json.dumps(response_data, ensure_ascii=False)}"
|
| 143 |
+
|
| 144 |
+
self.mcp_logger.info(message)
|
| 145 |
+
|
| 146 |
+
def log_mcp_agent_usage(self, agent_type: str, tools_used: List[str], chat_id: Optional[str] = None):
|
| 147 |
+
"""记录MCP代理使用情况"""
|
| 148 |
+
if agent_type == "ReactAgent":
|
| 149 |
+
message = f"MCP_AGENT_AVAILABLE: {agent_type}"
|
| 150 |
+
if chat_id:
|
| 151 |
+
message += f" - ChatID: {chat_id}"
|
| 152 |
+
message += f" - Available Tools: {', '.join(tools_used)}"
|
| 153 |
+
else:
|
| 154 |
+
message = f"MCP_AGENT_USAGE: {agent_type}"
|
| 155 |
+
if chat_id:
|
| 156 |
+
message += f" - ChatID: {chat_id}"
|
| 157 |
+
message += f" - Tools Used: {', '.join(tools_used)}"
|
| 158 |
+
self.mcp_logger.info(message)
|
| 159 |
+
|
| 160 |
+
def log_chat_message(self, role: str, content: str, chat_id: Optional[str] = None, has_tool: bool = False):
|
| 161 |
+
"""记录聊天消息"""
|
| 162 |
+
message = f"CHAT_MESSAGE: {role.upper()}"
|
| 163 |
+
if chat_id:
|
| 164 |
+
message += f" - ChatID: {chat_id}"
|
| 165 |
+
if has_tool:
|
| 166 |
+
message += " - HasTool: True"
|
| 167 |
+
|
| 168 |
+
# 对于长消息,只记录摘要
|
| 169 |
+
if len(content) > 500:
|
| 170 |
+
message += f" - Content: {content[:200]}... (truncated, total length: {len(content)})"
|
| 171 |
+
else:
|
| 172 |
+
message += f" - Content: {content}"
|
| 173 |
+
|
| 174 |
+
self.user_logger.info(message)
|
| 175 |
+
|
| 176 |
+
def log_llm_test(self, provider: str, success: bool, error: Optional[str] = None):
|
| 177 |
+
"""记录LLM连接测试"""
|
| 178 |
+
status = "SUCCESS" if success else "FAILED"
|
| 179 |
+
message = f"LLM_TEST: {provider} - {status}"
|
| 180 |
+
if error:
|
| 181 |
+
message += f" - Error: {error}"
|
| 182 |
+
self.system_logger.info(message)
|
| 183 |
+
|
| 184 |
+
def log_system_status(self, status: str, details: Optional[Dict[str, Any]] = None):
|
| 185 |
+
"""记录系统状态"""
|
| 186 |
+
message = f"SYSTEM_STATUS: {status}"
|
| 187 |
+
if details:
|
| 188 |
+
message += f" - Details: {json.dumps(details, ensure_ascii=False)}"
|
| 189 |
+
self.system_logger.info(message)
|
| 190 |
+
|
| 191 |
+
def log_error(self, error_type: str, error_message: str, context: Optional[Dict[str, Any]] = None):
|
| 192 |
+
"""记录错误"""
|
| 193 |
+
message = f"ERROR: {error_type} - {error_message}"
|
| 194 |
+
if context:
|
| 195 |
+
message += f" - Context: {json.dumps(context, ensure_ascii=False)}"
|
| 196 |
+
self.error_logger.error(message)
|
| 197 |
+
|
| 198 |
+
def log_long_running_task(self, task_name: str, duration_seconds: float, chat_id: Optional[str] = None):
|
| 199 |
+
"""记录长时间运行的任务"""
|
| 200 |
+
message = f"LONG_RUNNING_TASK: {task_name} - Duration: {duration_seconds:.2f}s"
|
| 201 |
+
if chat_id:
|
| 202 |
+
message += f" - ChatID: {chat_id}"
|
| 203 |
+
self.system_logger.info(message)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
# 全局日志记录器实例
|
| 207 |
+
chat_logger = ChatLogger()
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def get_logger():
|
| 211 |
+
"""获取全局日志记录器"""
|
| 212 |
+
return chat_logger
|
client/services/mcp_service.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, List
|
| 2 |
+
import streamlit as st
|
| 3 |
+
|
| 4 |
+
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 5 |
+
from langgraph.prebuilt import create_react_agent
|
| 6 |
+
from langchain_core.tools import BaseTool
|
| 7 |
+
from services.ai_service import create_llm_model
|
| 8 |
+
from services.logging_service import get_logger
|
| 9 |
+
from utils.async_helpers import run_async
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
async def setup_mcp_client(server_config: Dict[str, Dict]) -> MultiServerMCPClient:
|
| 13 |
+
"""Initialize a MultiServerMCPClient with the provided server configuration."""
|
| 14 |
+
client = MultiServerMCPClient(server_config)
|
| 15 |
+
return client
|
| 16 |
+
|
| 17 |
+
async def get_tools_from_client(client: MultiServerMCPClient) -> List[BaseTool]:
|
| 18 |
+
"""Get tools from the MCP client."""
|
| 19 |
+
return await client.get_tools()
|
| 20 |
+
|
| 21 |
+
async def run_agent(agent, message: str) -> Dict:
|
| 22 |
+
"""Run the agent with the provided message."""
|
| 23 |
+
return await agent.ainvoke({"messages": message})
|
| 24 |
+
|
| 25 |
+
async def run_tool(tool, **kwargs):
|
| 26 |
+
"""Run a tool with the provided parameters."""
|
| 27 |
+
return await tool.ainvoke(**kwargs)
|
| 28 |
+
|
| 29 |
+
def connect_to_mcp_servers():
|
| 30 |
+
logger = get_logger()
|
| 31 |
+
|
| 32 |
+
# Clean up existing client if any
|
| 33 |
+
client = st.session_state.get("client")
|
| 34 |
+
if client:
|
| 35 |
+
try:
|
| 36 |
+
# No need to call __aexit__ since we're not using context manager
|
| 37 |
+
logger.log_system_status("Cleaned up previous MCP client")
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.log_error("MCP_Client_Cleanup_Error", str(e))
|
| 40 |
+
st.warning(f"Error closing previous client: {str(e)}")
|
| 41 |
+
|
| 42 |
+
# Collect LLM config dynamically from session state
|
| 43 |
+
params = st.session_state['params']
|
| 44 |
+
llm_provider = params.get("model_id")
|
| 45 |
+
try:
|
| 46 |
+
llm = create_llm_model(llm_provider, temperature=params['temperature'], max_tokens=params['max_tokens'])
|
| 47 |
+
logger.log_system_status(f"Initialized LLM: {llm_provider}")
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.log_error("LLM_Initialization_Error", str(e), {'provider': llm_provider})
|
| 50 |
+
st.error(f"Failed to initialize LLM: {e}")
|
| 51 |
+
st.stop()
|
| 52 |
+
return
|
| 53 |
+
|
| 54 |
+
# Setup new client
|
| 55 |
+
try:
|
| 56 |
+
st.session_state.client = run_async(setup_mcp_client(st.session_state.servers))
|
| 57 |
+
st.session_state.tools = run_async(get_tools_from_client(st.session_state.client))
|
| 58 |
+
st.session_state.agent = create_react_agent(llm, st.session_state.tools)
|
| 59 |
+
|
| 60 |
+
# Log successful connection
|
| 61 |
+
logger.log_system_status("MCP client setup completed", {
|
| 62 |
+
'servers_count': len(st.session_state.servers),
|
| 63 |
+
'tools_count': len(st.session_state.tools)
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
# 记录每个服务器的详细信息
|
| 67 |
+
for server_name, server_config in st.session_state.servers.items():
|
| 68 |
+
logger.log_mcp_connection(
|
| 69 |
+
server_name,
|
| 70 |
+
server_config.get('url', 'unknown'),
|
| 71 |
+
True
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# 记录所有可用工具
|
| 75 |
+
tool_names = [tool.name for tool in st.session_state.tools]
|
| 76 |
+
logger.log_system_status("Available MCP tools", {
|
| 77 |
+
'tools': tool_names,
|
| 78 |
+
'total_tools': len(tool_names)
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
except Exception as e:
|
| 82 |
+
logger.log_error("MCP_Client_Setup_Error", str(e), {
|
| 83 |
+
'servers': list(st.session_state.servers.keys()),
|
| 84 |
+
'llm_provider': llm_provider
|
| 85 |
+
})
|
| 86 |
+
raise e
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def disconnect_from_mcp_servers():
|
| 90 |
+
# Clean up existing client if any and session state connections
|
| 91 |
+
client = st.session_state.get("client")
|
| 92 |
+
if client:
|
| 93 |
+
try:
|
| 94 |
+
# No need to call __aexit__ since we're not using context manager
|
| 95 |
+
pass
|
| 96 |
+
except Exception as e:
|
| 97 |
+
st.warning(f"Error during disconnect: {str(e)}")
|
| 98 |
+
else:
|
| 99 |
+
st.info("No MCP connection to disconnect.")
|
| 100 |
+
|
| 101 |
+
# Clean up session state
|
| 102 |
+
st.session_state.client = None
|
| 103 |
+
st.session_state.tools = []
|
| 104 |
+
st.session_state.agent = None
|
client/services/task_monitor.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from typing import Optional, Callable, Dict, Any
|
| 5 |
+
from services.logging_service import get_logger
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LongRunningTaskMonitor:
|
| 9 |
+
"""
|
| 10 |
+
长时间运行任务监控器,用于在MCP工具执行期间定期发送心跳
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def __init__(self, heartbeat_interval: int = 300): # 5分钟 = 300秒
|
| 14 |
+
self.heartbeat_interval = heartbeat_interval
|
| 15 |
+
self.logger = get_logger()
|
| 16 |
+
self.active_tasks: Dict[str, Dict[str, Any]] = {}
|
| 17 |
+
|
| 18 |
+
def start_monitoring(self, task_id: str, task_name: str, chat_id: Optional[str] = None,
|
| 19 |
+
heartbeat_callback: Optional[Callable] = None):
|
| 20 |
+
"""
|
| 21 |
+
开始监控一个长时间运行的任务
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
task_id: 任务唯一标识
|
| 25 |
+
task_name: 任务名称
|
| 26 |
+
chat_id: 聊天ID
|
| 27 |
+
heartbeat_callback: 心跳回调函数
|
| 28 |
+
"""
|
| 29 |
+
self.active_tasks[task_id] = {
|
| 30 |
+
'task_name': task_name,
|
| 31 |
+
'chat_id': chat_id,
|
| 32 |
+
'start_time': time.time(),
|
| 33 |
+
'heartbeat_callback': heartbeat_callback,
|
| 34 |
+
'last_heartbeat': time.time(),
|
| 35 |
+
'heartbeat_count': 0
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
self.logger.log_system_status(
|
| 39 |
+
f"Started monitoring long-running task: {task_name}",
|
| 40 |
+
{'task_id': task_id, 'chat_id': chat_id}
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
def stop_monitoring(self, task_id: str):
|
| 44 |
+
"""
|
| 45 |
+
停止监控一个任务
|
| 46 |
+
|
| 47 |
+
Args:
|
| 48 |
+
task_id: 任务唯一标识
|
| 49 |
+
"""
|
| 50 |
+
if task_id in self.active_tasks:
|
| 51 |
+
task_info = self.active_tasks[task_id]
|
| 52 |
+
duration = time.time() - task_info['start_time']
|
| 53 |
+
|
| 54 |
+
self.logger.log_long_running_task(
|
| 55 |
+
task_info['task_name'],
|
| 56 |
+
duration,
|
| 57 |
+
task_info['chat_id']
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
del self.active_tasks[task_id]
|
| 61 |
+
|
| 62 |
+
async def send_heartbeat(self, task_id: str):
|
| 63 |
+
"""
|
| 64 |
+
发送心跳信号
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
task_id: 任务唯一标识
|
| 68 |
+
"""
|
| 69 |
+
if task_id not in self.active_tasks:
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
task_info = self.active_tasks[task_id]
|
| 73 |
+
current_time = time.time()
|
| 74 |
+
|
| 75 |
+
# 检查是否需要发送心跳
|
| 76 |
+
if current_time - task_info['last_heartbeat'] >= self.heartbeat_interval:
|
| 77 |
+
task_info['last_heartbeat'] = current_time
|
| 78 |
+
task_info['heartbeat_count'] += 1
|
| 79 |
+
|
| 80 |
+
duration = current_time - task_info['start_time']
|
| 81 |
+
|
| 82 |
+
# 记录心跳日志
|
| 83 |
+
self.logger.log_system_status(
|
| 84 |
+
f"Heartbeat for long-running task: {task_info['task_name']}",
|
| 85 |
+
{
|
| 86 |
+
'task_id': task_id,
|
| 87 |
+
'chat_id': task_info['chat_id'],
|
| 88 |
+
'duration_seconds': duration,
|
| 89 |
+
'heartbeat_count': task_info['heartbeat_count']
|
| 90 |
+
}
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
# 执行心跳回调
|
| 94 |
+
if task_info['heartbeat_callback']:
|
| 95 |
+
try:
|
| 96 |
+
await task_info['heartbeat_callback'](task_id, task_info)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
self.logger.log_error(
|
| 99 |
+
"HeartbeatCallbackError",
|
| 100 |
+
str(e),
|
| 101 |
+
{'task_id': task_id, 'task_name': task_info['task_name']}
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
async def monitor_all_tasks(self):
|
| 105 |
+
"""
|
| 106 |
+
监控所有活跃任务并发送心跳
|
| 107 |
+
"""
|
| 108 |
+
while True:
|
| 109 |
+
try:
|
| 110 |
+
# 为每个活跃任务发送心跳
|
| 111 |
+
for task_id in list(self.active_tasks.keys()):
|
| 112 |
+
await self.send_heartbeat(task_id)
|
| 113 |
+
|
| 114 |
+
# 等待下一次检查
|
| 115 |
+
await asyncio.sleep(60) # 每分钟检查一次
|
| 116 |
+
|
| 117 |
+
except Exception as e:
|
| 118 |
+
self.logger.log_error(
|
| 119 |
+
"TaskMonitorError",
|
| 120 |
+
str(e),
|
| 121 |
+
{'active_tasks_count': len(self.active_tasks)}
|
| 122 |
+
)
|
| 123 |
+
await asyncio.sleep(60) # 出错后等待1分钟再继续
|
| 124 |
+
|
| 125 |
+
def get_active_tasks_info(self) -> Dict[str, Dict[str, Any]]:
|
| 126 |
+
"""
|
| 127 |
+
获取所有活跃任务的信息
|
| 128 |
+
|
| 129 |
+
Returns:
|
| 130 |
+
活跃任务信息字典
|
| 131 |
+
"""
|
| 132 |
+
result = {}
|
| 133 |
+
current_time = time.time()
|
| 134 |
+
|
| 135 |
+
for task_id, task_info in self.active_tasks.items():
|
| 136 |
+
duration = current_time - task_info['start_time']
|
| 137 |
+
result[task_id] = {
|
| 138 |
+
'task_name': task_info['task_name'],
|
| 139 |
+
'chat_id': task_info['chat_id'],
|
| 140 |
+
'duration_seconds': duration,
|
| 141 |
+
'heartbeat_count': task_info['heartbeat_count'],
|
| 142 |
+
'last_heartbeat_seconds_ago': current_time - task_info['last_heartbeat']
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
return result
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
# 全局任务监控器实例
|
| 149 |
+
task_monitor = LongRunningTaskMonitor()
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def get_task_monitor():
|
| 153 |
+
"""获取全局任务监控器"""
|
| 154 |
+
return task_monitor
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
async def start_task_monitoring():
|
| 158 |
+
"""启动任务监控"""
|
| 159 |
+
monitor = get_task_monitor()
|
| 160 |
+
await monitor.monitor_all_tasks()
|
client/static/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
client/ui_components/__init__.py
ADDED
|
File without changes
|
client/ui_components/log_viewer.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from services.logging_service import get_logger
|
| 5 |
+
from services.task_monitor import get_task_monitor
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def create_log_viewer():
|
| 9 |
+
"""
|
| 10 |
+
创建日志查看器组件
|
| 11 |
+
"""
|
| 12 |
+
st.markdown("## 📊 System Logs")
|
| 13 |
+
|
| 14 |
+
# 日志类型选择
|
| 15 |
+
log_types = {
|
| 16 |
+
"User Actions": "user_actions.log",
|
| 17 |
+
"MCP Services": "mcp_services.log",
|
| 18 |
+
"System Status": "system_status.log",
|
| 19 |
+
"Errors": "errors.log"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
selected_log = st.selectbox(
|
| 23 |
+
"Select Log Type",
|
| 24 |
+
options=list(log_types.keys()),
|
| 25 |
+
index=1 # 默认选择MCP Services
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# 时间范围选择
|
| 29 |
+
time_ranges = {
|
| 30 |
+
"Last Hour": 1,
|
| 31 |
+
"Last 6 Hours": 6,
|
| 32 |
+
"Last 24 Hours": 24,
|
| 33 |
+
"Last 7 Days": 168,
|
| 34 |
+
"All": 0
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
selected_range = st.selectbox(
|
| 38 |
+
"Time Range",
|
| 39 |
+
options=list(time_ranges.keys()),
|
| 40 |
+
index=2
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
# 显示日志内容
|
| 44 |
+
log_file = log_types[selected_log]
|
| 45 |
+
log_path = os.path.join("logs", log_file)
|
| 46 |
+
|
| 47 |
+
if os.path.exists(log_path):
|
| 48 |
+
# 读取日志文件
|
| 49 |
+
with open(log_path, 'r', encoding='utf-8') as f:
|
| 50 |
+
lines = f.readlines()
|
| 51 |
+
|
| 52 |
+
# 过滤时间范围
|
| 53 |
+
if time_ranges[selected_range] > 0:
|
| 54 |
+
cutoff_time = datetime.now() - timedelta(hours=time_ranges[selected_range])
|
| 55 |
+
filtered_lines = []
|
| 56 |
+
|
| 57 |
+
for line in lines:
|
| 58 |
+
try:
|
| 59 |
+
# 解析时间戳
|
| 60 |
+
timestamp_str = line.split(' - ')[0]
|
| 61 |
+
log_time = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S,%f')
|
| 62 |
+
if log_time >= cutoff_time:
|
| 63 |
+
filtered_lines.append(line)
|
| 64 |
+
except:
|
| 65 |
+
# 如果无法解析时间戳,保留该行
|
| 66 |
+
filtered_lines.append(line)
|
| 67 |
+
|
| 68 |
+
lines = filtered_lines
|
| 69 |
+
|
| 70 |
+
# 显示日志
|
| 71 |
+
if lines:
|
| 72 |
+
st.markdown(f"**Showing {len(lines)} log entries**")
|
| 73 |
+
|
| 74 |
+
# 搜索功能
|
| 75 |
+
search_term = st.text_input("Search in logs (e.g., 'bio_qa_stream_chat', 'review_generate')", "")
|
| 76 |
+
if search_term:
|
| 77 |
+
lines = [line for line in lines if search_term.lower() in line.lower()]
|
| 78 |
+
st.markdown(f"**Found {len(lines)} matching entries**")
|
| 79 |
+
|
| 80 |
+
# 显示日志内容
|
| 81 |
+
if lines:
|
| 82 |
+
# 只显示最后1000行以避免性能问题
|
| 83 |
+
display_lines = lines[-1000:] if len(lines) > 1000 else lines
|
| 84 |
+
|
| 85 |
+
st.text_area(
|
| 86 |
+
"Log Content",
|
| 87 |
+
value=''.join(display_lines),
|
| 88 |
+
height=400,
|
| 89 |
+
disabled=True
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
if len(lines) > 1000:
|
| 93 |
+
st.info(f"Showing last 1000 lines of {len(lines)} total entries")
|
| 94 |
+
else:
|
| 95 |
+
st.info("No log entries found matching the criteria")
|
| 96 |
+
else:
|
| 97 |
+
st.info("No log entries found in the selected time range")
|
| 98 |
+
else:
|
| 99 |
+
st.warning(f"Log file {log_file} not found")
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def create_system_status_dashboard():
|
| 103 |
+
"""
|
| 104 |
+
创建系统状态仪表板
|
| 105 |
+
"""
|
| 106 |
+
st.markdown("## 🏥 System Status Dashboard")
|
| 107 |
+
|
| 108 |
+
logger = get_logger()
|
| 109 |
+
task_monitor = get_task_monitor()
|
| 110 |
+
|
| 111 |
+
# 获取活跃任务信息
|
| 112 |
+
active_tasks = task_monitor.get_active_tasks_info()
|
| 113 |
+
|
| 114 |
+
# 显示活跃任务
|
| 115 |
+
if active_tasks:
|
| 116 |
+
st.markdown("### 🔄 Active Long-Running Tasks")
|
| 117 |
+
for task_id, task_info in active_tasks.items():
|
| 118 |
+
with st.expander(f"Task: {task_info['task_name']}", expanded=False):
|
| 119 |
+
col1, col2, col3 = st.columns(3)
|
| 120 |
+
with col1:
|
| 121 |
+
st.metric("Duration", f"{task_info['duration_seconds']:.1f}s")
|
| 122 |
+
with col2:
|
| 123 |
+
st.metric("Heartbeats", task_info['heartbeat_count'])
|
| 124 |
+
with col3:
|
| 125 |
+
st.metric("Last Heartbeat", f"{task_info['last_heartbeat_seconds_ago']:.1f}s ago")
|
| 126 |
+
|
| 127 |
+
if task_info['chat_id']:
|
| 128 |
+
st.info(f"Chat ID: {task_info['chat_id']}")
|
| 129 |
+
else:
|
| 130 |
+
st.info("No active long-running tasks")
|
| 131 |
+
|
| 132 |
+
# 显示系统统计信息
|
| 133 |
+
st.markdown("### 📈 System Statistics")
|
| 134 |
+
|
| 135 |
+
# 这里可以添加更多系统统计信息
|
| 136 |
+
# 比如:总对话数、总消息数、MCP工具调用次数等
|
| 137 |
+
|
| 138 |
+
col1, col2, col3 = st.columns(3)
|
| 139 |
+
with col1:
|
| 140 |
+
st.metric("Total Chats", len(st.session_state.get("history_chats", [])))
|
| 141 |
+
with col2:
|
| 142 |
+
total_messages = sum(len(chat.get('messages', [])) for chat in st.session_state.get("history_chats", []))
|
| 143 |
+
st.metric("Total Messages", total_messages)
|
| 144 |
+
with col3:
|
| 145 |
+
st.metric("MCP Tools", len(st.session_state.get("tools", [])))
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def create_log_management():
|
| 149 |
+
"""
|
| 150 |
+
创建日志管理功能
|
| 151 |
+
"""
|
| 152 |
+
st.markdown("## 🔧 Log Management")
|
| 153 |
+
|
| 154 |
+
col1, col2 = st.columns(2)
|
| 155 |
+
|
| 156 |
+
with col1:
|
| 157 |
+
if st.button("📥 Download All Logs"):
|
| 158 |
+
# 这里可以实现下载所有日志文件的功能
|
| 159 |
+
st.info("Log download feature coming soon...")
|
| 160 |
+
|
| 161 |
+
with col2:
|
| 162 |
+
if st.button("🗑️ Clear Old Logs"):
|
| 163 |
+
# 这里可以实现清理旧日志的功能
|
| 164 |
+
st.info("Log cleanup feature coming soon...")
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def main():
|
| 168 |
+
"""
|
| 169 |
+
主日志管理界面
|
| 170 |
+
"""
|
| 171 |
+
st.title("📊 System Monitoring & Logs")
|
| 172 |
+
|
| 173 |
+
# 创建选项卡
|
| 174 |
+
tab1, tab2, tab3 = st.tabs(["📋 Logs", "🏥 Status", "🔧 Management"])
|
| 175 |
+
|
| 176 |
+
with tab1:
|
| 177 |
+
create_log_viewer()
|
| 178 |
+
|
| 179 |
+
with tab2:
|
| 180 |
+
create_system_status_dashboard()
|
| 181 |
+
|
| 182 |
+
with tab3:
|
| 183 |
+
create_log_management()
|
client/ui_components/main_components.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import json
|
| 3 |
+
|
| 4 |
+
# Function to display tool execution details
|
| 5 |
+
def display_tool_executions():
|
| 6 |
+
if st.session_state.tool_executions:
|
| 7 |
+
with st.expander("Tool Execution History", expanded=False):
|
| 8 |
+
for i, exec_record in enumerate(st.session_state.tool_executions):
|
| 9 |
+
st.markdown(f"### Execution #{i+1}: `{exec_record['tool_name']}`")
|
| 10 |
+
st.markdown(f"**Input:** ```json{json.dumps(exec_record['input'])}```")
|
| 11 |
+
st.markdown(f"**Output:** ```{exec_record['output'][:250]}...```")
|
| 12 |
+
st.markdown(f"**Time:** {exec_record['timestamp']}")
|
| 13 |
+
st.divider()
|
client/ui_components/sidebar_components.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from config import MODEL_OPTIONS
|
| 3 |
+
import traceback
|
| 4 |
+
from services.mcp_service import connect_to_mcp_servers
|
| 5 |
+
from services.chat_service import create_chat, delete_chat, get_current_chat
|
| 6 |
+
from services.export_service import create_download_button_for_chat
|
| 7 |
+
from services.import_service import create_import_widget
|
| 8 |
+
from services.logging_service import get_logger
|
| 9 |
+
from utils.tool_schema_parser import extract_tool_parameters
|
| 10 |
+
from utils.async_helpers import reset_connection_state
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def create_history_chat_container():
|
| 14 |
+
history_container = st.sidebar.container(height=400, border=None)
|
| 15 |
+
with history_container:
|
| 16 |
+
chat_history_menu = [
|
| 17 |
+
f"{chat['chat_name']}_::_{chat['chat_id']}"
|
| 18 |
+
for chat in st.session_state["history_chats"]
|
| 19 |
+
]
|
| 20 |
+
chat_history_menu = chat_history_menu[:50][::-1]
|
| 21 |
+
|
| 22 |
+
if chat_history_menu:
|
| 23 |
+
current_chat = st.radio(
|
| 24 |
+
label="History Chats",
|
| 25 |
+
format_func=lambda x: x.split("_::_")[0] + '...' if "_::_" in x else x,
|
| 26 |
+
options=chat_history_menu,
|
| 27 |
+
label_visibility="collapsed",
|
| 28 |
+
index=st.session_state["current_chat_index"],
|
| 29 |
+
key="current_chat"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
if current_chat:
|
| 33 |
+
new_chat_id = current_chat.split("_::_")[1]
|
| 34 |
+
# Only update if chat actually changed
|
| 35 |
+
if st.session_state.get('current_chat_id') != new_chat_id:
|
| 36 |
+
logger = get_logger()
|
| 37 |
+
logger.log_system_status(f"Switching from chat {st.session_state.get('current_chat_id')} to {new_chat_id}")
|
| 38 |
+
|
| 39 |
+
st.session_state['current_chat_id'] = new_chat_id
|
| 40 |
+
# Update current chat index
|
| 41 |
+
for i, chat in enumerate(st.session_state["history_chats"]):
|
| 42 |
+
if chat['chat_id'] == new_chat_id:
|
| 43 |
+
st.session_state["current_chat_index"] = i
|
| 44 |
+
break
|
| 45 |
+
# Update messages to current chat
|
| 46 |
+
st.session_state["messages"] = get_current_chat(new_chat_id)
|
| 47 |
+
|
| 48 |
+
logger.log_system_status(f"Loaded {len(st.session_state['messages'])} messages for chat {new_chat_id}")
|
| 49 |
+
# Debug: log message structure
|
| 50 |
+
for i, msg in enumerate(st.session_state["messages"]):
|
| 51 |
+
has_tool = "tool" in msg and msg["tool"]
|
| 52 |
+
has_content = "content" in msg and msg["content"]
|
| 53 |
+
logger.log_system_status(f"Message {i}: role={msg.get('role')}, has_tool={has_tool}, has_content={has_content}")
|
| 54 |
+
|
| 55 |
+
# Add download buttons for the selected chat
|
| 56 |
+
chat_id = current_chat.split("_::_")[1]
|
| 57 |
+
st.markdown("---")
|
| 58 |
+
st.markdown("**📥 Export Chat History:**")
|
| 59 |
+
|
| 60 |
+
# Create download button for JSON export only
|
| 61 |
+
create_download_button_for_chat(chat_id, "json")
|
| 62 |
+
|
| 63 |
+
# Add import functionality
|
| 64 |
+
st.markdown("---")
|
| 65 |
+
create_import_widget()
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def create_sidebar_chat_buttons():
|
| 69 |
+
with st.sidebar:
|
| 70 |
+
c1, c2 = st.columns(2)
|
| 71 |
+
create_chat_button = c1.button(
|
| 72 |
+
"New Chat", use_container_width=True, key="create_chat_button"
|
| 73 |
+
)
|
| 74 |
+
if create_chat_button:
|
| 75 |
+
create_chat()
|
| 76 |
+
st.rerun()
|
| 77 |
+
|
| 78 |
+
delete_chat_button = c2.button(
|
| 79 |
+
"Delete Chat", use_container_width=True, key="delete_chat_button"
|
| 80 |
+
)
|
| 81 |
+
if delete_chat_button and st.session_state.get('current_chat_id'):
|
| 82 |
+
delete_chat(st.session_state['current_chat_id'])
|
| 83 |
+
st.rerun()
|
| 84 |
+
|
| 85 |
+
def create_model_select_widget():
|
| 86 |
+
params = st.session_state["params"]
|
| 87 |
+
params['model_id'] = st.sidebar.selectbox('🔎 Choose model',
|
| 88 |
+
options=MODEL_OPTIONS.keys(),
|
| 89 |
+
index=0)
|
| 90 |
+
|
| 91 |
+
def create_provider_select_widget():
|
| 92 |
+
params = st.session_state.setdefault('params', {})
|
| 93 |
+
# Load previously selected provider or default to the first
|
| 94 |
+
default_provider = params.get("model_id", list(MODEL_OPTIONS.keys())[0])
|
| 95 |
+
default_index = list(MODEL_OPTIONS.keys()).index(default_provider)
|
| 96 |
+
# Provider selector with synced state
|
| 97 |
+
selected_provider = st.sidebar.selectbox(
|
| 98 |
+
'🔎 Choose Provider',
|
| 99 |
+
options=list(MODEL_OPTIONS.keys()),
|
| 100 |
+
index=default_index,
|
| 101 |
+
key="provider_selection",
|
| 102 |
+
on_change=reset_connection_state
|
| 103 |
+
)
|
| 104 |
+
# Save new provider and its index
|
| 105 |
+
if selected_provider:
|
| 106 |
+
params['model_id'] = selected_provider
|
| 107 |
+
params['provider_index'] = list(MODEL_OPTIONS.keys()).index(selected_provider)
|
| 108 |
+
st.sidebar.success(f"Model: {MODEL_OPTIONS[selected_provider]}")
|
| 109 |
+
|
| 110 |
+
# Dynamic input fields based on provider
|
| 111 |
+
with st.sidebar.container():
|
| 112 |
+
if selected_provider == "Bedrock":
|
| 113 |
+
with st.expander("🔐 Bedrock Credentials", expanded=True):
|
| 114 |
+
# Configuration mode selector
|
| 115 |
+
config_mode = st.radio(
|
| 116 |
+
"Configuration Mode",
|
| 117 |
+
["🔄 Default", "✏️ Custom"],
|
| 118 |
+
key="bedrock_config_mode",
|
| 119 |
+
horizontal=True
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
if config_mode == "🔄 Default":
|
| 123 |
+
# Use environment variables - Force update params to ensure using environment variables
|
| 124 |
+
from config import DEFAULT_ENV_CONFIG
|
| 125 |
+
env_config = DEFAULT_ENV_CONFIG.get('Bedrock', {})
|
| 126 |
+
|
| 127 |
+
# Force set to environment variable values to ensure passing to LLM
|
| 128 |
+
params['region_name'] = env_config.get('region_name', '')
|
| 129 |
+
params['aws_access_key'] = env_config.get('aws_access_key', '')
|
| 130 |
+
params['aws_secret_key'] = env_config.get('aws_secret_key', '')
|
| 131 |
+
|
| 132 |
+
st.info("🔒 Using configuration from environment variables")
|
| 133 |
+
if env_config.get('region_name'):
|
| 134 |
+
st.success(f"Region: {env_config.get('region_name')}")
|
| 135 |
+
else:
|
| 136 |
+
st.warning("⚠️ AWS_REGION environment variable not set")
|
| 137 |
+
if env_config.get('aws_access_key'):
|
| 138 |
+
st.success("✅ AWS Access Key configured")
|
| 139 |
+
else:
|
| 140 |
+
st.warning("⚠️ AWS_ACCESS_KEY_ID environment variable not set")
|
| 141 |
+
if env_config.get('aws_secret_key'):
|
| 142 |
+
st.success("✅ AWS Secret Key configured")
|
| 143 |
+
else:
|
| 144 |
+
st.warning("⚠️ AWS_SECRET_ACCESS_KEY environment variable not set")
|
| 145 |
+
|
| 146 |
+
else: # Custom mode
|
| 147 |
+
# Clear parameters for user input
|
| 148 |
+
params['region_name'] = st.text_input("AWS Region", value='', placeholder="Enter AWS Region", key="region_name")
|
| 149 |
+
params['aws_access_key'] = st.text_input("AWS Access Key", value='', type="password", placeholder="Enter AWS Access Key", key="aws_access_key")
|
| 150 |
+
params['aws_secret_key'] = st.text_input("AWS Secret Key", value='', type="password", placeholder="Enter AWS Secret Key", key="aws_secret_key")
|
| 151 |
+
|
| 152 |
+
# Test button (always show)
|
| 153 |
+
if st.button("🧪 Test Connection", key="bedrock_test"):
|
| 154 |
+
from services.ai_service import test_llm_connection
|
| 155 |
+
test_params = {
|
| 156 |
+
'region_name': params.get('region_name'),
|
| 157 |
+
'aws_access_key': params.get('aws_access_key'),
|
| 158 |
+
'aws_secret_key': params.get('aws_secret_key')
|
| 159 |
+
}
|
| 160 |
+
success, message = test_llm_connection(selected_provider, test_params)
|
| 161 |
+
|
| 162 |
+
# Log the test result
|
| 163 |
+
logger = get_logger()
|
| 164 |
+
logger.log_llm_test(selected_provider, success, None if success else message)
|
| 165 |
+
|
| 166 |
+
if success:
|
| 167 |
+
st.success(message)
|
| 168 |
+
else:
|
| 169 |
+
st.error(message)
|
| 170 |
+
else:
|
| 171 |
+
with st.expander("🔐 API Key", expanded=True):
|
| 172 |
+
# Configuration mode selector
|
| 173 |
+
config_mode = st.radio(
|
| 174 |
+
"Configuration Mode",
|
| 175 |
+
["🔄 Default", "✏️ Custom"],
|
| 176 |
+
key=f"{selected_provider.lower()}_config_mode",
|
| 177 |
+
horizontal=True
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
from config import DEFAULT_ENV_CONFIG
|
| 181 |
+
env_config = DEFAULT_ENV_CONFIG.get(selected_provider, {})
|
| 182 |
+
|
| 183 |
+
if config_mode == "🔄 Default":
|
| 184 |
+
# Use environment variables - Force update params to ensure using environment variables
|
| 185 |
+
# Force set to environment variable values to ensure passing to LLM
|
| 186 |
+
params['api_key'] = env_config.get('api_key', '')
|
| 187 |
+
params['base_url'] = env_config.get('base_url', '')
|
| 188 |
+
|
| 189 |
+
st.info("🔒 Using configuration from environment variables")
|
| 190 |
+
if env_config.get('api_key'):
|
| 191 |
+
if selected_provider == "OpenAI":
|
| 192 |
+
st.success("✅ OpenAI API Key configured (hidden display)")
|
| 193 |
+
else:
|
| 194 |
+
st.success(f"✅ {selected_provider} API Key configured")
|
| 195 |
+
else:
|
| 196 |
+
st.warning(f"⚠️ {selected_provider.upper()}_API_KEY environment variable not set")
|
| 197 |
+
if env_config.get('base_url'):
|
| 198 |
+
st.success(f"Base URL: {env_config.get('base_url')}")
|
| 199 |
+
else:
|
| 200 |
+
st.info(f"Using default Base URL: {env_config.get('base_url', 'N/A')}")
|
| 201 |
+
|
| 202 |
+
else: # Custom mode
|
| 203 |
+
# Clear parameters for user input
|
| 204 |
+
params['api_key'] = st.text_input(
|
| 205 |
+
f"{selected_provider} API Key",
|
| 206 |
+
value='',
|
| 207 |
+
type="password",
|
| 208 |
+
placeholder=f"Enter {selected_provider} API Key",
|
| 209 |
+
key="api_key"
|
| 210 |
+
)
|
| 211 |
+
params['base_url'] = st.text_input(
|
| 212 |
+
f"{selected_provider} Base URL",
|
| 213 |
+
value='',
|
| 214 |
+
placeholder=env_config.get('base_url', f"Enter {selected_provider} Base URL"),
|
| 215 |
+
key="base_url"
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
# Test button (always show)
|
| 219 |
+
if st.button("🧪 Test Connection", key=f"{selected_provider.lower()}_test"):
|
| 220 |
+
from services.ai_service import test_llm_connection
|
| 221 |
+
test_params = {
|
| 222 |
+
'api_key': params.get('api_key'),
|
| 223 |
+
'base_url': params.get('base_url')
|
| 224 |
+
}
|
| 225 |
+
success, message = test_llm_connection(selected_provider, test_params)
|
| 226 |
+
|
| 227 |
+
# Log the test result
|
| 228 |
+
logger = get_logger()
|
| 229 |
+
logger.log_llm_test(selected_provider, success, None if success else message)
|
| 230 |
+
|
| 231 |
+
if success:
|
| 232 |
+
st.success(message)
|
| 233 |
+
else:
|
| 234 |
+
st.error(message)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def create_advanced_configuration_widget():
|
| 238 |
+
params = st.session_state["params"]
|
| 239 |
+
with st.sidebar.expander("⚙️ Basic config", expanded=False):
|
| 240 |
+
params['max_tokens'] = st.number_input("Max tokens",
|
| 241 |
+
min_value=1024,
|
| 242 |
+
max_value=10240,
|
| 243 |
+
value=4096,
|
| 244 |
+
step=512,)
|
| 245 |
+
params['temperature'] = st.slider("Temperature", 0.0, 1.0, step=0.05, value=1.0)
|
| 246 |
+
|
| 247 |
+
def create_mcp_connection_widget():
|
| 248 |
+
# Auto-connect to MCP servers after parameters are configured
|
| 249 |
+
if not st.session_state.get("auto_connect_attempted", False):
|
| 250 |
+
from services.chat_service import auto_connect_to_mcp
|
| 251 |
+
auto_connect_to_mcp()
|
| 252 |
+
|
| 253 |
+
with st.sidebar:
|
| 254 |
+
st.subheader("Server Management")
|
| 255 |
+
with st.expander(f"MCP Servers ({len(st.session_state.servers)})"):
|
| 256 |
+
for name, config in st.session_state.servers.items():
|
| 257 |
+
with st.container(border=True):
|
| 258 |
+
st.markdown(f"**Server:** {name}")
|
| 259 |
+
st.markdown(f"**URL:** {config['url']}")
|
| 260 |
+
if st.button(f"Remove {name}", key=f"remove_{name}"):
|
| 261 |
+
del st.session_state.servers[name]
|
| 262 |
+
st.rerun()
|
| 263 |
+
|
| 264 |
+
if st.session_state.get("agent"):
|
| 265 |
+
st.success(f"📶 Connected to {len(st.session_state.servers)} MCP servers!"
|
| 266 |
+
f" Found {len(st.session_state.tools)} tools.")
|
| 267 |
+
if st.button("Disconnect to MCP Servers"):
|
| 268 |
+
with st.spinner("Connecting to MCP servers..."):
|
| 269 |
+
try:
|
| 270 |
+
logger = get_logger()
|
| 271 |
+
logger.log_system_status("Disconnecting from MCP servers")
|
| 272 |
+
|
| 273 |
+
reset_connection_state()
|
| 274 |
+
|
| 275 |
+
# Log successful disconnection
|
| 276 |
+
logger.log_system_status("Successfully disconnected from MCP servers")
|
| 277 |
+
|
| 278 |
+
st.rerun()
|
| 279 |
+
except Exception as e:
|
| 280 |
+
# Log disconnection error
|
| 281 |
+
logger.log_error(
|
| 282 |
+
"MCP_Disconnection_Error",
|
| 283 |
+
str(e),
|
| 284 |
+
{'servers': list(st.session_state.servers.keys())}
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
st.error(f"Error disconnecting to MCP servers: {str(e)}")
|
| 288 |
+
st.code(traceback.format_exc(), language="python")
|
| 289 |
+
else:
|
| 290 |
+
st.warning("⚠️ Not connected to MCP server")
|
| 291 |
+
if st.button("Connect to MCP Servers"):
|
| 292 |
+
with st.spinner("Connecting to MCP servers..."):
|
| 293 |
+
try:
|
| 294 |
+
logger = get_logger()
|
| 295 |
+
logger.log_system_status("Attempting to connect to MCP servers")
|
| 296 |
+
|
| 297 |
+
connect_to_mcp_servers()
|
| 298 |
+
|
| 299 |
+
# Log successful connection
|
| 300 |
+
logger.log_system_status("Successfully connected to MCP servers", {
|
| 301 |
+
'servers_count': len(st.session_state.servers),
|
| 302 |
+
'tools_count': len(st.session_state.tools)
|
| 303 |
+
})
|
| 304 |
+
|
| 305 |
+
st.rerun()
|
| 306 |
+
except Exception as e:
|
| 307 |
+
# Log connection error
|
| 308 |
+
logger.log_error(
|
| 309 |
+
"MCP_Connection_Error",
|
| 310 |
+
str(e),
|
| 311 |
+
{'servers': list(st.session_state.servers.keys())}
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
st.error(f"Error connecting to MCP servers: {str(e)}")
|
| 315 |
+
st.code(traceback.format_exc(), language="python")
|
| 316 |
+
|
| 317 |
+
def create_mcp_tools_widget():
|
| 318 |
+
with st.sidebar:
|
| 319 |
+
if st.session_state.tools:
|
| 320 |
+
st.subheader("🧰 Available Tools")
|
| 321 |
+
|
| 322 |
+
selected_tool_name = st.selectbox(
|
| 323 |
+
"Select a Tool",
|
| 324 |
+
options=[tool.name for tool in st.session_state.tools],
|
| 325 |
+
index=0
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
if selected_tool_name:
|
| 329 |
+
selected_tool = next(
|
| 330 |
+
(tool for tool in st.session_state.tools if tool.name == selected_tool_name),
|
| 331 |
+
None
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
if selected_tool:
|
| 335 |
+
with st.container():
|
| 336 |
+
st.write("**Description:**")
|
| 337 |
+
st.write(selected_tool.description)
|
| 338 |
+
|
| 339 |
+
parameters = extract_tool_parameters(selected_tool)
|
| 340 |
+
|
| 341 |
+
if parameters:
|
| 342 |
+
st.write("**Parameters:**")
|
| 343 |
+
for param in parameters:
|
| 344 |
+
st.code(param)
|
client/utils/__init__.py
ADDED
|
File without changes
|
client/utils/ai_prompts.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# All AI prompts
|
| 2 |
+
|
| 3 |
+
def make_system_prompt():
|
| 4 |
+
prompt = f"""
|
| 5 |
+
You are a helpful and analytical assistant specialized in interpreting documents and answering data-related questions.
|
| 6 |
+
|
| 7 |
+
You have access to various MCP (Model Context Protocol) tools, including:
|
| 8 |
+
|
| 9 |
+
**bio_qa_stream_chat Tool**: A sophisticated biomedical Q&A system with evidence-based RAG (Retrieval-Augmented Generation). This tool can:
|
| 10 |
+
- Provide comprehensive, research-backed answers to biological and medical questions
|
| 11 |
+
- Perform intelligent query rewriting to improve search effectiveness
|
| 12 |
+
- Conduct configurable multi-source research (PubMed scientific literature and/or web search)
|
| 13 |
+
- Use advanced reranking to prioritize relevant, authoritative sources
|
| 14 |
+
- Generate evidence-based answers with proper citations and references
|
| 15 |
+
- Provide real-time progress updates during processing (1-3 minutes depending on search configuration)
|
| 16 |
+
- Cover topics including genetics, molecular biology, diseases, treatments, drug mechanisms
|
| 17 |
+
- Deliver answers supported by peer-reviewed scientific papers and authoritative sources
|
| 18 |
+
- Include direct links to source materials and comprehensive evidence summaries
|
| 19 |
+
- Support flexible search configuration to balance comprehensiveness vs. speed
|
| 20 |
+
|
| 21 |
+
**bio_review Tool**: A comprehensive literature review generation tool for biomedical topics. This tool can:
|
| 22 |
+
- Generate detailed 15,000-word literature reviews on biomedical research topics
|
| 23 |
+
- Perform extensive PubMed database searches (50-100+ papers)
|
| 24 |
+
- Conduct web searches for additional context and recent developments
|
| 25 |
+
- Create structured academic reviews with proper sections and citations
|
| 26 |
+
- Provide real-time progress updates during the 30-minute generation process
|
| 27 |
+
- Include abstract, introduction, multiple detailed sections, discussion, and bibliography
|
| 28 |
+
- Ensure academic-grade formatting and comprehensive coverage
|
| 29 |
+
- Validate scientific claims and check references for accuracy
|
| 30 |
+
|
| 31 |
+
**bio_check Tool**: A tool for checking and validating biological and medical information. This tool can:
|
| 32 |
+
- Verify the accuracy of medical claims
|
| 33 |
+
- Check the validity of scientific statements
|
| 34 |
+
- Validate research findings against current knowledge
|
| 35 |
+
- Confirm the reliability of medical information sources
|
| 36 |
+
- Identify potential misinformation or outdated claims
|
| 37 |
+
|
| 38 |
+
**Decision Making Process**:
|
| 39 |
+
When a user asks a question, follow this decision tree:
|
| 40 |
+
|
| 41 |
+
1. **Is it a biological or medical question?**
|
| 42 |
+
- If YES → Use the bio_qa_stream_chat tool
|
| 43 |
+
- If NO → Continue to step 2
|
| 44 |
+
|
| 45 |
+
2. **Does it require a comprehensive literature review?**
|
| 46 |
+
- If YES → Use the bio_review tool
|
| 47 |
+
- If NO → Continue to step 3
|
| 48 |
+
|
| 49 |
+
3. **Does it require information validation?**
|
| 50 |
+
- If YES → Use the bio_check tool
|
| 51 |
+
- If NO → Continue to step 4
|
| 52 |
+
|
| 53 |
+
4. **General questions** → Answer directly without tools
|
| 54 |
+
|
| 55 |
+
**For Biological Questions**:
|
| 56 |
+
- Always use the bio_qa_stream_chat tool for any biology, medicine, genetics, or health-related queries
|
| 57 |
+
- Examples: "What causes Alzheimer's disease?", "How do mRNA vaccines work?", "What are the latest treatments for diabetes?", "Explain CRISPR gene editing"
|
| 58 |
+
- The bio_qa_stream_chat tool will provide evidence-based answers with proper citations and source links
|
| 59 |
+
- Note: This process takes approximately 1-3 minutes depending on search configuration and involves query rewriting, multi-source search, reranking, and evidence synthesis
|
| 60 |
+
|
| 61 |
+
**For Literature Reviews**:
|
| 62 |
+
- Use bio_review tool when users want comprehensive, academic-grade literature reviews
|
| 63 |
+
- Examples: "Generate a literature review on CRISPR gene editing", "Write a review on COVID-19 vaccines",
|
| 64 |
+
"Create a comprehensive review on Alzheimer's disease mechanisms"
|
| 65 |
+
- The bio_review tool will generate 15,000-word reviews with extensive research and proper citations
|
| 66 |
+
- Note: This process takes approximately 30 minutes and involves multiple research phases
|
| 67 |
+
|
| 68 |
+
**For Information Validation**:
|
| 69 |
+
- Use bio_check tool when users want to verify the accuracy of medical or scientific information
|
| 70 |
+
- Examples: "Is this medical claim true?", "Verify this research finding", "Check if this information is accurate"
|
| 71 |
+
|
| 72 |
+
**Your Core Responsibilities**:
|
| 73 |
+
1. **Understand the user's question** – Identify the analytical intent and determine the appropriate tool to use
|
| 74 |
+
2. **Use the right tool** – Select the most appropriate MCP tool based on the question type
|
| 75 |
+
3. **Extract relevant insights** – Get information from the selected tool
|
| 76 |
+
4. **Respond clearly and step-by-step** – Give a structured, thoughtful reply that walks the user through your reasoning
|
| 77 |
+
|
| 78 |
+
Always prioritize using the appropriate tool for the question type, especially bio_qa_stream_chat for biological questions, bio_review for comprehensive literature reviews, and bio_check for information validation.
|
| 79 |
+
"""
|
| 80 |
+
return prompt
|
| 81 |
+
|
| 82 |
+
def make_main_prompt(user_text):
|
| 83 |
+
prompt = f"""
|
| 84 |
+
Below is the relevant context for the user's current data-related question.
|
| 85 |
+
Use this information to generate a helpful, concise, and insight-driven response.
|
| 86 |
+
"""
|
| 87 |
+
# Always add the user query
|
| 88 |
+
prompt += f"""
|
| 89 |
+
---
|
| 90 |
+
### 🧠 User's Query:
|
| 91 |
+
{user_text}
|
| 92 |
+
"""
|
| 93 |
+
return prompt
|
client/utils/async_helpers.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
|
| 3 |
+
# Helper function for running async functions
|
| 4 |
+
def run_async(coro):
|
| 5 |
+
"""Run an async function within the stored event loop."""
|
| 6 |
+
return st.session_state.loop.run_until_complete(coro)
|
| 7 |
+
|
| 8 |
+
def reset_connection_state():
|
| 9 |
+
"""Reset all connection-related session state variables."""
|
| 10 |
+
if st.session_state.client is not None:
|
| 11 |
+
try:
|
| 12 |
+
# Close the existing client properly
|
| 13 |
+
run_async(st.session_state.client.__aexit__(None, None, None))
|
| 14 |
+
except Exception as e:
|
| 15 |
+
st.error(f"Error closing previous client: {str(e)}")
|
| 16 |
+
|
| 17 |
+
st.session_state.client = None
|
| 18 |
+
st.session_state.agent = None
|
| 19 |
+
st.session_state.tools = []
|
| 20 |
+
|
| 21 |
+
def on_shutdown():
|
| 22 |
+
# Proper cleanup when the session ends
|
| 23 |
+
if st.session_state.client is not None:
|
| 24 |
+
try:
|
| 25 |
+
# Close the client properly
|
| 26 |
+
run_async(st.session_state.client.__aexit__(None, None, None))
|
| 27 |
+
except Exception as e:
|
| 28 |
+
st.error(f"Error during shutdown: {str(e)}")
|
client/utils/tool_schema_parser.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def extract_tool_parameters(tool):
|
| 2 |
+
parameters = []
|
| 3 |
+
|
| 4 |
+
if not hasattr(tool, 'args_schema'):
|
| 5 |
+
return parameters
|
| 6 |
+
|
| 7 |
+
schema = tool.args_schema
|
| 8 |
+
if isinstance(schema, dict):
|
| 9 |
+
schema_dict = schema
|
| 10 |
+
else:
|
| 11 |
+
schema_dict = schema.schema()
|
| 12 |
+
|
| 13 |
+
properties = schema_dict.get('properties', {})
|
| 14 |
+
required = schema_dict.get('required', [])
|
| 15 |
+
|
| 16 |
+
for name, info in properties.items():
|
| 17 |
+
param_type = info.get('type', 'string')
|
| 18 |
+
title = info.get('title', name)
|
| 19 |
+
default = info.get('default', None)
|
| 20 |
+
is_required = name in required
|
| 21 |
+
|
| 22 |
+
desc = f"{title} ({param_type})"
|
| 23 |
+
desc += " - required" if is_required else " - optional"
|
| 24 |
+
if default is not None:
|
| 25 |
+
desc += f" [default: {default}]"
|
| 26 |
+
|
| 27 |
+
parameters.append(desc)
|
| 28 |
+
|
| 29 |
+
return parameters
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
# 客户端服务 - Python 3.11
|
| 5 |
+
client:
|
| 6 |
+
build:
|
| 7 |
+
context: .
|
| 8 |
+
dockerfile: Dockerfile.client
|
| 9 |
+
ports:
|
| 10 |
+
- "8501:8501"
|
| 11 |
+
environment:
|
| 12 |
+
- PORT=8501
|
| 13 |
+
depends_on:
|
| 14 |
+
- retrieve
|
| 15 |
+
- review
|
| 16 |
+
networks:
|
| 17 |
+
- mcp-network
|
| 18 |
+
|
| 19 |
+
# Retrieve服务 - Python 3.11 + 特殊依赖
|
| 20 |
+
retrieve:
|
| 21 |
+
build:
|
| 22 |
+
context: .
|
| 23 |
+
dockerfile: Dockerfile.retrieve
|
| 24 |
+
ports:
|
| 25 |
+
- "9487:9487"
|
| 26 |
+
networks:
|
| 27 |
+
- mcp-network
|
| 28 |
+
environment:
|
| 29 |
+
- PYTHONPATH=/app
|
| 30 |
+
|
| 31 |
+
# Review服务 - Python 3.11
|
| 32 |
+
review:
|
| 33 |
+
build:
|
| 34 |
+
context: .
|
| 35 |
+
dockerfile: Dockerfile.review
|
| 36 |
+
ports:
|
| 37 |
+
- "8880:8880"
|
| 38 |
+
networks:
|
| 39 |
+
- mcp-network
|
| 40 |
+
environment:
|
| 41 |
+
- PYTHONPATH=/app
|
| 42 |
+
|
| 43 |
+
networks:
|
| 44 |
+
mcp-network:
|
| 45 |
+
driver: bridge
|
servers/Retrieve/.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
logs/*
|
| 2 |
+
*.pyc
|
| 3 |
+
py_milvus_test.py
|
| 4 |
+
test_vector_search.py
|
| 5 |
+
.vscode/settings.json
|
| 6 |
+
service/Qwen3-Reranker-0.6B
|
| 7 |
+
test_model_api.py
|
| 8 |
+
test/logs
|
| 9 |
+
.conda/*
|