jackkuo commited on
Commit
82bf89e
·
0 Parent(s):

reinit repo

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. .gitignore +87 -0
  3. Dockerfile +90 -0
  4. README.md +119 -0
  5. assets/extract1.png +3 -0
  6. assets/extract2.png +3 -0
  7. assets/main_pic.png +3 -0
  8. assets/main_pic_review.png +3 -0
  9. assets/mcp_playground.png +3 -0
  10. assets/sidebar_available_tools.png +3 -0
  11. assets/sidebar_available_tools_list.png +3 -0
  12. assets/sidebar_providers.png +3 -0
  13. assets/sidebar_server_management.png +3 -0
  14. assets/tool_execution_history.png +3 -0
  15. client/.env-example.txt +2 -0
  16. client/.streamlit/config.toml +22 -0
  17. client/.streamlit/style.css +139 -0
  18. client/Dockerfile +32 -0
  19. client/README.md +80 -0
  20. client/app.py +50 -0
  21. client/apps/__init__.py +0 -0
  22. client/apps/mcp_playground.py +1353 -0
  23. client/chat_history/chat_Bio_QA_mcp_agent_20250908_122027.json +37 -0
  24. client/chat_history/chat_Review_mcp_agent_20250908_121128.json +0 -0
  25. client/config.py +59 -0
  26. client/docs/import_export_guide.md +65 -0
  27. client/docs/logging_config.md +78 -0
  28. client/icons/playground.png +3 -0
  29. client/requirements.txt +23 -0
  30. client/run.sh +13 -0
  31. client/servers_config.json +18 -0
  32. client/services/__init__.py +0 -0
  33. client/services/ai_service.py +149 -0
  34. client/services/chat_service.py +207 -0
  35. client/services/export_service.py +166 -0
  36. client/services/import_service.py +92 -0
  37. client/services/logging_service.py +212 -0
  38. client/services/mcp_service.py +104 -0
  39. client/services/task_monitor.py +160 -0
  40. client/static/.gitkeep +1 -0
  41. client/ui_components/__init__.py +0 -0
  42. client/ui_components/log_viewer.py +183 -0
  43. client/ui_components/main_components.py +13 -0
  44. client/ui_components/sidebar_components.py +344 -0
  45. client/utils/__init__.py +0 -0
  46. client/utils/ai_prompts.py +93 -0
  47. client/utils/async_helpers.py +28 -0
  48. client/utils/tool_schema_parser.py +29 -0
  49. docker-compose.yml +45 -0
  50. 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
+ ![](assets/main_pic.png)
20
+
21
+ ![](assets/main_pic_review.png)
22
+
23
+ ![](assets/extract1.png)
24
+ ![](assets/extract2.png)
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

  • SHA256: a51a71ae824cf962ba2ba5d7b86c2465fdd827243159f939e6711beaf5a6ed1f
  • Pointer size: 131 Bytes
  • Size of remote file: 562 kB
assets/extract2.png ADDED

Git LFS Details

  • SHA256: f0a892813d5d38a9e1a1665bb5e79cab4df4118bc1a4848a5b97bbfe7e68b077
  • Pointer size: 131 Bytes
  • Size of remote file: 601 kB
assets/main_pic.png ADDED

Git LFS Details

  • SHA256: bea0a17c1dde0e94d47386b79c7700dde94350cd99272a018d9fd952758f9d97
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
assets/main_pic_review.png ADDED

Git LFS Details

  • SHA256: 703f1946b28632ab5c032163c6402ec1a4583d0e9afa2ff516d82e34b210dee1
  • Pointer size: 131 Bytes
  • Size of remote file: 775 kB
assets/mcp_playground.png ADDED

Git LFS Details

  • SHA256: fc12111efd623a9dd7eb1b7232738e262a640cb8f796ab50fa773d105a3d6f8e
  • Pointer size: 131 Bytes
  • Size of remote file: 412 kB
assets/sidebar_available_tools.png ADDED

Git LFS Details

  • SHA256: d73e510a6925e03f3d7819501ef970272ecf4f6968afee6db8334dfb8588bead
  • Pointer size: 130 Bytes
  • Size of remote file: 24.6 kB
assets/sidebar_available_tools_list.png ADDED

Git LFS Details

  • SHA256: b93c028bd796026ff3f74952013b6737c77d29e26351bb0861940ac14ef6b10c
  • Pointer size: 130 Bytes
  • Size of remote file: 18.8 kB
assets/sidebar_providers.png ADDED

Git LFS Details

  • SHA256: 659f4ef5b0b8dd44c6b33782a90f9cdeb356fb9f8af3fade7f4c856a26fc1327
  • Pointer size: 130 Bytes
  • Size of remote file: 11.8 kB
assets/sidebar_server_management.png ADDED

Git LFS Details

  • SHA256: 3288987bcad09152394deddb2f1071a9d7653ab7502827f8ab5b9e1d30ac7fae
  • Pointer size: 130 Bytes
  • Size of remote file: 36.3 kB
assets/tool_execution_history.png ADDED

Git LFS Details

  • SHA256: fce40903b0bdf37b29547bf73249135741212cde66139f76fa01c1bc71b9ebe0
  • Pointer size: 130 Bytes
  • Size of remote file: 42 kB
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

  • SHA256: c548d88e96b36cb572ef23c310030bb46b207763bcd2e52a308ccdba956e43da
  • Pointer size: 130 Bytes
  • Size of remote file: 17.2 kB
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/*