tech-envision commited on
Commit
7a7b1d3
·
1 Parent(s): aee9883

Add document upload support and update prompt

Browse files
Files changed (7) hide show
  1. README.md +10 -1
  2. run.py +2 -3
  3. src/chat.py +31 -7
  4. src/config.py +8 -3
  5. src/db.py +20 -1
  6. src/tools.py +2 -2
  7. src/vm.py +10 -1
README.md CHANGED
@@ -6,7 +6,8 @@ database using Peewee. Histories are persisted per user and session so
6
  conversations can be resumed with context. One example tool is included:
7
 
8
  * **execute_terminal** – Executes a shell command inside a persistent Linux VM
9
- with network access. Output from ``stdout`` and ``stderr`` is captured and
 
10
  returned. The VM is created when a chat session starts and reused for all
11
  subsequent tool calls.
12
 
@@ -22,6 +23,14 @@ python run.py
22
 
23
  The script will instruct the model to run a simple shell command and print the result. Conversations are automatically persisted to `chat.db` and are now associated with a user and session.
24
 
 
 
 
 
 
 
 
 
25
  ## Docker
26
 
27
  A Dockerfile is provided to run the Discord bot along with an Ollama server. The image installs Ollama, pulls the LLM and embedding models, and starts both the server and the bot.
 
6
  conversations can be resumed with context. One example tool is included:
7
 
8
  * **execute_terminal** – Executes a shell command inside a persistent Linux VM
9
+ with network access. Use it to read uploaded documents under ``/data`` or run
10
+ other commands. Output from ``stdout`` and ``stderr`` is captured and
11
  returned. The VM is created when a chat session starts and reused for all
12
  subsequent tool calls.
13
 
 
23
 
24
  The script will instruct the model to run a simple shell command and print the result. Conversations are automatically persisted to `chat.db` and are now associated with a user and session.
25
 
26
+ Uploaded files are stored under the `uploads` directory and mounted inside the VM at `/data`. Call ``upload_document`` on the chat session to make a file available to the model:
27
+
28
+ ```python
29
+ async with ChatSession() as chat:
30
+ path_in_vm = chat.upload_document("path/to/file.pdf")
31
+ reply = await chat.chat(f"Summarize {path_in_vm}")
32
+ ```
33
+
34
  ## Docker
35
 
36
  A Dockerfile is provided to run the Discord bot along with an Ollama server. The image installs Ollama, pulls the LLM and embedding models, and starts both the server and the bot.
run.py CHANGED
@@ -7,9 +7,8 @@ from src.chat import ChatSession
7
 
8
  async def _main() -> None:
9
  async with ChatSession(user="demo_user", session="demo_session") as chat:
10
- answer = await chat.chat(
11
- "check the processes running on the system"
12
- )
13
  print("\n>>>", answer)
14
 
15
 
 
7
 
8
  async def _main() -> None:
9
  async with ChatSession(user="demo_user", session="demo_session") as chat:
10
+ doc_path = chat.upload_document("README.md")
11
+ answer = await chat.chat(f"List the first three lines of {doc_path}")
 
12
  print("\n>>>", answer)
13
 
14
 
src/chat.py CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
2
 
3
  from typing import List
4
  import json
 
 
5
 
6
  from ollama import AsyncClient, ChatResponse, Message
7
 
@@ -12,8 +14,16 @@ from .config import (
12
  NUM_CTX,
13
  OLLAMA_HOST,
14
  SYSTEM_PROMPT,
 
 
 
 
 
 
 
 
 
15
  )
16
- from .db import Conversation, Message as DBMessage, User, _db, init_db
17
  from .log import get_logger
18
  from .schema import Msg
19
  from .tools import execute_terminal, set_vm
@@ -53,6 +63,24 @@ class ChatSession:
53
  if not _db.is_closed():
54
  _db.close()
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def _ensure_system_prompt(self) -> None:
57
  if any(m.get("role") == "system" for m in self._messages):
58
  return
@@ -84,9 +112,7 @@ class ChatSession:
84
  return messages
85
 
86
  @staticmethod
87
- def _store_assistant_message(
88
- conversation: Conversation, message: Message
89
- ) -> None:
90
  """Persist assistant messages, storing tool calls when present."""
91
 
92
  if message.tool_calls:
@@ -135,9 +161,7 @@ class ChatSession:
135
  )
136
  nxt = await self.ask(messages, think=True)
137
  self._store_assistant_message(conversation, nxt.message)
138
- return await self._handle_tool_calls(
139
- messages, nxt, conversation, depth + 1
140
- )
141
 
142
  return response
143
 
 
2
 
3
  from typing import List
4
  import json
5
+ import shutil
6
+ from pathlib import Path
7
 
8
  from ollama import AsyncClient, ChatResponse, Message
9
 
 
14
  NUM_CTX,
15
  OLLAMA_HOST,
16
  SYSTEM_PROMPT,
17
+ UPLOAD_DIR,
18
+ )
19
+ from .db import (
20
+ Conversation,
21
+ Message as DBMessage,
22
+ User,
23
+ _db,
24
+ init_db,
25
+ add_document,
26
  )
 
27
  from .log import get_logger
28
  from .schema import Msg
29
  from .tools import execute_terminal, set_vm
 
63
  if not _db.is_closed():
64
  _db.close()
65
 
66
+ def upload_document(self, file_path: str) -> str:
67
+ """Save a document for later access inside the VM.
68
+
69
+ The file is copied into ``UPLOAD_DIR`` and recorded in the database. The
70
+ returned path is the location inside the VM (prefixed with ``/data``).
71
+ """
72
+
73
+ src = Path(file_path)
74
+ if not src.exists():
75
+ raise FileNotFoundError(file_path)
76
+
77
+ dest = Path(UPLOAD_DIR) / self._user.username
78
+ dest.mkdir(parents=True, exist_ok=True)
79
+ target = dest / src.name
80
+ shutil.copy(src, target)
81
+ add_document(self._user.username, str(target), src.name)
82
+ return f"/data/{self._user.username}/{src.name}"
83
+
84
  def _ensure_system_prompt(self) -> None:
85
  if any(m.get("role") == "system" for m in self._messages):
86
  return
 
112
  return messages
113
 
114
  @staticmethod
115
+ def _store_assistant_message(conversation: Conversation, message: Message) -> None:
 
 
116
  """Persist assistant messages, storing tool calls when present."""
117
 
118
  if message.tool_calls:
 
161
  )
162
  nxt = await self.ask(messages, think=True)
163
  self._store_assistant_message(conversation, nxt.message)
164
+ return await self._handle_tool_calls(messages, nxt, conversation, depth + 1)
 
 
165
 
166
  return response
167
 
src/config.py CHANGED
@@ -1,18 +1,23 @@
1
  from __future__ import annotations
2
 
3
  import os
 
4
  from typing import Final
5
 
6
  MODEL_NAME: Final[str] = os.getenv("OLLAMA_MODEL", "qwen3:1.7b")
7
- EMBEDDING_MODEL_NAME: Final[str] = os.getenv("OLLAMA_EMBEDDING_MODEL", "snowflake-arctic-embed:137m") # unused for now
 
 
8
  OLLAMA_HOST: Final[str] = os.getenv("OLLAMA_HOST", "http://localhost:11434")
9
  MAX_TOOL_CALL_DEPTH: Final[int] = 5
10
  NUM_CTX: Final[int] = int(os.getenv("OLLAMA_NUM_CTX", "16000"))
 
11
 
12
  SYSTEM_PROMPT: Final[str] = (
13
  "You are a versatile AI assistant named Starlette able to orchestrate several tools to "
14
  "complete tasks. Plan your responses carefully and, when needed, call one "
15
  "or more tools consecutively to gather data, compute answers, or transform "
16
- "information. Continue chaining tools until the user's request is fully "
17
- "addressed and then deliver a concise, coherent final reply."
 
18
  )
 
1
  from __future__ import annotations
2
 
3
  import os
4
+ from pathlib import Path
5
  from typing import Final
6
 
7
  MODEL_NAME: Final[str] = os.getenv("OLLAMA_MODEL", "qwen3:1.7b")
8
+ EMBEDDING_MODEL_NAME: Final[str] = os.getenv(
9
+ "OLLAMA_EMBEDDING_MODEL", "snowflake-arctic-embed:137m"
10
+ ) # unused for now
11
  OLLAMA_HOST: Final[str] = os.getenv("OLLAMA_HOST", "http://localhost:11434")
12
  MAX_TOOL_CALL_DEPTH: Final[int] = 5
13
  NUM_CTX: Final[int] = int(os.getenv("OLLAMA_NUM_CTX", "16000"))
14
+ UPLOAD_DIR: Final[str] = os.getenv("UPLOAD_DIR", str(Path.cwd() / "uploads"))
15
 
16
  SYSTEM_PROMPT: Final[str] = (
17
  "You are a versatile AI assistant named Starlette able to orchestrate several tools to "
18
  "complete tasks. Plan your responses carefully and, when needed, call one "
19
  "or more tools consecutively to gather data, compute answers, or transform "
20
+ "information. Uploaded documents are available under /data and can be read "
21
+ "or modified using the execute_terminal tool. Continue chaining tools until "
22
+ "the user's request is fully addressed and then deliver a concise, coherent final reply."
23
  )
src/db.py CHANGED
@@ -46,12 +46,22 @@ class Message(BaseModel):
46
  created_at = DateTimeField(default=datetime.utcnow)
47
 
48
 
 
 
 
 
 
 
 
 
49
  __all__ = [
50
  "_db",
51
  "User",
52
  "Conversation",
53
  "Message",
 
54
  "reset_history",
 
55
  ]
56
 
57
 
@@ -59,7 +69,7 @@ def init_db() -> None:
59
  """Initialise the database and create tables if they do not exist."""
60
  if _db.is_closed():
61
  _db.connect()
62
- _db.create_tables([User, Conversation, Message])
63
 
64
 
65
  def reset_history(username: str, session_name: str) -> int:
@@ -79,3 +89,12 @@ def reset_history(username: str, session_name: str) -> int:
79
  if not Conversation.select().where(Conversation.user == user).exists():
80
  user.delete_instance()
81
  return deleted
 
 
 
 
 
 
 
 
 
 
46
  created_at = DateTimeField(default=datetime.utcnow)
47
 
48
 
49
+ class Document(BaseModel):
50
+ id = AutoField()
51
+ user = ForeignKeyField(User, backref="documents")
52
+ file_path = CharField()
53
+ original_name = CharField()
54
+ created_at = DateTimeField(default=datetime.utcnow)
55
+
56
+
57
  __all__ = [
58
  "_db",
59
  "User",
60
  "Conversation",
61
  "Message",
62
+ "Document",
63
  "reset_history",
64
+ "add_document",
65
  ]
66
 
67
 
 
69
  """Initialise the database and create tables if they do not exist."""
70
  if _db.is_closed():
71
  _db.connect()
72
+ _db.create_tables([User, Conversation, Message, Document])
73
 
74
 
75
  def reset_history(username: str, session_name: str) -> int:
 
89
  if not Conversation.select().where(Conversation.user == user).exists():
90
  user.delete_instance()
91
  return deleted
92
+
93
+
94
+ def add_document(username: str, file_path: str, original_name: str) -> Document:
95
+ """Record an uploaded document and return the created entry."""
96
+
97
+ init_db()
98
+ user, _ = User.get_or_create(username=username)
99
+ doc = Document.create(user=user, file_path=file_path, original_name=original_name)
100
+ return doc
src/tools.py CHANGED
@@ -20,7 +20,7 @@ def set_vm(vm: LinuxVM | None) -> None:
20
  def execute_terminal(command: str) -> str:
21
  """
22
  Execute a shell command in a Linux terminal.
23
- Use this tool to run various commands.
24
 
25
  The command is executed with network access enabled. Output from both
26
  ``stdout`` and ``stderr`` is captured and returned. Commands are killed if
@@ -29,7 +29,7 @@ def execute_terminal(command: str) -> str:
29
  timeout = 2
30
  if not command:
31
  return "No command provided."
32
-
33
  if _VM:
34
  try:
35
  return _VM.execute(command, timeout=timeout)
 
20
  def execute_terminal(command: str) -> str:
21
  """
22
  Execute a shell command in a Linux terminal.
23
+ Use this tool to inspect uploaded documents under ``/data`` or run other commands.
24
 
25
  The command is executed with network access enabled. Output from both
26
  ``stdout`` and ``stderr`` is captured and returned. Commands are killed if
 
29
  timeout = 2
30
  if not command:
31
  return "No command provided."
32
+
33
  if _VM:
34
  try:
35
  return _VM.execute(command, timeout=timeout)
src/vm.py CHANGED
@@ -2,6 +2,9 @@ from __future__ import annotations
2
 
3
  import subprocess
4
  import uuid
 
 
 
5
 
6
  from .log import get_logger
7
 
@@ -11,10 +14,14 @@ _LOG = get_logger(__name__)
11
  class LinuxVM:
12
  """Manage a lightweight Linux VM using Docker."""
13
 
14
- def __init__(self, image: str = "ubuntu:latest") -> None:
 
 
15
  self._image = image
16
  self._name = f"chat-vm-{uuid.uuid4().hex[:8]}"
17
  self._running = False
 
 
18
 
19
  def start(self) -> None:
20
  """Start the VM if it is not already running."""
@@ -36,6 +43,8 @@ class LinuxVM:
36
  "-d",
37
  "--name",
38
  self._name,
 
 
39
  self._image,
40
  "sleep",
41
  "infinity",
 
2
 
3
  import subprocess
4
  import uuid
5
+ from pathlib import Path
6
+
7
+ from .config import UPLOAD_DIR
8
 
9
  from .log import get_logger
10
 
 
14
  class LinuxVM:
15
  """Manage a lightweight Linux VM using Docker."""
16
 
17
+ def __init__(
18
+ self, image: str = "ubuntu:latest", host_dir: str = UPLOAD_DIR
19
+ ) -> None:
20
  self._image = image
21
  self._name = f"chat-vm-{uuid.uuid4().hex[:8]}"
22
  self._running = False
23
+ self._host_dir = Path(host_dir)
24
+ self._host_dir.mkdir(parents=True, exist_ok=True)
25
 
26
  def start(self) -> None:
27
  """Start the VM if it is not already running."""
 
43
  "-d",
44
  "--name",
45
  self._name,
46
+ "-v",
47
+ f"{self._host_dir}:/data",
48
  self._image,
49
  "sleep",
50
  "infinity",