tech-envision commited on
Commit
7bd7366
·
1 Parent(s): 4f519ea

Allow limited imports in execute_python

Browse files
Files changed (5) hide show
  1. README.md +9 -1
  2. run.py +3 -1
  3. src/__init__.py +2 -2
  4. src/chat.py +24 -19
  5. src/tools.py +59 -1
README.md CHANGED
@@ -1,6 +1,14 @@
1
  # llm-backend
2
 
3
- This project provides a simple async interface to interact with an Ollama model and demonstrates basic tool usage. Chat histories are stored in a local SQLite database using Peewee. Histories are persisted per user and session so conversations can be resumed with context.
 
 
 
 
 
 
 
 
4
 
5
  ## Usage
6
 
 
1
  # llm-backend
2
 
3
+ This project provides a simple async interface to interact with an Ollama model
4
+ and demonstrates basic tool usage. Chat histories are stored in a local SQLite
5
+ database using Peewee. Histories are persisted per user and session so
6
+ conversations can be resumed with context. Two example tools are included:
7
+
8
+ * **add_two_numbers** – Adds two integers.
9
+ * **execute_python** – Executes Python code in a sandbox with selected built-ins
10
+ and allows importing safe modules like ``math``. The result is returned from a
11
+ ``result`` variable or captured output.
12
 
13
  ## Usage
14
 
run.py CHANGED
@@ -7,7 +7,9 @@ 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("What did you just say?")
 
 
11
  print("\n>>>", answer)
12
 
13
 
 
7
 
8
  async def _main() -> None:
9
  async with ChatSession(user="demo_user", session="demo_session") as chat:
10
+ answer = await chat.chat(
11
+ "Run this Python code: import math\nresult = math.factorial(5)"
12
+ )
13
  print("\n>>>", answer)
14
 
15
 
src/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
  from .chat import ChatSession
2
- from .tools import add_two_numbers
3
 
4
- __all__ = ["ChatSession", "add_two_numbers"]
 
1
  from .chat import ChatSession
2
+ from .tools import add_two_numbers, execute_python
3
 
4
+ __all__ = ["ChatSession", "add_two_numbers", "execute_python"]
src/chat.py CHANGED
@@ -9,7 +9,7 @@ from .config import MAX_TOOL_CALL_DEPTH, MODEL_NAME, OLLAMA_HOST
9
  from .db import Conversation, Message as DBMessage, User, _db, init_db
10
  from .log import get_logger
11
  from .schema import Msg
12
- from .tools import add_two_numbers
13
 
14
  _LOG = get_logger(__name__)
15
 
@@ -77,7 +77,7 @@ class ChatSession:
77
  self._model,
78
  messages=messages,
79
  think=think,
80
- tools=[add_two_numbers],
81
  )
82
 
83
  async def _handle_tool_calls(
@@ -93,23 +93,28 @@ class ChatSession:
93
  for call in response.message.tool_calls:
94
  if call.function.name == "add_two_numbers":
95
  result = add_two_numbers(**call.function.arguments)
96
- messages.append(
97
- {
98
- "role": "tool",
99
- "name": call.function.name,
100
- "content": str(result),
101
- }
102
- )
103
- DBMessage.create(
104
- conversation=conversation,
105
- role="tool",
106
- content=str(result),
107
- )
108
- nxt = await self.ask(messages, think=True)
109
- self._store_assistant_message(conversation, nxt.message)
110
- return await self._handle_tool_calls(
111
- messages, nxt, conversation, depth + 1
112
- )
 
 
 
 
 
113
 
114
  return response
115
 
 
9
  from .db import Conversation, Message as DBMessage, User, _db, init_db
10
  from .log import get_logger
11
  from .schema import Msg
12
+ from .tools import add_two_numbers, execute_python
13
 
14
  _LOG = get_logger(__name__)
15
 
 
77
  self._model,
78
  messages=messages,
79
  think=think,
80
+ tools=[add_two_numbers, execute_python],
81
  )
82
 
83
  async def _handle_tool_calls(
 
93
  for call in response.message.tool_calls:
94
  if call.function.name == "add_two_numbers":
95
  result = add_two_numbers(**call.function.arguments)
96
+ elif call.function.name == "execute_python":
97
+ result = execute_python(**call.function.arguments)
98
+ else:
99
+ continue
100
+
101
+ messages.append(
102
+ {
103
+ "role": "tool",
104
+ "name": call.function.name,
105
+ "content": str(result),
106
+ }
107
+ )
108
+ DBMessage.create(
109
+ conversation=conversation,
110
+ role="tool",
111
+ content=str(result),
112
+ )
113
+ nxt = await self.ask(messages, think=True)
114
+ self._store_assistant_message(conversation, nxt.message)
115
+ return await self._handle_tool_calls(
116
+ messages, nxt, conversation, depth + 1
117
+ )
118
 
119
  return response
120
 
src/tools.py CHANGED
@@ -1,6 +1,6 @@
1
  from __future__ import annotations
2
 
3
- __all__ = ["add_two_numbers"]
4
 
5
 
6
  def add_two_numbers(a: int, b: int) -> int: # noqa: D401
@@ -14,3 +14,61 @@ def add_two_numbers(a: int, b: int) -> int: # noqa: D401
14
  int: The sum of the two numbers.
15
  """
16
  return a + b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
 
3
+ __all__ = ["add_two_numbers", "execute_python"]
4
 
5
 
6
  def add_two_numbers(a: int, b: int) -> int: # noqa: D401
 
14
  int: The sum of the two numbers.
15
  """
16
  return a + b
17
+
18
+
19
+ def execute_python(code: str) -> str:
20
+ """Execute Python code in a sandbox with a broader set of built-ins.
21
+
22
+ The code is executed with restricted but useful built-ins and can import a
23
+ small whitelist of standard library modules. Results should be stored in a
24
+ variable named ``result`` or printed. The value of ``result`` is returned if
25
+ present; otherwise any standard output captured during execution is
26
+ returned.
27
+ """
28
+ import sys
29
+ from io import StringIO
30
+
31
+ allowed_modules = {"math", "random", "statistics"}
32
+
33
+ def _safe_import(name: str, globals=None, locals=None, fromlist=(), level=0):
34
+ if name in allowed_modules:
35
+ return __import__(name, globals, locals, fromlist, level)
36
+ raise ImportError(f"Import of '{name}' is not allowed")
37
+
38
+ allowed_builtins = {
39
+ "abs": abs,
40
+ "min": min,
41
+ "max": max,
42
+ "sum": sum,
43
+ "len": len,
44
+ "range": range,
45
+ "sorted": sorted,
46
+ "enumerate": enumerate,
47
+ "map": map,
48
+ "filter": filter,
49
+ "list": list,
50
+ "dict": dict,
51
+ "set": set,
52
+ "tuple": tuple,
53
+ "float": float,
54
+ "int": int,
55
+ "str": str,
56
+ "bool": bool,
57
+ "print": print,
58
+ "__import__": _safe_import,
59
+ }
60
+
61
+ safe_globals: dict[str, object] = {"__builtins__": allowed_builtins}
62
+ safe_locals: dict[str, object] = {}
63
+
64
+ stdout = StringIO()
65
+ original_stdout = sys.stdout
66
+ try:
67
+ sys.stdout = stdout
68
+ exec(code, safe_globals, safe_locals)
69
+ finally:
70
+ sys.stdout = original_stdout
71
+
72
+ if "result" in safe_locals:
73
+ return str(safe_locals["result"])
74
+ return stdout.getvalue().strip()