Spaces:
Running
Running
Commit
Β·
6ac5190
1
Parent(s):
2fd5de9
First agent
Browse files- Dockerfile +2 -2
- README.md +1 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/__pycache__/test.cpython-312.pyc +0 -0
- app/import asyncio.py +187 -0
- app/main.py +283 -112
- app/templates/Llama8bq4k.html +73 -0
- app/templates/Qwen5bq2k.html +76 -0
- app/templates/default.html +9 -0
- app/templates/index.html +25 -26
- app/test.py +314 -0
- entrypoint.py +1 -25
Dockerfile
CHANGED
@@ -21,7 +21,7 @@ COPY --chown=user . /app
|
|
21 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
22 |
|
23 |
# Expose the port
|
24 |
-
EXPOSE
|
25 |
|
26 |
# Command to start the server
|
27 |
-
CMD ["uvicorn", main:app", "--host", "0.0.0.0", "--port", "
|
|
|
21 |
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
22 |
|
23 |
# Expose the port
|
24 |
+
EXPOSE 8000
|
25 |
|
26 |
# Command to start the server
|
27 |
+
CMD ["uvicorn", app/main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
@@ -4,6 +4,7 @@ emoji: π»
|
|
4 |
colorFrom: pink
|
5 |
colorTo: purple
|
6 |
sdk: docker
|
|
|
7 |
pinned: false
|
8 |
---
|
9 |
|
|
|
4 |
colorFrom: pink
|
5 |
colorTo: purple
|
6 |
sdk: docker
|
7 |
+
port: 8000
|
8 |
pinned: false
|
9 |
---
|
10 |
|
app/__pycache__/main.cpython-312.pyc
CHANGED
Binary files a/app/__pycache__/main.cpython-312.pyc and b/app/__pycache__/main.cpython-312.pyc differ
|
|
app/__pycache__/test.cpython-312.pyc
ADDED
Binary file (15 kB). View file
|
|
app/import asyncio.py
ADDED
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
import subprocess
|
3 |
+
import json
|
4 |
+
from fastapi import FastAPI, WebSocket
|
5 |
+
from fastapi.responses import HTMLResponse
|
6 |
+
from jinja2 import Template
|
7 |
+
from llama_cpp import Llama
|
8 |
+
import logging
|
9 |
+
|
10 |
+
# Set up logging
|
11 |
+
logging.basicConfig(level=logging.INFO)
|
12 |
+
|
13 |
+
# Initialize the FastAPI application
|
14 |
+
app = FastAPI()
|
15 |
+
|
16 |
+
# Define the models and their paths
|
17 |
+
models = {
|
18 |
+
"production": {"file": "DeepSeek-R1-Distill-Llama-8B-Q4_K_L.gguf", "alias": "R1Llama8BQ4L"},
|
19 |
+
"development": {"file": "/home/ali/Projects/VirtualLabDev/Local/DeepSeek-R1-Distill-Qwen-1.5B-Q2_K.gguf", "alias": "R1Qwen1.5BQ2"},
|
20 |
+
}
|
21 |
+
|
22 |
+
# Load the Llama model
|
23 |
+
llm = Llama(model_path=models["development"]["file"], n_ctx=2048)
|
24 |
+
|
25 |
+
# Define the shell execution tool
|
26 |
+
def execute_shell(arguments):
|
27 |
+
"""Execute a shell command."""
|
28 |
+
try:
|
29 |
+
args = json.loads(arguments)
|
30 |
+
command = args.get("command", "")
|
31 |
+
if not command:
|
32 |
+
return json.dumps({"error": "No command provided."})
|
33 |
+
|
34 |
+
process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
35 |
+
return json.dumps({"stdout": process.stdout, "stderr": process.stderr})
|
36 |
+
except Exception as e:
|
37 |
+
return json.dumps({"error": str(e)})
|
38 |
+
|
39 |
+
# Define the tools available to the assistant
|
40 |
+
tools = {
|
41 |
+
"shell": {
|
42 |
+
"description": "Execute shell commands.",
|
43 |
+
"example_input": '{"command": "ls -l"}',
|
44 |
+
"example_output": '{"stdout": "...", "stderr": "..."}',
|
45 |
+
"function": execute_shell,
|
46 |
+
},
|
47 |
+
}
|
48 |
+
|
49 |
+
# Generate the dynamic system prompt
|
50 |
+
def generate_system_prompt(tools):
|
51 |
+
"""
|
52 |
+
Dynamically generate the system prompt based on available tools.
|
53 |
+
"""
|
54 |
+
tool_descriptions = []
|
55 |
+
for tool_name, tool_data in tools.items():
|
56 |
+
description = tool_data.get("description", "No description available.")
|
57 |
+
example_input = tool_data.get("example_input", "{}")
|
58 |
+
example_output = tool_data.get("example_output", "{}")
|
59 |
+
tool_descriptions.append(
|
60 |
+
f"""- **{tool_name}**:
|
61 |
+
- Description: {description}
|
62 |
+
- Input: {example_input}
|
63 |
+
- Output: {example_output}"""
|
64 |
+
)
|
65 |
+
return """You are an autonomous computational biology researcher with access to the following tools:\n\n""" + "\n\n".join(tool_descriptions)
|
66 |
+
|
67 |
+
# Create the system prompt
|
68 |
+
system_prompt = generate_system_prompt(tools)
|
69 |
+
|
70 |
+
# Tool output handler
|
71 |
+
def extract_tool_calls(response_text):
|
72 |
+
"""Parse tool calls from model output."""
|
73 |
+
if "<ο½toolβcallsβbeginο½>" not in response_text:
|
74 |
+
return []
|
75 |
+
|
76 |
+
tool_calls_part = response_text.split("<ο½toolβcallsβbeginο½>")[1]
|
77 |
+
tool_calls_part = tool_calls_part.split("<ο½toolβcallsβendο½>")[0]
|
78 |
+
tool_calls = tool_calls_part.split("<ο½toolβcallβbeginο½>")
|
79 |
+
|
80 |
+
parsed_tool_calls = []
|
81 |
+
for tool_call in tool_calls:
|
82 |
+
tool_call = tool_call.strip()
|
83 |
+
if tool_call:
|
84 |
+
try:
|
85 |
+
tool_type, tool_name_and_args = tool_call.split("<ο½toolβsepο½>")
|
86 |
+
tool_name, tool_args = tool_name_and_args.split("\n```json\n", 1)
|
87 |
+
tool_args = tool_args.split("\n```")[0]
|
88 |
+
parsed_tool_calls.append({"type": tool_type, "name": tool_name.strip(), "arguments": tool_args.strip()})
|
89 |
+
except ValueError:
|
90 |
+
logging.warning("Failed to parse tool call: %s", tool_call)
|
91 |
+
return parsed_tool_calls
|
92 |
+
|
93 |
+
def process_tool_call(tool_call):
|
94 |
+
"""Execute the requested tool and return its output."""
|
95 |
+
tool_name = tool_call["name"]
|
96 |
+
tool_args = tool_call["arguments"]
|
97 |
+
|
98 |
+
if tool_name in tools:
|
99 |
+
tool_function = tools[tool_name]["function"]
|
100 |
+
return tool_function(tool_args)
|
101 |
+
else:
|
102 |
+
return json.dumps({"error": f"Tool {tool_name} not found."})
|
103 |
+
|
104 |
+
# Chat template for generating prompts
|
105 |
+
CHAT_TEMPLATE = """
|
106 |
+
{% for message in messages %}
|
107 |
+
{% if message.role == "system" -%}
|
108 |
+
{{ message.content }}
|
109 |
+
{% elif message.role == "assistant" -%}
|
110 |
+
<ο½Assistantο½>{{ message.content }}
|
111 |
+
{% elif message.role == "tool" -%}
|
112 |
+
<ο½Toolο½>{{ message.content }}
|
113 |
+
{% endif %}
|
114 |
+
{% endfor %}
|
115 |
+
"""
|
116 |
+
|
117 |
+
# Response handler for generating prompts and parsing results
|
118 |
+
async def generate_response(conversation):
|
119 |
+
"""Generate a model response asynchronously."""
|
120 |
+
template = Template(CHAT_TEMPLATE)
|
121 |
+
prompt = template.render(messages=conversation, bos_token="")
|
122 |
+
|
123 |
+
logging.info(f"Prompt: {prompt}")
|
124 |
+
|
125 |
+
for token in llm(prompt, stream=True):
|
126 |
+
yield token["choices"][0]["text"] # Regular generator
|
127 |
+
|
128 |
+
await asyncio.sleep(0) # Allows async execution
|
129 |
+
|
130 |
+
# WebSocket for streaming autonomous research interactions
|
131 |
+
@app.websocket("/stream")
|
132 |
+
async def stream(websocket: WebSocket):
|
133 |
+
"""WebSocket handler to stream AI research process."""
|
134 |
+
logging.info("WebSocket connection established.")
|
135 |
+
await websocket.accept()
|
136 |
+
await websocket.send_text("π Autonomous computational biology research initiated!")
|
137 |
+
|
138 |
+
# Initialize the conversation
|
139 |
+
conversation = [{"role": "system", "content": system_prompt}]
|
140 |
+
|
141 |
+
while True:
|
142 |
+
try:
|
143 |
+
# Stream AI thought process
|
144 |
+
async for response_text in generate_response(conversation):
|
145 |
+
logging.info(f"Response: {response_text}")
|
146 |
+
await websocket.send_text(f"π§ Model Thinking: {response_text}")
|
147 |
+
|
148 |
+
# Check for tool calls in response
|
149 |
+
tool_calls = extract_tool_calls(response_text)
|
150 |
+
logging.info(f"Tool calls: {tool_calls}")
|
151 |
+
if tool_calls:
|
152 |
+
for tool_call in tool_calls:
|
153 |
+
# Process each tool call
|
154 |
+
tool_output = process_tool_call(tool_call)
|
155 |
+
await websocket.send_text(f"π§ Tool Execution: {tool_output}")
|
156 |
+
|
157 |
+
# Add the tool's output to the conversation
|
158 |
+
conversation.append({"role": "tool", "content": tool_output})
|
159 |
+
|
160 |
+
except Exception as e:
|
161 |
+
logging.error(f"Error occurred: {str(e)}")
|
162 |
+
break
|
163 |
+
|
164 |
+
await websocket.close()
|
165 |
+
|
166 |
+
# Serve the frontend
|
167 |
+
@app.get("/", response_class=HTMLResponse)
|
168 |
+
async def get():
|
169 |
+
"""Serve the frontend application."""
|
170 |
+
html_content = """
|
171 |
+
<!DOCTYPE html>
|
172 |
+
<html>
|
173 |
+
<head>
|
174 |
+
<title>Autonomous Computational Biology Research</title>
|
175 |
+
</head>
|
176 |
+
<body>
|
177 |
+
<h1>AI Agent for Computational Biology Research</h1>
|
178 |
+
<div id="log" style="white-space: pre-line; font-family: monospace;"></div>
|
179 |
+
<script>
|
180 |
+
const ws = new WebSocket("ws://localhost:8000/stream");
|
181 |
+
const log = document.getElementById("log");
|
182 |
+
ws.onmessage = (event) => { log.textContent += event.data + "\\n"; };
|
183 |
+
</script>
|
184 |
+
</body>
|
185 |
+
</html>
|
186 |
+
"""
|
187 |
+
return HTMLResponse(html_content)
|
app/main.py
CHANGED
@@ -1,143 +1,314 @@
|
|
|
|
1 |
import asyncio
|
2 |
import subprocess
|
3 |
import json
|
|
|
4 |
from fastapi import FastAPI, WebSocket
|
5 |
from fastapi.responses import HTMLResponse
|
6 |
from jinja2 import Template
|
7 |
from llama_cpp import Llama
|
|
|
8 |
import logging
|
|
|
9 |
|
10 |
-
# Set up logging
|
11 |
logging.basicConfig(level=logging.INFO)
|
12 |
|
13 |
-
#
|
14 |
-
|
|
|
|
|
|
|
|
|
15 |
|
16 |
# Define the models and their paths
|
17 |
-
models = {
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
{% set add_generation_prompt = false %}
|
30 |
-
{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}
|
31 |
-
{%- for message in messages -%}
|
32 |
-
{%- if message['role'] == 'system' -%}
|
33 |
-
{% set ns.system_prompt = message['content'] %}
|
34 |
-
{%- endif -%}
|
35 |
-
{%- endfor %}
|
36 |
-
{{bos_token}}{{ns.system_prompt}}
|
37 |
-
{%- for message in messages -%}
|
38 |
-
{%- if message['role'] == 'user' %}
|
39 |
-
{{ '<ο½Userο½>' + message['content'] }}
|
40 |
-
{%- elif message['role'] == 'assistant' and message['content'] is none %}
|
41 |
-
{%- for tool in message['tool_calls'] %}
|
42 |
-
{{ '<ο½Assistantο½><ο½toolβcallsβbeginο½><ο½toolβcallβbeginο½>' + tool['type'] + '<ο½toolβsepο½>' + tool['function']['name'] + '\n```json\n' + tool['function']['arguments'] + '\n```\n<ο½toolβcallβendο½>' }}
|
43 |
-
{%- endfor %}
|
44 |
-
{%- elif message['role'] == 'assistant' %}
|
45 |
-
{{ '<ο½Assistantο½>' + message['content'] + '<ο½endβofβsentenceο½>' }}
|
46 |
-
{%- elif message['role'] == 'tool' %}
|
47 |
-
{{ '<ο½toolβoutputsβbeginο½><ο½toolβoutputβbeginο½>' + message['content'] + '<ο½toolβoutputβendο½>' }}
|
48 |
-
{%- endif %}
|
49 |
-
{%- endfor -%}
|
50 |
-
"""
|
51 |
-
|
52 |
-
# Helper function for generating tool use responses
|
53 |
-
def generate_response(messages):
|
54 |
-
"""Generate a model response using the provided chat template and messages."""
|
55 |
-
template = Template(CHAT_TEMPLATE)
|
56 |
-
prompt = template.render(messages=messages, bos_token="")
|
57 |
-
|
58 |
-
# Feed to llama_cpp
|
59 |
-
output = llm(prompt)
|
60 |
-
return output["choices"][0]["text"].strip() # Extract the assistant's response
|
61 |
|
|
|
62 |
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
await websocket.send_text(f"π€ User: {user_message}")
|
78 |
|
79 |
-
|
80 |
-
|
|
|
|
|
|
|
81 |
|
82 |
-
|
83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
|
|
|
|
|
|
|
|
90 |
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
93 |
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
97 |
try:
|
98 |
-
|
99 |
-
|
100 |
-
command,
|
101 |
-
shell=True,
|
102 |
-
stdout=subprocess.PIPE,
|
103 |
-
stderr=subprocess.PIPE,
|
104 |
-
text=True
|
105 |
-
)
|
106 |
-
|
107 |
-
# Stream the shell outputs
|
108 |
-
while True:
|
109 |
-
output = process.stdout.readline()
|
110 |
-
if output:
|
111 |
-
await websocket.send_text(f"π€ Shell Output: {output.strip()}")
|
112 |
-
elif process.poll() is not None:
|
113 |
-
break
|
114 |
-
|
115 |
-
# Capture errors if any
|
116 |
-
error = process.stderr.read()
|
117 |
-
if error:
|
118 |
-
await websocket.send_text(f"β Shell Error: {error.strip()}")
|
119 |
-
else:
|
120 |
-
await websocket.send_text("β
Command executed successfully.")
|
121 |
except Exception as e:
|
122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
-
|
125 |
-
|
126 |
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
except Exception as e:
|
132 |
-
|
133 |
-
break
|
134 |
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
|
|
|
137 |
@app.get("/", response_class=HTMLResponse)
|
138 |
async def get():
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
import asyncio
|
3 |
import subprocess
|
4 |
import json
|
5 |
+
import concurrent.futures
|
6 |
from fastapi import FastAPI, WebSocket
|
7 |
from fastapi.responses import HTMLResponse
|
8 |
from jinja2 import Template
|
9 |
from llama_cpp import Llama
|
10 |
+
from contextlib import asynccontextmanager
|
11 |
import logging
|
12 |
+
from pathlib import Path
|
13 |
|
14 |
+
# Set up logging
|
15 |
logging.basicConfig(level=logging.INFO)
|
16 |
|
17 |
+
# Set up the log file and ensure it exists
|
18 |
+
log_path = Path("interaction_history.log")
|
19 |
+
log_path.touch(exist_ok=True)
|
20 |
+
|
21 |
+
# Global variable to keep track of the last read position in the log file
|
22 |
+
last_read_position = 0
|
23 |
|
24 |
# Define the models and their paths
|
25 |
+
models = {
|
26 |
+
"production": {
|
27 |
+
"file": "DeepSeek-R1-Distill-Llama-8B-Q4_K_L.gguf",
|
28 |
+
"alias": "R1Llama8BQ4L",
|
29 |
+
"template": "/templates/Llama8bq4k.html"
|
30 |
+
},
|
31 |
+
"development": {
|
32 |
+
"file": "/home/ali/Projects/VirtualLabDev/Local/DeepSeek-R1-Distill-Qwen-1.5B-Q2_K.gguf",
|
33 |
+
"alias": "R1Qwen1.5BQ2",
|
34 |
+
"template": "./templates/Qwen5bq2k.html"
|
35 |
+
},
|
36 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
37 |
|
38 |
+
model_in_use = models["development"]
|
39 |
|
40 |
+
with open(model_in_use["template"], "r") as jinja_template:
|
41 |
+
CHAT_TEMPLATE = jinja_template.read()
|
42 |
+
|
43 |
+
with open("templates/default.html", "r") as jinja_template:
|
44 |
+
CHAT_TEMPLATE = jinja_template.read()
|
45 |
+
|
46 |
+
# Define the shell execution tool
|
47 |
+
def execute_shell(arguments):
|
48 |
+
"""Execute a shell command."""
|
49 |
+
try:
|
50 |
+
args = json.loads(arguments)
|
51 |
+
command = args.get("command", "")
|
52 |
+
if not command:
|
53 |
+
return json.dumps({"error": "No command provided."})
|
54 |
+
process = subprocess.run(
|
55 |
+
command,
|
56 |
+
shell=True,
|
57 |
+
stdout=subprocess.PIPE,
|
58 |
+
stderr=subprocess.PIPE,
|
59 |
+
text=True
|
60 |
+
)
|
61 |
+
return json.dumps({"stdout": process.stdout, "stderr": process.stderr})
|
62 |
+
except Exception as e:
|
63 |
+
return json.dumps({"error": str(e)})
|
64 |
+
|
65 |
+
# Define the tools available to the assistant
|
66 |
+
tools = {
|
67 |
+
"shell": {
|
68 |
+
"description": "Execute shell commands.",
|
69 |
+
"example_input": '{"command": "ls -l"}',
|
70 |
+
"example_output": '{"stdout": "...", "stderr": "..."}',
|
71 |
+
"function": execute_shell,
|
72 |
+
},
|
73 |
+
}
|
74 |
+
|
75 |
+
# Dynamically generate the system prompt based on available tools.
|
76 |
+
def generate_system_prompt(tools):
|
77 |
+
tool_descriptions = []
|
78 |
+
for tool_name, tool_data in tools.items():
|
79 |
+
description = tool_data.get("description", "No description available.")
|
80 |
+
example_input = tool_data.get("example_input", "{}")
|
81 |
+
example_output = tool_data.get("example_output", "{}")
|
82 |
+
tool_descriptions.append(
|
83 |
+
f"""- **{tool_name}**:
|
84 |
+
- Description: {description}
|
85 |
+
- Input: {example_input}
|
86 |
+
- Output: {example_output}"""
|
87 |
+
)
|
88 |
+
return (
|
89 |
+
"You are an autonomous computational biology researcher with access to the following tools:\n\n"
|
90 |
+
+ "\n\n".join(tool_descriptions)
|
91 |
+
)
|
92 |
+
|
93 |
+
# Create the system prompt.
|
94 |
+
system_prompt = generate_system_prompt(tools)
|
95 |
+
|
96 |
+
# Parse out any tool calls embedded in the model's output.
|
97 |
+
def extract_tool_calls(response_text):
|
98 |
+
"""
|
99 |
+
Parse tool calls from model output.
|
100 |
+
|
101 |
+
The model is expected to demarcate tool calls between markers like:
|
102 |
+
<ο½toolβcallsβbeginο½> ... <ο½toolβcallsβendο½>
|
103 |
+
and each individual call between:
|
104 |
+
<ο½toolβcallβbeginο½> ... <ο½toolβsepο½> ... "```json" ... "```"
|
105 |
+
"""
|
106 |
+
if "<ο½toolβcallsβbeginο½>" not in response_text:
|
107 |
+
return []
|
108 |
+
|
109 |
+
tool_calls_part = response_text.split("<ο½toolβcallsβbeginο½>")[1]
|
110 |
+
tool_calls_part = tool_calls_part.split("<ο½toolβcallsβendο½>")[0]
|
111 |
+
tool_calls = tool_calls_part.split("<ο½toolβcallβbeginο½>")
|
112 |
|
113 |
+
parsed_tool_calls = []
|
114 |
+
for tool_call in tool_calls:
|
115 |
+
tool_call = tool_call.strip()
|
116 |
+
if tool_call:
|
117 |
+
try:
|
118 |
+
tool_type, tool_name_and_args = tool_call.split("<ο½toolβsepο½>")
|
119 |
+
tool_name, tool_args = tool_name_and_args.split("\n```json\n", 1)
|
120 |
+
tool_args = tool_args.split("\n```")[0]
|
121 |
+
parsed_tool_calls.append({
|
122 |
+
"type": tool_type,
|
123 |
+
"name": tool_name.strip(),
|
124 |
+
"arguments": tool_args.strip()
|
125 |
+
})
|
126 |
+
except ValueError:
|
127 |
+
logging.warning("Failed to parse tool call: %s", tool_call)
|
128 |
+
return parsed_tool_calls
|
129 |
|
130 |
+
def process_tool_call(tool_call):
|
131 |
+
"""Execute the requested tool and return its output."""
|
132 |
+
tool_name = tool_call["name"]
|
133 |
+
tool_args = tool_call["arguments"]
|
|
|
134 |
|
135 |
+
if tool_name in tools:
|
136 |
+
tool_function = tools[tool_name]["function"]
|
137 |
+
return tool_function(tool_args)
|
138 |
+
else:
|
139 |
+
return json.dumps({"error": f"Tool {tool_name} not found."})
|
140 |
|
141 |
+
#
|
142 |
+
# Helper: Wrap a synchronous generator as an asynchronous generator.
|
143 |
+
#
|
144 |
+
async def async_generator_from_sync(sync_gen_func, *args, **kwargs):
|
145 |
+
"""
|
146 |
+
Runs a synchronous generator function in a thread and yields items asynchronously.
|
147 |
+
"""
|
148 |
+
loop = asyncio.get_running_loop()
|
149 |
+
q = asyncio.Queue()
|
150 |
|
151 |
+
def producer():
|
152 |
+
try:
|
153 |
+
for item in sync_gen_func(*args, **kwargs):
|
154 |
+
loop.call_soon_threadsafe(q.put_nowait, item)
|
155 |
+
except Exception as e:
|
156 |
+
loop.call_soon_threadsafe(q.put_nowait, e)
|
157 |
+
finally:
|
158 |
+
# Signal the end of iteration with a sentinel (None)
|
159 |
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
160 |
|
161 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
162 |
+
executor.submit(producer)
|
163 |
+
while True:
|
164 |
+
item = await q.get()
|
165 |
+
if item is None:
|
166 |
+
break
|
167 |
+
if isinstance(item, Exception):
|
168 |
+
raise item
|
169 |
+
yield item
|
170 |
|
171 |
+
#
|
172 |
+
# Background response generator without requiring a WebSocket.
|
173 |
+
#
|
174 |
+
async def generate_response_background(conversation):
|
175 |
+
"""Generate a model response asynchronously."""
|
176 |
+
#template = Template(CHAT_TEMPLATE)
|
177 |
+
#prompt = template.render(messages=conversation)
|
178 |
+
#logging.info(f"Prompt: {prompt}")
|
179 |
+
async for token_chunk in async_generator_from_sync(
|
180 |
+
llm.create_chat_completion,
|
181 |
+
messages=conversation,
|
182 |
+
stream=True,
|
183 |
+
max_tokens=2048
|
184 |
+
):
|
185 |
+
# Extract token from OpenAI-compatible format
|
186 |
+
token = token_chunk["choices"][0]["delta"].get("content", "")
|
187 |
+
yield token_chunk # Yield the token string directly
|
188 |
+
await asyncio.sleep(0)
|
189 |
+
|
190 |
+
#
|
191 |
+
# Main research loop running continuously in the background.
|
192 |
+
#
|
193 |
+
async def run_research_forever():
|
194 |
+
global log_path
|
195 |
+
logging.info("π Autonomous computational biology research initiated!")
|
196 |
+
with log_path.open("a") as f:
|
197 |
+
f.write("π Autonomous computational biology research initiated!\n")
|
198 |
+
|
199 |
+
conversation = [{"role": "system", "content": system_prompt}]
|
200 |
+
while True:
|
201 |
+
full_response = ""
|
202 |
+
try:
|
203 |
+
# Generate the model response and accumulate the full text.
|
204 |
+
async for token in generate_response_background(conversation):
|
205 |
+
token_text = token["choices"][0]["delta"].get("content", "")
|
206 |
+
full_response += token_text
|
207 |
+
# Log each token individually
|
208 |
+
with open(log_path, "a") as f:
|
209 |
+
f.write(token_text)
|
210 |
+
f.flush()
|
211 |
+
# Optionally, check if a finish reason is provided
|
212 |
+
if token['choices'][0].get("finish_reason", "") is not None:
|
213 |
+
|
214 |
+
# The presence of a finish reason (like "stop") indicates that generation is complete.
|
215 |
+
# Append the assistant's response to the conversation log.
|
216 |
+
conversation.append({"role": "assistant", "content": full_response})
|
217 |
try:
|
218 |
+
tool_output = parse_tool_calls(full_response)
|
219 |
+
conversation.append({"role": "tool", "content": tool_output})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
except Exception as e:
|
221 |
+
logging.error(f"π οΈ Tool execution failed: {e}")
|
222 |
+
continue
|
223 |
+
except Exception as e:
|
224 |
+
logging.error(f"Autonomous research error during response generation: {e}")
|
225 |
+
continue
|
226 |
+
|
227 |
+
# Delay before the next query iteration.
|
228 |
+
await asyncio.sleep(1)
|
229 |
+
|
230 |
+
def parse_tool_calls(full_response):
|
231 |
+
# Check for tool calls in the response and process them.
|
232 |
+
logging.info(f"Full response: {full_response}")
|
233 |
+
tool_calls = extract_tool_calls(full_response)
|
234 |
+
logging.info(f"Tool calls: {tool_calls}")
|
235 |
+
for tool_call in tool_calls:
|
236 |
+
tool_output = process_tool_call(tool_call)
|
237 |
+
logging.info(f"π§ Tool Execution: {tool_output}")
|
238 |
+
with log_path.open("a") as f:
|
239 |
+
f.write(f"π§ Tool Execution: {tool_output}\n")
|
240 |
+
return tool_output
|
241 |
+
|
242 |
+
# Automatically start the research process when the app starts.
|
243 |
+
@asynccontextmanager
|
244 |
+
async def lifespan(app: FastAPI):
|
245 |
+
"""Start the background task when FastAPI starts."""
|
246 |
+
logging.info("Starting run_research_forever()...")
|
247 |
+
await asyncio.sleep(5) # Wait for the server to load
|
248 |
+
asyncio.create_task(run_research_forever()) # Run in background
|
249 |
+
yield
|
250 |
+
logging.info("FastAPI shutdown: Cleaning up resources.")
|
251 |
+
|
252 |
+
# Initialize the FastAPI application
|
253 |
+
app = FastAPI(lifespan=lifespan)
|
254 |
|
255 |
+
# Load the Llama model (assumed to return a synchronous generator when stream=True)
|
256 |
+
llm = Llama(model_path=model_in_use["file"], n_ctx=2048)
|
257 |
|
258 |
+
@app.websocket("/stream")
|
259 |
+
async def stream(websocket: WebSocket):
|
260 |
+
logging.info("WebSocket connection established.")
|
261 |
+
global log_path, last_read_position
|
262 |
+
await websocket.accept()
|
263 |
+
|
264 |
+
# Send existing interaction history to the client.
|
265 |
+
try:
|
266 |
+
with open(log_path, "r") as log_file:
|
267 |
+
log_file.seek(last_read_position)
|
268 |
+
interaction_history = log_file.read()
|
269 |
+
last_read_position = log_file.tell()
|
270 |
+
if interaction_history:
|
271 |
+
await websocket.send_text(interaction_history)
|
272 |
+
except Exception as e:
|
273 |
+
logging.error(f"Error reading interaction history: {e}")
|
274 |
+
|
275 |
+
# Continuously send updates from the log file.
|
276 |
+
while True:
|
277 |
+
await asyncio.sleep(0.1)
|
278 |
+
try:
|
279 |
+
with open(log_path, "r") as log_file:
|
280 |
+
log_file.seek(last_read_position)
|
281 |
+
new_content = log_file.read()
|
282 |
+
if new_content:
|
283 |
+
await websocket.send_text(new_content)
|
284 |
+
last_read_position = log_file.tell()
|
285 |
except Exception as e:
|
286 |
+
logging.error(f"Error reading interaction history: {e}")
|
|
|
287 |
|
288 |
+
# Endpoint to retrieve the interaction log.
|
289 |
+
@app.get("/log")
|
290 |
+
async def get_log():
|
291 |
+
try:
|
292 |
+
with open("interaction_history.log", "r") as f:
|
293 |
+
log_content = f.read()
|
294 |
+
# Return the log inside a <pre> block for readability.
|
295 |
+
return HTMLResponse(content=f"<pre>{log_content}</pre>")
|
296 |
+
except Exception as e:
|
297 |
+
logging.error(f"Error reading log: {e}")
|
298 |
+
return {"error": str(e)}
|
299 |
|
300 |
+
# A simple frontend page with a link to the log.
|
301 |
@app.get("/", response_class=HTMLResponse)
|
302 |
async def get():
|
303 |
+
try:
|
304 |
+
with open("templates/index.html", "r") as f:
|
305 |
+
html_content = f.read()
|
306 |
+
except Exception as e:
|
307 |
+
logging.error(f"Error loading template: {e}")
|
308 |
+
html_content = "<html><body><h1>Error loading template</h1></body></html>"
|
309 |
+
return HTMLResponse(html_content)
|
310 |
+
|
311 |
+
# To run the app, use a command like:
|
312 |
+
if __name__ == "__main__":
|
313 |
+
import uvicorn
|
314 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
app/templates/Llama8bq4k.html
ADDED
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% if not add_generation_prompt is defined %}
|
2 |
+
{% set add_generation_prompt = false %}
|
3 |
+
{% endif %}
|
4 |
+
|
5 |
+
{% set ns = namespace(
|
6 |
+
is_first=false,
|
7 |
+
is_tool=false,
|
8 |
+
is_output_first=true,
|
9 |
+
system_prompt=''
|
10 |
+
) %}
|
11 |
+
|
12 |
+
{%- for message in messages %}
|
13 |
+
{%- if message['role'] == 'system' %}
|
14 |
+
{% set ns.system_prompt = message['content'] %}
|
15 |
+
{%- endif %}
|
16 |
+
{%- endfor %}
|
17 |
+
|
18 |
+
{{ bos_token }}{{ ns.system_prompt }}
|
19 |
+
|
20 |
+
{%- for message in messages %}
|
21 |
+
{%- if message['role'] == 'user' %}
|
22 |
+
{%- set ns.is_tool = false -%}
|
23 |
+
{{ '<ο½Userο½>' + message['content'] }}
|
24 |
+
{%- endif %}
|
25 |
+
|
26 |
+
{%- if message['role'] == 'assistant' and message['content'] is none %}
|
27 |
+
{%- set ns.is_tool = false -%}
|
28 |
+
{%- for tool in message['tool_calls'] %}
|
29 |
+
{%- if not ns.is_first %}
|
30 |
+
{{ '<ο½Assistantο½><ο½toolβcallsβbeginο½><ο½toolβcallβbeginο½>' +
|
31 |
+
tool['type'] + '<ο½toolβsepο½>' + tool['function']['name'] + '\n' +
|
32 |
+
'```json' + '\n' + tool['function']['arguments'] + '\n' + '```' + '<ο½toolβcallβendο½>' }}
|
33 |
+
{% set ns.is_first = true %}
|
34 |
+
{%- else %}
|
35 |
+
{{ '\n' + '<ο½toolβcallβbeginο½>' + tool['type'] + '<ο½toolβsepο½>' +
|
36 |
+
tool['function']['name'] + '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' +
|
37 |
+
'```' + '<ο½toolβcallβendο½>' }}
|
38 |
+
{{ '<ο½toolβcallsβendο½><ο½endβofβsentenceο½>' }}
|
39 |
+
{%- endif %}
|
40 |
+
{%- endfor %}
|
41 |
+
{%- endif %}
|
42 |
+
|
43 |
+
{%- if message['role'] == 'assistant' and message['content'] is not none %}
|
44 |
+
{%- if ns.is_tool %}
|
45 |
+
{{ '<ο½toolβoutputsβendο½>' + message['content'] + '<ο½endβofβsentenceο½>' }}
|
46 |
+
{% set ns.is_tool = false %}
|
47 |
+
{%- else %}
|
48 |
+
{% set content = message['content'] %}
|
49 |
+
{% if '</think>' in content %}
|
50 |
+
{% set content = content.split('</think>')[-1] %}
|
51 |
+
{% endif %}
|
52 |
+
{{ '<ο½Assistantο½>' + content + '<ο½endβofβsentenceο½>' }}
|
53 |
+
{%- endif %}
|
54 |
+
{%- endif %}
|
55 |
+
|
56 |
+
{%- if message['role'] == 'tool' %}
|
57 |
+
{% set ns.is_tool = true %}
|
58 |
+
{%- if ns.is_output_first %}
|
59 |
+
{{ '<ο½toolβoutputsβbeginο½><ο½toolβoutputβbeginο½>' + message['content'] + '<ο½toolβoutputβendο½>' }}
|
60 |
+
{% set ns.is_output_first = false %}
|
61 |
+
{%- else %}
|
62 |
+
{{ '\n<ο½toolβoutputβbeginο½>' + message['content'] + '<ο½toolβoutputβendο½>' }}
|
63 |
+
{%- endif %}
|
64 |
+
{%- endif %}
|
65 |
+
{%- endfor -%}
|
66 |
+
|
67 |
+
{% if ns.is_tool %}
|
68 |
+
{{ '<ο½toolβoutputsβendο½>' }}
|
69 |
+
{% endif %}
|
70 |
+
|
71 |
+
{% if add_generation_prompt and not ns.is_tool %}
|
72 |
+
{{ '<ο½Assistantο½>' }}
|
73 |
+
{% endif %}
|
app/templates/Qwen5bq2k.html
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% if not add_generation_prompt is defined %}
|
2 |
+
{% set add_generation_prompt = false %}
|
3 |
+
{% endif %}
|
4 |
+
|
5 |
+
{% set ns = namespace(
|
6 |
+
is_first=false,
|
7 |
+
is_tool=false,
|
8 |
+
is_output_first=true,
|
9 |
+
system_prompt=''
|
10 |
+
) %}
|
11 |
+
|
12 |
+
{%- for message in messages %}
|
13 |
+
{%- if message['role'] == 'system' %}
|
14 |
+
{% set ns.system_prompt = message['content'] %}
|
15 |
+
{%- endif %}
|
16 |
+
{%- endfor %}
|
17 |
+
|
18 |
+
{{ bos_token }}{{ ns.system_prompt }}
|
19 |
+
|
20 |
+
{%- for message in messages %}
|
21 |
+
{%- if message['role'] == 'user' %}
|
22 |
+
{%- set ns.is_tool = false -%}
|
23 |
+
{{ '<ο½Userο½>' + message['content'] }}
|
24 |
+
{%- endif %}
|
25 |
+
|
26 |
+
{%- if message['role'] == 'assistant' and message['content'] is none %}
|
27 |
+
{%- set ns.is_tool = false -%}
|
28 |
+
{%- for tool in message['tool_calls'] %}
|
29 |
+
{%- if not ns.is_first %}
|
30 |
+
{{ '<ο½Assistantο½><ο½toolβcallsβbeginο½><ο½toolβcallβbeginο½>' + tool['type']
|
31 |
+
+ '<ο½toolβsepο½>' + tool['function']['name'] + '\n'
|
32 |
+
+ '```json' + '\n' + tool['function']['arguments'] + '\n' + '```'
|
33 |
+
+ '<ο½toolβcallβendο½>'
|
34 |
+
}}
|
35 |
+
{%- set ns.is_first = true -%}
|
36 |
+
{%- else %}
|
37 |
+
{{ '\n' + '<ο½toolβcallβbeginο½>' + tool['type'] + '<ο½toolβsepο½>' + tool['function']['name']
|
38 |
+
+ '\n' + '```json' + '\n' + tool['function']['arguments'] + '\n' + '```'
|
39 |
+
+ '<ο½toolβcallβendο½>'
|
40 |
+
}}
|
41 |
+
{{ '<ο½toolβcallsβendο½><ο½endβofβsentenceο½>' }}
|
42 |
+
{%- endif %}
|
43 |
+
{%- endfor %}
|
44 |
+
{%- endif %}
|
45 |
+
|
46 |
+
{%- if message['role'] == 'assistant' and message['content'] is not none %}
|
47 |
+
{%- if ns.is_tool %}
|
48 |
+
{{ '<ο½toolβoutputsβendο½>' + message['content'] + '<ο½endβofβsentenceο½>' }}
|
49 |
+
{%- set ns.is_tool = false -%}
|
50 |
+
{%- else %}
|
51 |
+
{% set content = message['content'] %}
|
52 |
+
{% if '</think>' in content %}
|
53 |
+
{% set content = content.split('</think>')[-1] %}
|
54 |
+
{% endif %}
|
55 |
+
{{ '<ο½Assistantο½>' + content + '<ο½endβofβsentenceο½>' }}
|
56 |
+
{%- endif %}
|
57 |
+
{%- endif %}
|
58 |
+
|
59 |
+
{%- if message['role'] == 'tool' %}
|
60 |
+
{%- set ns.is_tool = true -%}
|
61 |
+
{%- if ns.is_output_first %}
|
62 |
+
{{ '<ο½toolβoutputsβbeginο½><ο½toolβoutputβbeginο½>' + message['content'] + '<ο½toolβoutputβendο½>' }}
|
63 |
+
{%- set ns.is_output_first = false %}
|
64 |
+
{%- else %}
|
65 |
+
{{ '\n<ο½toolβoutputβbeginο½>' + message['content'] + '<ο½toolβoutputβendο½>' }}
|
66 |
+
{%- endif %}
|
67 |
+
{%- endif %}
|
68 |
+
{%- endfor -%}
|
69 |
+
|
70 |
+
{% if ns.is_tool %}
|
71 |
+
{{ '<ο½toolβoutputsβendο½>' }}
|
72 |
+
{% endif %}
|
73 |
+
|
74 |
+
{% if add_generation_prompt and not ns.is_tool %}
|
75 |
+
{{ '<ο½Assistantο½>' }}
|
76 |
+
{% endif %}
|
app/templates/default.html
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% for message in messages %}
|
2 |
+
{% if message.role == "system" -%}
|
3 |
+
{{ message.content }}
|
4 |
+
{% elif message.role == "assistant" -%}
|
5 |
+
<ο½Assistantο½>{{ message.content }}
|
6 |
+
{% elif message.role == "tool" -%}
|
7 |
+
<ο½Toolο½>{{ message.content }}
|
8 |
+
{% endif %}
|
9 |
+
{% endfor %}
|
app/templates/index.html
CHANGED
@@ -2,34 +2,33 @@
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
-
<
|
6 |
-
<
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
<script>
|
12 |
-
const terminal = document.getElementById('terminal');
|
13 |
-
const websocket = new WebSocket("ws://" + location.host + "/stream");
|
14 |
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
terminal.appendChild(message);
|
19 |
-
terminal.scrollTop = terminal.scrollHeight; // Scroll to bottom
|
20 |
-
};
|
21 |
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
terminal.appendChild(welcomeMessage);
|
26 |
-
};
|
27 |
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
33 |
</script>
|
|
|
|
|
|
|
|
|
34 |
</body>
|
35 |
-
</html>
|
|
|
2 |
<html lang="en">
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
+
<title>WebSocket Log Viewer</title>
|
6 |
+
<script type="text/javascript">
|
7 |
+
document.addEventListener("DOMContentLoaded", function() {
|
8 |
+
const logContainer = document.getElementById("log");
|
9 |
+
|
10 |
+
const ws = new WebSocket("ws://localhost:8000/stream");
|
|
|
|
|
|
|
11 |
|
12 |
+
ws.onmessage = function(event) {
|
13 |
+
logContainer.textContent += event.data;
|
14 |
+
};
|
|
|
|
|
|
|
15 |
|
16 |
+
ws.onopen = function() {
|
17 |
+
console.log("WebSocket connection established.");
|
18 |
+
};
|
|
|
|
|
19 |
|
20 |
+
ws.onclose = function() {
|
21 |
+
console.log("WebSocket connection closed.");
|
22 |
+
};
|
23 |
+
|
24 |
+
ws.onerror = function(error) {
|
25 |
+
console.error("WebSocket error:", error);
|
26 |
+
};
|
27 |
+
});
|
28 |
</script>
|
29 |
+
</head>
|
30 |
+
<body>
|
31 |
+
<h1>Interaction Log</h1>
|
32 |
+
<pre id="log"></pre>
|
33 |
</body>
|
34 |
+
</html>
|
app/test.py
ADDED
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import asyncio
|
3 |
+
import subprocess
|
4 |
+
import json
|
5 |
+
import concurrent.futures
|
6 |
+
from fastapi import FastAPI, WebSocket
|
7 |
+
from fastapi.responses import HTMLResponse
|
8 |
+
from jinja2 import Template
|
9 |
+
from llama_cpp import Llama
|
10 |
+
from contextlib import asynccontextmanager
|
11 |
+
import logging
|
12 |
+
from pathlib import Path
|
13 |
+
|
14 |
+
# Set up logging
|
15 |
+
logging.basicConfig(level=logging.INFO)
|
16 |
+
|
17 |
+
# Set up the log file and ensure it exists
|
18 |
+
log_path = Path("interaction_history.log")
|
19 |
+
log_path.touch(exist_ok=True)
|
20 |
+
|
21 |
+
# Global variable to keep track of the last read position in the log file
|
22 |
+
last_read_position = 0
|
23 |
+
|
24 |
+
# Define the models and their paths
|
25 |
+
models = {
|
26 |
+
"production": {
|
27 |
+
"file": "DeepSeek-R1-Distill-Llama-8B-Q4_K_L.gguf",
|
28 |
+
"alias": "R1Llama8BQ4L",
|
29 |
+
"template": "/templates/Llama8bq4k.html"
|
30 |
+
},
|
31 |
+
"development": {
|
32 |
+
"file": "/home/ali/Projects/VirtualLabDev/Local/DeepSeek-R1-Distill-Qwen-1.5B-Q2_K.gguf",
|
33 |
+
"alias": "R1Qwen1.5BQ2",
|
34 |
+
"template": "./templates/Qwen5bq2k.html"
|
35 |
+
},
|
36 |
+
}
|
37 |
+
|
38 |
+
model_in_use = models["development"]
|
39 |
+
|
40 |
+
with open(model_in_use["template"], "r") as jinja_template:
|
41 |
+
CHAT_TEMPLATE = jinja_template.read()
|
42 |
+
|
43 |
+
with open("templates/default.html", "r") as jinja_template:
|
44 |
+
CHAT_TEMPLATE = jinja_template.read()
|
45 |
+
|
46 |
+
# Define the shell execution tool
|
47 |
+
def execute_shell(arguments):
|
48 |
+
"""Execute a shell command."""
|
49 |
+
try:
|
50 |
+
args = json.loads(arguments)
|
51 |
+
command = args.get("command", "")
|
52 |
+
if not command:
|
53 |
+
return json.dumps({"error": "No command provided."})
|
54 |
+
process = subprocess.run(
|
55 |
+
command,
|
56 |
+
shell=True,
|
57 |
+
stdout=subprocess.PIPE,
|
58 |
+
stderr=subprocess.PIPE,
|
59 |
+
text=True
|
60 |
+
)
|
61 |
+
return json.dumps({"stdout": process.stdout, "stderr": process.stderr})
|
62 |
+
except Exception as e:
|
63 |
+
return json.dumps({"error": str(e)})
|
64 |
+
|
65 |
+
# Define the tools available to the assistant
|
66 |
+
tools = {
|
67 |
+
"shell": {
|
68 |
+
"description": "Execute shell commands.",
|
69 |
+
"example_input": '{"command": "ls -l"}',
|
70 |
+
"example_output": '{"stdout": "...", "stderr": "..."}',
|
71 |
+
"function": execute_shell,
|
72 |
+
},
|
73 |
+
}
|
74 |
+
|
75 |
+
# Dynamically generate the system prompt based on available tools.
|
76 |
+
def generate_system_prompt(tools):
|
77 |
+
tool_descriptions = []
|
78 |
+
for tool_name, tool_data in tools.items():
|
79 |
+
description = tool_data.get("description", "No description available.")
|
80 |
+
example_input = tool_data.get("example_input", "{}")
|
81 |
+
example_output = tool_data.get("example_output", "{}")
|
82 |
+
tool_descriptions.append(
|
83 |
+
f"""- **{tool_name}**:
|
84 |
+
- Description: {description}
|
85 |
+
- Input: {example_input}
|
86 |
+
- Output: {example_output}"""
|
87 |
+
)
|
88 |
+
return (
|
89 |
+
"You are an autonomous computational biology researcher with access to the following tools:\n\n"
|
90 |
+
+ "\n\n".join(tool_descriptions)
|
91 |
+
)
|
92 |
+
|
93 |
+
# Create the system prompt.
|
94 |
+
system_prompt = generate_system_prompt(tools)
|
95 |
+
|
96 |
+
# Parse out any tool calls embedded in the model's output.
|
97 |
+
def extract_tool_calls(response_text):
|
98 |
+
"""
|
99 |
+
Parse tool calls from model output.
|
100 |
+
|
101 |
+
The model is expected to demarcate tool calls between markers like:
|
102 |
+
<ο½toolβcallsβbeginο½> ... <ο½toolβcallsβendο½>
|
103 |
+
and each individual call between:
|
104 |
+
<ο½toolβcallβbeginο½> ... <ο½toolβsepο½> ... "```json" ... "```"
|
105 |
+
"""
|
106 |
+
if "<ο½toolβcallsβbeginο½>" not in response_text:
|
107 |
+
return []
|
108 |
+
|
109 |
+
tool_calls_part = response_text.split("<ο½toolβcallsβbeginο½>")[1]
|
110 |
+
tool_calls_part = tool_calls_part.split("<ο½toolβcallsβendο½>")[0]
|
111 |
+
tool_calls = tool_calls_part.split("<ο½toolβcallβbeginο½>")
|
112 |
+
|
113 |
+
parsed_tool_calls = []
|
114 |
+
for tool_call in tool_calls:
|
115 |
+
tool_call = tool_call.strip()
|
116 |
+
if tool_call:
|
117 |
+
try:
|
118 |
+
tool_type, tool_name_and_args = tool_call.split("<ο½toolβsepο½>")
|
119 |
+
tool_name, tool_args = tool_name_and_args.split("\n```json\n", 1)
|
120 |
+
tool_args = tool_args.split("\n```")[0]
|
121 |
+
parsed_tool_calls.append({
|
122 |
+
"type": tool_type,
|
123 |
+
"name": tool_name.strip(),
|
124 |
+
"arguments": tool_args.strip()
|
125 |
+
})
|
126 |
+
except ValueError:
|
127 |
+
logging.warning("Failed to parse tool call: %s", tool_call)
|
128 |
+
return parsed_tool_calls
|
129 |
+
|
130 |
+
def process_tool_call(tool_call):
|
131 |
+
"""Execute the requested tool and return its output."""
|
132 |
+
tool_name = tool_call["name"]
|
133 |
+
tool_args = tool_call["arguments"]
|
134 |
+
|
135 |
+
if tool_name in tools:
|
136 |
+
tool_function = tools[tool_name]["function"]
|
137 |
+
return tool_function(tool_args)
|
138 |
+
else:
|
139 |
+
return json.dumps({"error": f"Tool {tool_name} not found."})
|
140 |
+
|
141 |
+
#
|
142 |
+
# Helper: Wrap a synchronous generator as an asynchronous generator.
|
143 |
+
#
|
144 |
+
async def async_generator_from_sync(sync_gen_func, *args, **kwargs):
|
145 |
+
"""
|
146 |
+
Runs a synchronous generator function in a thread and yields items asynchronously.
|
147 |
+
"""
|
148 |
+
loop = asyncio.get_running_loop()
|
149 |
+
q = asyncio.Queue()
|
150 |
+
|
151 |
+
def producer():
|
152 |
+
try:
|
153 |
+
for item in sync_gen_func(*args, **kwargs):
|
154 |
+
loop.call_soon_threadsafe(q.put_nowait, item)
|
155 |
+
except Exception as e:
|
156 |
+
loop.call_soon_threadsafe(q.put_nowait, e)
|
157 |
+
finally:
|
158 |
+
# Signal the end of iteration with a sentinel (None)
|
159 |
+
loop.call_soon_threadsafe(q.put_nowait, None)
|
160 |
+
|
161 |
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
162 |
+
executor.submit(producer)
|
163 |
+
while True:
|
164 |
+
item = await q.get()
|
165 |
+
if item is None:
|
166 |
+
break
|
167 |
+
if isinstance(item, Exception):
|
168 |
+
raise item
|
169 |
+
yield item
|
170 |
+
|
171 |
+
#
|
172 |
+
# Background response generator without requiring a WebSocket.
|
173 |
+
#
|
174 |
+
async def generate_response_background(conversation):
|
175 |
+
"""Generate a model response asynchronously."""
|
176 |
+
#template = Template(CHAT_TEMPLATE)
|
177 |
+
#prompt = template.render(messages=conversation)
|
178 |
+
#logging.info(f"Prompt: {prompt}")
|
179 |
+
async for token_chunk in async_generator_from_sync(
|
180 |
+
llm.create_chat_completion,
|
181 |
+
messages=conversation,
|
182 |
+
stream=True,
|
183 |
+
max_tokens=2048
|
184 |
+
):
|
185 |
+
# Extract token from OpenAI-compatible format
|
186 |
+
token = token_chunk["choices"][0]["delta"].get("content", "")
|
187 |
+
yield token_chunk # Yield the token string directly
|
188 |
+
await asyncio.sleep(0)
|
189 |
+
|
190 |
+
#
|
191 |
+
# Main research loop running continuously in the background.
|
192 |
+
#
|
193 |
+
async def run_research_forever():
|
194 |
+
global log_path
|
195 |
+
logging.info("π Autonomous computational biology research initiated!")
|
196 |
+
with log_path.open("a") as f:
|
197 |
+
f.write("π Autonomous computational biology research initiated!\n")
|
198 |
+
|
199 |
+
conversation = [{"role": "system", "content": system_prompt}]
|
200 |
+
while True:
|
201 |
+
full_response = ""
|
202 |
+
try:
|
203 |
+
# Generate the model response and accumulate the full text.
|
204 |
+
async for token in generate_response_background(conversation):
|
205 |
+
token_text = token["choices"][0]["delta"].get("content", "")
|
206 |
+
full_response += token_text
|
207 |
+
# Log each token individually
|
208 |
+
with open(log_path, "a") as f:
|
209 |
+
f.write(token_text)
|
210 |
+
f.flush()
|
211 |
+
# Optionally, check if a finish reason is provided
|
212 |
+
if token['choices'][0].get("finish_reason", "") is not None:
|
213 |
+
|
214 |
+
# The presence of a finish reason (like "stop") indicates that generation is complete.
|
215 |
+
# Append the assistant's response to the conversation log.
|
216 |
+
conversation.append({"role": "assistant", "content": full_response})
|
217 |
+
try:
|
218 |
+
tool_output = parse_tool_calls(full_response)
|
219 |
+
conversation.append({"role": "tool", "content": tool_output})
|
220 |
+
except Exception as e:
|
221 |
+
logging.error(f"π οΈ Tool execution failed: {e}")
|
222 |
+
continue
|
223 |
+
except Exception as e:
|
224 |
+
logging.error(f"Autonomous research error during response generation: {e}")
|
225 |
+
continue
|
226 |
+
|
227 |
+
# Delay before the next query iteration.
|
228 |
+
await asyncio.sleep(1)
|
229 |
+
|
230 |
+
def parse_tool_calls(full_response):
|
231 |
+
# Check for tool calls in the response and process them.
|
232 |
+
logging.info(f"Full response: {full_response}")
|
233 |
+
tool_calls = extract_tool_calls(full_response)
|
234 |
+
logging.info(f"Tool calls: {tool_calls}")
|
235 |
+
for tool_call in tool_calls:
|
236 |
+
tool_output = process_tool_call(tool_call)
|
237 |
+
logging.info(f"π§ Tool Execution: {tool_output}")
|
238 |
+
with log_path.open("a") as f:
|
239 |
+
f.write(f"π§ Tool Execution: {tool_output}\n")
|
240 |
+
return tool_output
|
241 |
+
|
242 |
+
# Automatically start the research process when the app starts.
|
243 |
+
@asynccontextmanager
|
244 |
+
async def lifespan(app: FastAPI):
|
245 |
+
"""Start the background task when FastAPI starts."""
|
246 |
+
logging.info("Starting run_research_forever()...")
|
247 |
+
await asyncio.sleep(5) # Wait for the server to load
|
248 |
+
asyncio.create_task(run_research_forever()) # Run in background
|
249 |
+
yield
|
250 |
+
logging.info("FastAPI shutdown: Cleaning up resources.")
|
251 |
+
|
252 |
+
# Initialize the FastAPI application
|
253 |
+
app = FastAPI(lifespan=lifespan)
|
254 |
+
|
255 |
+
# Load the Llama model (assumed to return a synchronous generator when stream=True)
|
256 |
+
llm = Llama(model_path=model_in_use["file"], n_ctx=2048)
|
257 |
+
|
258 |
+
@app.websocket("/stream")
|
259 |
+
async def stream(websocket: WebSocket):
|
260 |
+
logging.info("WebSocket connection established.")
|
261 |
+
global log_path, last_read_position
|
262 |
+
await websocket.accept()
|
263 |
+
|
264 |
+
# Send existing interaction history to the client.
|
265 |
+
try:
|
266 |
+
with open(log_path, "r") as log_file:
|
267 |
+
log_file.seek(last_read_position)
|
268 |
+
interaction_history = log_file.read()
|
269 |
+
last_read_position = log_file.tell()
|
270 |
+
if interaction_history:
|
271 |
+
await websocket.send_text(interaction_history)
|
272 |
+
except Exception as e:
|
273 |
+
logging.error(f"Error reading interaction history: {e}")
|
274 |
+
|
275 |
+
# Continuously send updates from the log file.
|
276 |
+
while True:
|
277 |
+
await asyncio.sleep(0.1)
|
278 |
+
try:
|
279 |
+
with open(log_path, "r") as log_file:
|
280 |
+
log_file.seek(last_read_position)
|
281 |
+
new_content = log_file.read()
|
282 |
+
if new_content:
|
283 |
+
await websocket.send_text(new_content)
|
284 |
+
last_read_position = log_file.tell()
|
285 |
+
except Exception as e:
|
286 |
+
logging.error(f"Error reading interaction history: {e}")
|
287 |
+
|
288 |
+
# Endpoint to retrieve the interaction log.
|
289 |
+
@app.get("/log")
|
290 |
+
async def get_log():
|
291 |
+
try:
|
292 |
+
with open("interaction_history.log", "r") as f:
|
293 |
+
log_content = f.read()
|
294 |
+
# Return the log inside a <pre> block for readability.
|
295 |
+
return HTMLResponse(content=f"<pre>{log_content}</pre>")
|
296 |
+
except Exception as e:
|
297 |
+
logging.error(f"Error reading log: {e}")
|
298 |
+
return {"error": str(e)}
|
299 |
+
|
300 |
+
# A simple frontend page with a link to the log.
|
301 |
+
@app.get("/", response_class=HTMLResponse)
|
302 |
+
async def get():
|
303 |
+
try:
|
304 |
+
with open("templates/index.html", "r") as f:
|
305 |
+
html_content = f.read()
|
306 |
+
except Exception as e:
|
307 |
+
logging.error(f"Error loading template: {e}")
|
308 |
+
html_content = "<html><body><h1>Error loading template</h1></body></html>"
|
309 |
+
return HTMLResponse(html_content)
|
310 |
+
|
311 |
+
# To run the app, use a command like:
|
312 |
+
if __name__ == "__main__":
|
313 |
+
import uvicorn
|
314 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
entrypoint.py
CHANGED
@@ -1,27 +1,3 @@
|
|
1 |
import subprocess
|
2 |
-
import time
|
3 |
-
import logging
|
4 |
|
5 |
-
|
6 |
-
logging.basicConfig(level=logging.INFO)
|
7 |
-
|
8 |
-
models = {"production":{"file":"DeepSeek-R1-Distill-Llama-8B-Q4_K_L.gguf", "alias":"R1Llama8BQ4L",},
|
9 |
-
"development":{"file":"/home/ali/Projects/VirtualLabDev/Local/DeepSeek-R1-Distill-Qwen-1.5B-Q2_K.gguf", "alias":"R1Qwen1.5BQ2",}}
|
10 |
-
|
11 |
-
selected_model = models["production"]
|
12 |
-
|
13 |
-
# Start the model server in the background
|
14 |
-
time_to_wait = 10
|
15 |
-
logging.info(f"Starting Llama model server... Give it {time_to_wait} seconds to start.")
|
16 |
-
output = subprocess.Popen(["python", "-m", "llama_cpp.server",
|
17 |
-
"--model", selected_model["file"],
|
18 |
-
"--model_alias", selected_model["alias"],
|
19 |
-
"--port", "8000"
|
20 |
-
])
|
21 |
-
# Give the server 30 seconds to start
|
22 |
-
time.sleep(time_to_wait)
|
23 |
-
logging.info("Llama model server should be ready.")
|
24 |
-
|
25 |
-
# Start the main application and wait for it to finish
|
26 |
-
logging.info("Starting FastAPI server...")
|
27 |
-
subprocess.run(["uvicorn", f"main:app", "--host", "0.0.0.0", "--port", "7860"])
|
|
|
1 |
import subprocess
|
|
|
|
|
2 |
|
3 |
+
subprocess.run(["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|