VirtualLab commited on
Commit
6ac5190
Β·
1 Parent(s): 2fd5de9

First agent

Browse files
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 7860
25
 
26
  # Command to start the server
27
- CMD ["uvicorn", main:app", "--host", "0.0.0.0", "--port", "7860"]
 
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 (for debugging)
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 = {"production":{"file":"DeepSeek-R1-Distill-Llama-8B-Q4_K_L.gguf", "alias":"R1Llama8BQ4L",},
18
- "development":{"file":"/home/ali/Projects/VirtualLabDev/Local/DeepSeek-R1-Distill-Qwen-1.5B-Q2_K.gguf", "alias":"R1Qwen1.5BQ2",}}
19
-
20
- # Load the model using llama_cpp
21
- MODEL_PATH = models["development"]["file"]
22
- llm = Llama(model_path=MODEL_PATH, n_ctx=2048) # Context size can be adjusted based on model requirements
23
-
24
- # System Prompt (inject this into the chat template)
25
- SYSTEM_PROMPT = """You are an AI computational biology assistant capable of running shell commands via tool calls. Your task is to autonomously explore, analyze, and provide insights into biological datasets."""
26
-
27
- # Input Message Template
28
- CHAT_TEMPLATE = """
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
- @app.websocket("/stream")
64
- async def stream(websocket: WebSocket):
65
- """WebSocket handler for live streaming interactions."""
66
- await websocket.accept()
67
- await websocket.send_text("πŸš€ Connected to the AI-driven computational biology assistant!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
- # The conversation state
70
- conversation = []
71
- conversation.append({"role": "system", "content": SYSTEM_PROMPT})
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- while True:
74
- try:
75
- # Wait for the user message via WebSocket
76
- user_message = await websocket.receive_text()
77
- await websocket.send_text(f"πŸ€– User: {user_message}")
78
 
79
- # Add the user message to the conversation
80
- conversation.append({"role": "user", "content": user_message})
 
 
 
81
 
82
- # Generate a response using the model
83
- response = generate_response(conversation)
 
 
 
 
 
 
 
84
 
85
- # Check for tool-related outputs (defined by the chat template formats)
86
- if "<|tool▁call▁begin|>" in response:
87
- # Extract tool-related information
88
- tool_name = response.split("<|tool▁sep|>")[1].split("\n")[0].strip()
89
- tool_args = response.split("```json\n")[1].split("\n```")[0].strip()
 
 
 
 
90
 
91
- await websocket.send_text(f"πŸ”§ Tool Detected: {tool_name}")
92
- await websocket.send_text(f"πŸ“œ Arguments: {tool_args}")
 
 
 
 
 
 
 
93
 
94
- # Execute the tool's shell command if relevant
95
- if tool_name == "shell":
96
- # Execute the command in the shell (using subprocess)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  try:
98
- command = json.loads(tool_args).get("command", "")
99
- process = subprocess.Popen(
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
- await websocket.send_text(f"⚠️ Error executing tool: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
- # Add tool response back to the conversation
125
- conversation.append({"role": "tool", "content": "Tool executed successfully."})
126
 
127
- else:
128
- # Regular assistant response (not a tool call)
129
- conversation.append({"role": "assistant", "content": response})
130
- await websocket.send_text(f"πŸ€– AI: {response}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  except Exception as e:
132
- await websocket.send_text(f"⚠️ Error: {str(e)}")
133
- break
134
 
135
- await websocket.close()
 
 
 
 
 
 
 
 
 
 
136
 
 
137
  @app.get("/", response_class=HTMLResponse)
138
  async def get():
139
- """Serve the frontend."""
140
- # Load the HTML terminal-like UI from the template
141
- with open("templates/index.html", "r", encoding="utf-8") as file:
142
- html_content = file.read()
143
- return html_content
 
 
 
 
 
 
 
 
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
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI Commandline</title>
7
- <link rel="stylesheet" href="/static/style.css">
8
- </head>
9
- <body>
10
- <div id="terminal"></div>
11
- <script>
12
- const terminal = document.getElementById('terminal');
13
- const websocket = new WebSocket("ws://" + location.host + "/stream");
14
 
15
- websocket.onmessage = (event) => {
16
- const message = document.createElement('div');
17
- message.textContent = event.data;
18
- terminal.appendChild(message);
19
- terminal.scrollTop = terminal.scrollHeight; // Scroll to bottom
20
- };
21
 
22
- websocket.onopen = () => {
23
- const welcomeMessage = document.createElement('div');
24
- welcomeMessage.textContent = "✨ Welcome to the AI Command-line Interface!";
25
- terminal.appendChild(welcomeMessage);
26
- };
27
 
28
- websocket.onclose = () => {
29
- const goodbyeMessage = document.createElement('div');
30
- goodbyeMessage.textContent = "πŸ”Œ Disconnected.";
31
- terminal.appendChild(goodbyeMessage);
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
- # Configure logging
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"])