ndurner commited on
Commit
a607daa
·
1 Parent(s): be5421b

add Python-Use

Browse files
Files changed (3) hide show
  1. app.py +130 -26
  2. code_exec.py +243 -0
  3. requirements.txt +2 -1
app.py CHANGED
@@ -9,6 +9,7 @@ import io
9
  from settings_mgr import generate_download_settings_js, generate_upload_settings_js
10
 
11
  from doc2json import process_docx
 
12
 
13
  dump_controls = False
14
  log_to_console = False
@@ -143,7 +144,7 @@ def process_values_js():
143
  }
144
  """
145
 
146
- def bot(message, history, oai_key, system_prompt, seed, temperature, max_tokens, model):
147
  try:
148
  client = OpenAI(
149
  api_key=oai_key
@@ -192,6 +193,31 @@ def bot(message, history, oai_key, system_prompt, seed, temperature, max_tokens,
192
  if seed:
193
  seed_i = int(seed)
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  if log_to_console:
196
  print(f"bot history: {str(history)}")
197
 
@@ -242,30 +268,107 @@ def bot(message, history, oai_key, system_prompt, seed, temperature, max_tokens,
242
  if log_to_console:
243
  print(f"usage: {response.usage}")
244
  else:
245
- response = client.chat.completions.create(
246
- model=model,
247
- messages= history_openai_format,
248
- temperature=temperature,
249
- seed=seed_i,
250
- max_tokens=max_tokens,
251
- stream=True,
252
- stream_options={"include_usage": True}
253
- )
254
-
255
- partial_response=""
256
- for chunk in response:
257
- if chunk.choices:
258
- txt = ""
259
- for choice in chunk.choices:
260
- cont = choice.delta.content
261
- if cont:
262
- txt += cont
263
-
264
- partial_response += txt
265
- yield partial_response
266
-
267
- if chunk.usage and log_to_console:
268
- print(f"usage: {chunk.usage}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  if log_to_console:
271
  print(f"br_result: {str(history)}")
@@ -311,6 +414,7 @@ with gr.Blocks(delete_cache=(86400, 86400)) as demo:
311
  seed = gr.Textbox(label="Seed", elem_id="seed")
312
  temp = gr.Slider(0, 2, label="Temperature", elem_id="temp", value=1)
313
  max_tokens = gr.Slider(1, 16384, label="Max. Tokens", elem_id="max_tokens", value=800)
 
314
  save_button = gr.Button("Save Settings")
315
  load_button = gr.Button("Load Settings")
316
  dl_settings_button = gr.Button("Download Settings")
@@ -345,7 +449,7 @@ with gr.Blocks(delete_cache=(86400, 86400)) as demo:
345
  ('temp', '#temp input'),
346
  ('max_tokens', '#max_tokens input'),
347
  ('model', '#model')]
348
- controls = [oai_key, system_prompt, seed, temp, max_tokens, model]
349
 
350
  dl_settings_button.click(None, controls, js=generate_download_settings_js("oai_chat_settings.bin", control_ids))
351
  ul_settings_button.click(None, None, None, js=generate_upload_settings_js(control_ids))
 
9
  from settings_mgr import generate_download_settings_js, generate_upload_settings_js
10
 
11
  from doc2json import process_docx
12
+ from code_exec import eval_restricted_script
13
 
14
  dump_controls = False
15
  log_to_console = False
 
144
  }
145
  """
146
 
147
+ def bot(message, history, oai_key, system_prompt, seed, temperature, max_tokens, model, python_use):
148
  try:
149
  client = OpenAI(
150
  api_key=oai_key
 
193
  if seed:
194
  seed_i = int(seed)
195
 
196
+ tools = None if not python_use else [
197
+ {
198
+ "type": "function",
199
+ "function": {
200
+ "name": "eval_python",
201
+ "description": "Evaluate a simple script written in a conservative, restricted subset of Python."
202
+ "Note: Augmented assignments, in-place operations (e.g., +=, -=), lambdas (e.g. list comprehensions) are not supported. "
203
+ "Use regular assignments and operations instead. Only 'import math' is allowed. "
204
+ "Returns: unquoted results without HTML encoding.",
205
+ "parameters": {
206
+ "type": "object",
207
+ "properties": {
208
+ "python_source_code": {
209
+ "type": "string",
210
+ "description": "The Python script that will run in a RestrictedPython context. "
211
+ "Avoid using augmented assignments or in-place operations (+=, -=, etc.), as well as lambdas (e.g. list comprehensions). "
212
+ "Use regular assignments and operations instead. Only 'import math' is allowed. Results need to be reported through print()."
213
+ }
214
+ },
215
+ "required": ["python_source_code"]
216
+ }
217
+ }
218
+ }
219
+ ]
220
+
221
  if log_to_console:
222
  print(f"bot history: {str(history)}")
223
 
 
268
  if log_to_console:
269
  print(f"usage: {response.usage}")
270
  else:
271
+ whole_response = ""
272
+ while True:
273
+ response = client.chat.completions.create(
274
+ model=model,
275
+ messages= history_openai_format,
276
+ temperature=temperature,
277
+ seed=seed_i,
278
+ max_tokens=max_tokens,
279
+ stream=True,
280
+ stream_options={"include_usage": True},
281
+ tools = tools,
282
+ tool_choice = "auto" if python_use else None
283
+ )
284
+
285
+ # Accumulators for partial model responses
286
+ tool_name_accum = None
287
+ tool_args_accum = ""
288
+ tool_call_id = None
289
+ # process
290
+ for chunk in response:
291
+ if chunk.choices:
292
+ txt = ""
293
+ for choice in chunk.choices:
294
+ delta = choice.delta
295
+ if not delta:
296
+ continue
297
+
298
+ cont = delta.content
299
+ if cont:
300
+ txt += cont
301
+
302
+ if delta.tool_calls:
303
+ for tc in delta.tool_calls:
304
+ if tc.function.name:
305
+ tool_name_accum = tc.function.name
306
+ if tc.function.arguments:
307
+ tool_args_accum += tc.function.arguments
308
+ if tc.id:
309
+ tool_call_id = tc.id
310
+
311
+ finish_reason = choice.finish_reason
312
+ if finish_reason:
313
+ if finish_reason == "tool_calls":
314
+ try:
315
+ parsed_args = json.loads(tool_args_accum)
316
+ tool_script = parsed_args.get("python_source_code", "")
317
+
318
+ whole_response += f"\n``` script\n{tool_script}\n```\n"
319
+ yield whole_response
320
+
321
+ tool_result = eval_restricted_script(tool_script)
322
+
323
+ whole_response += f"\n``` result\n{tool_result if not tool_result['success'] else tool_result['prints']}\n```\n"
324
+ yield whole_response
325
+
326
+ history_openai_format.extend([
327
+ {
328
+ "role": "assistant",
329
+ "content": txt,
330
+ "tool_calls": [
331
+ {
332
+ "id": tool_call_id,
333
+ "type": "function",
334
+ "function": {
335
+ "name": tool_name_accum,
336
+ "arguments": json.dumps(parsed_args)
337
+ }
338
+ }
339
+ ]
340
+ },
341
+ {
342
+ "role": "tool",
343
+ "tool_call_id": tool_call_id,
344
+ "name": tool_name_accum,
345
+ "content": json.dumps(tool_result)
346
+ }
347
+ ])
348
+
349
+ except Exception as e:
350
+ history_openai_format.extend([{
351
+ "role": "tool",
352
+ "tool_call_id": tool_call_id,
353
+ "name": tool_name_accum,
354
+ "content": [
355
+ {
356
+ "toolResult": {
357
+ "content": [{"text": e.args[0]}],
358
+ "status": 'error'
359
+ }
360
+ }
361
+ ]
362
+ }])
363
+ whole_response += f"\n``` error\n{e.args[0]}\n```\n"
364
+ yield whole_response
365
+ else:
366
+ return
367
+ else:
368
+ whole_response += txt
369
+ yield whole_response
370
+ if chunk.usage and log_to_console:
371
+ print(f"usage: {chunk.usage}")
372
 
373
  if log_to_console:
374
  print(f"br_result: {str(history)}")
 
414
  seed = gr.Textbox(label="Seed", elem_id="seed")
415
  temp = gr.Slider(0, 2, label="Temperature", elem_id="temp", value=1)
416
  max_tokens = gr.Slider(1, 16384, label="Max. Tokens", elem_id="max_tokens", value=800)
417
+ python_use = gr.Checkbox(label="Python Use", value=False)
418
  save_button = gr.Button("Save Settings")
419
  load_button = gr.Button("Load Settings")
420
  dl_settings_button = gr.Button("Download Settings")
 
449
  ('temp', '#temp input'),
450
  ('max_tokens', '#max_tokens input'),
451
  ('model', '#model')]
452
+ controls = [oai_key, system_prompt, seed, temp, max_tokens, model, python_use]
453
 
454
  dl_settings_button.click(None, controls, js=generate_download_settings_js("oai_chat_settings.bin", control_ids))
455
  ul_settings_button.click(None, None, None, js=generate_upload_settings_js(control_ids))
code_exec.py ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from RestrictedPython import compile_restricted
2
+ from RestrictedPython.PrintCollector import PrintCollector
3
+ from RestrictedPython.Guards import safe_globals, safe_builtins, guarded_iter_unpack_sequence
4
+ from RestrictedPython.Eval import default_guarded_getiter, default_guarded_getitem
5
+ from RestrictedPython.Utilities import utility_builtins
6
+ from RestrictedPython.Guards import guarded_unpack_sequence
7
+ from RestrictedPython.Limits import limited_range, limited_list, limited_tuple
8
+ from io import StringIO
9
+ import types
10
+
11
+ def eval_restricted_script(script):
12
+ # Set up print collector and output handling
13
+ all_prints = StringIO()
14
+
15
+ class CustomPrintCollector:
16
+ """Collect printed text, accumulating in shared StringIO"""
17
+
18
+ def __init__(self, _getattr_=None):
19
+ self.txt = []
20
+ self._getattr_ = _getattr_
21
+
22
+ def write(self, text):
23
+ all_prints.write(text)
24
+ self.txt.append(text)
25
+
26
+ def __call__(self):
27
+ result = ''.join(self.txt)
28
+ return result
29
+
30
+ def _call_print(self, *objects, **kwargs):
31
+ if kwargs.get('file', None) is None:
32
+ kwargs['file'] = self
33
+ else:
34
+ self._getattr_(kwargs['file'], 'write')
35
+
36
+ print(*objects, **kwargs)
37
+
38
+ # Create the restricted builtins dictionary
39
+ restricted_builtins = dict(safe_builtins)
40
+ restricted_builtins.update(utility_builtins) # Add safe __import__
41
+ restricted_builtins.update({
42
+ # Print handling
43
+ '_print_': CustomPrintCollector,
44
+ '_getattr_': getattr,
45
+ '_getitem_': default_guarded_getitem,
46
+ '_getiter_': default_guarded_getiter,
47
+ '_iter_unpack_sequence_': guarded_iter_unpack_sequence,
48
+ '_unpack_sequence_': guarded_unpack_sequence,
49
+ '_inplacevar_': protected_inplacevar,
50
+ '_apply_': _apply,
51
+ '_write_': _default_write_,
52
+
53
+ # Define allowed imports
54
+ '__allowed_modules__': ['math', 'datetime'],
55
+ '__import__': __import__,
56
+
57
+ # Basic functions
58
+ 'len': len,
59
+ 'range': limited_range,
60
+ 'enumerate': enumerate,
61
+ 'zip': zip,
62
+
63
+ # Math operations
64
+ 'sum': sum,
65
+ 'max': max,
66
+ 'min': min,
67
+ 'abs': abs,
68
+ 'round': round,
69
+ 'pow': pow,
70
+
71
+ # Type conversions
72
+ 'int': int,
73
+ 'float': float,
74
+ 'str': str,
75
+ 'bool': bool,
76
+ 'list': limited_list,
77
+ 'tuple': limited_tuple,
78
+ 'set': set,
79
+ 'dict': dict,
80
+ 'bytes': bytes,
81
+ 'bytearray': bytearray,
82
+
83
+ # Sequence operations
84
+ 'all': all,
85
+ 'any': any,
86
+ 'sorted': sorted,
87
+ 'reversed': reversed,
88
+
89
+ # String operations
90
+ 'chr': chr,
91
+ 'ord': ord,
92
+
93
+ # Other safe operations
94
+ 'isinstance': isinstance,
95
+ 'issubclass': issubclass,
96
+ 'hasattr': hasattr,
97
+ 'callable': callable,
98
+ 'format': format,
99
+ })
100
+
101
+ # Create the restricted globals dictionary
102
+ restricted_globals = dict(safe_globals)
103
+ restricted_globals['__builtins__'] = restricted_builtins
104
+
105
+ try:
106
+ byte_code = compile_restricted(script, filename='<inline>', mode='exec')
107
+ exec(byte_code, restricted_globals)
108
+
109
+ return {
110
+ 'prints': all_prints.getvalue(),
111
+ 'success': True
112
+ }
113
+ except Exception as e:
114
+ return {
115
+ 'error': str(e),
116
+ 'success': False
117
+ }
118
+
119
+ def _default_write_(obj):
120
+ if isinstance(obj, types.ModuleType):
121
+ raise ValueError("Modules are not allowed in to be written to.")
122
+
123
+ return obj
124
+
125
+ """
126
+ Borrowed implementation of _inplacevar_ from the Zope Foundations's AccessControl module
127
+ https://github.com/zopefoundation/AccessControl/blob/f9ae58816f0712eb6ea97459b4ccafbf4662d9db/src/AccessControl/ZopeGuards.py#L530
128
+ """
129
+
130
+ valid_inplace_types = (list, set)
131
+
132
+
133
+ inplace_slots = {
134
+ '+=': '__iadd__',
135
+ '-=': '__isub__',
136
+ '*=': '__imul__',
137
+ '/=': (1 / 2 == 0) and '__idiv__' or '__itruediv__',
138
+ '//=': '__ifloordiv__',
139
+ '%=': '__imod__',
140
+ '**=': '__ipow__',
141
+ '<<=': '__ilshift__',
142
+ '>>=': '__irshift__',
143
+ '&=': '__iand__',
144
+ '^=': '__ixor__',
145
+ '|=': '__ior__',
146
+ }
147
+
148
+
149
+ def __iadd__(x, y):
150
+ x += y
151
+ return x
152
+
153
+
154
+ def __isub__(x, y):
155
+ x -= y
156
+ return x
157
+
158
+
159
+ def __imul__(x, y):
160
+ x *= y
161
+ return x
162
+
163
+
164
+ def __idiv__(x, y):
165
+ x /= y
166
+ return x
167
+
168
+
169
+ def __ifloordiv__(x, y):
170
+ x //= y
171
+ return x
172
+
173
+
174
+ def __imod__(x, y):
175
+ x %= y
176
+ return x
177
+
178
+
179
+ def __ipow__(x, y):
180
+ x **= y
181
+ return x
182
+
183
+
184
+ def __ilshift__(x, y):
185
+ x <<= y
186
+ return x
187
+
188
+
189
+ def __irshift__(x, y):
190
+ x >>= y
191
+ return x
192
+
193
+
194
+ def __iand__(x, y):
195
+ x &= y
196
+ return x
197
+
198
+
199
+ def __ixor__(x, y):
200
+ x ^= y
201
+ return x
202
+
203
+
204
+ def __ior__(x, y):
205
+ x |= y
206
+ return x
207
+
208
+
209
+ inplace_ops = {
210
+ '+=': __iadd__,
211
+ '-=': __isub__,
212
+ '*=': __imul__,
213
+ '/=': __idiv__,
214
+ '//=': __ifloordiv__,
215
+ '%=': __imod__,
216
+ '**=': __ipow__,
217
+ '<<=': __ilshift__,
218
+ '>>=': __irshift__,
219
+ '&=': __iand__,
220
+ '^=': __ixor__,
221
+ '|=': __ior__,
222
+ }
223
+
224
+
225
+ def protected_inplacevar(op, var, expr):
226
+ """Do an inplace operation
227
+
228
+ If the var has an inplace slot, then disallow the operation
229
+ unless the var an instance of ``valid_inplace_types``.
230
+ """
231
+ if hasattr(var, inplace_slots[op]) and \
232
+ not isinstance(var, valid_inplace_types):
233
+ try:
234
+ cls = var.__class__
235
+ except AttributeError:
236
+ cls = type(var)
237
+ raise TypeError(
238
+ "Augmented assignment to %s objects is not allowed"
239
+ " in untrusted code" % cls.__name__)
240
+ return inplace_ops[op](var, expr)
241
+
242
+ def _apply(f, *a, **kw):
243
+ return f(*a, **kw)
requirements.txt CHANGED
@@ -1,4 +1,5 @@
1
  gradio == 5.1
2
  openai >= 1.0.0
3
  lxml
4
- PyMuPDF
 
 
1
  gradio == 5.1
2
  openai >= 1.0.0
3
  lxml
4
+ PyMuPDF
5
+ RestrictedPython