npcpy 1.2.35__py3-none-any.whl → 1.2.37__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- npcpy/__init__.py +10 -2
- npcpy/gen/image_gen.py +5 -2
- npcpy/gen/response.py +262 -64
- npcpy/llm_funcs.py +478 -832
- npcpy/ml_funcs.py +746 -0
- npcpy/npc_array.py +1294 -0
- npcpy/npc_compiler.py +348 -252
- npcpy/npc_sysenv.py +17 -2
- npcpy/serve.py +684 -90
- npcpy/sql/npcsql.py +96 -59
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/METADATA +173 -1
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/RECORD +15 -13
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/WHEEL +0 -0
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.2.35.dist-info → npcpy-1.2.37.dist-info}/top_level.txt +0 -0
npcpy/llm_funcs.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from jinja2 import Environment, FileSystemLoader, Undefined
|
|
2
2
|
import json
|
|
3
|
+
import os
|
|
3
4
|
import PIL
|
|
4
|
-
import random
|
|
5
|
+
import random
|
|
5
6
|
import subprocess
|
|
7
|
+
import copy
|
|
8
|
+
import itertools
|
|
6
9
|
from typing import List, Dict, Any, Optional, Union
|
|
7
10
|
from npcpy.npc_sysenv import (
|
|
8
11
|
print_and_process_stream_with_markdown,
|
|
@@ -28,6 +31,7 @@ def gen_image(
|
|
|
28
31
|
input_images: List[Union[str, bytes, PIL.Image.Image]] = None,
|
|
29
32
|
save = False,
|
|
30
33
|
filename = '',
|
|
34
|
+
api_key: str = None,
|
|
31
35
|
):
|
|
32
36
|
"""This function generates an image using the specified provider and model.
|
|
33
37
|
Args:
|
|
@@ -37,6 +41,7 @@ def gen_image(
|
|
|
37
41
|
provider (str): The provider to use for generating the image.
|
|
38
42
|
filename (str): The filename to save the image to.
|
|
39
43
|
npc (Any): The NPC object.
|
|
44
|
+
api_key (str): The API key for the image generation service.
|
|
40
45
|
Returns:
|
|
41
46
|
str: The filename of the saved image.
|
|
42
47
|
"""
|
|
@@ -60,6 +65,7 @@ def gen_image(
|
|
|
60
65
|
width=width,
|
|
61
66
|
attachments=input_images,
|
|
62
67
|
n_images=n_images,
|
|
68
|
+
api_key=api_key,
|
|
63
69
|
|
|
64
70
|
)
|
|
65
71
|
if save:
|
|
@@ -152,10 +158,12 @@ def get_llm_response(
|
|
|
152
158
|
messages: List[Dict[str, str]] = None,
|
|
153
159
|
api_url: str = None,
|
|
154
160
|
api_key: str = None,
|
|
155
|
-
context=None,
|
|
161
|
+
context=None,
|
|
156
162
|
stream: bool = False,
|
|
157
163
|
attachments: List[str] = None,
|
|
158
164
|
include_usage: bool = False,
|
|
165
|
+
n_samples: int = 1,
|
|
166
|
+
matrix: Optional[Dict[str, List[Any]]] = None,
|
|
159
167
|
**kwargs,
|
|
160
168
|
):
|
|
161
169
|
"""This function generates a response using the specified provider and model.
|
|
@@ -172,71 +180,132 @@ def get_llm_response(
|
|
|
172
180
|
Returns:
|
|
173
181
|
Any: The response generated by the specified provider and model.
|
|
174
182
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
api_url
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
183
|
+
import logging
|
|
184
|
+
logger = logging.getLogger("npcpy.llm_funcs")
|
|
185
|
+
logger.debug(f"[get_llm_response] Received {len(messages) if messages else 0} messages, prompt_len={len(prompt) if prompt else 0}")
|
|
186
|
+
|
|
187
|
+
def _resolve_model_provider(npc_override, team_override, model_override, provider_override):
|
|
188
|
+
m = model_override
|
|
189
|
+
p = provider_override
|
|
190
|
+
a_url = api_url
|
|
191
|
+
if m is not None and p is not None:
|
|
192
|
+
pass
|
|
193
|
+
elif p is None and m is not None:
|
|
194
|
+
p = lookup_provider(m)
|
|
195
|
+
elif npc_override is not None:
|
|
196
|
+
if npc_override.provider is not None:
|
|
197
|
+
p = npc_override.provider
|
|
198
|
+
if npc_override.model is not None:
|
|
199
|
+
m = npc_override.model
|
|
200
|
+
if npc_override.api_url is not None:
|
|
201
|
+
a_url = npc_override.api_url
|
|
202
|
+
elif team_override is not None:
|
|
203
|
+
if team_override.model is not None:
|
|
204
|
+
m = team_override.model
|
|
205
|
+
if team_override.provider is not None:
|
|
206
|
+
p = team_override.provider
|
|
207
|
+
if team_override.api_url is not None:
|
|
208
|
+
a_url = team_override.api_url
|
|
198
209
|
else:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
p = "ollama"
|
|
211
|
+
if images is not None or attachments is not None:
|
|
212
|
+
m = "llava:7b"
|
|
213
|
+
else:
|
|
214
|
+
m = "llama3.2"
|
|
215
|
+
return m, p, a_url
|
|
216
|
+
|
|
217
|
+
def _context_suffix(ctx):
|
|
218
|
+
if ctx is not None:
|
|
219
|
+
return f'User Provided Context: {ctx}'
|
|
220
|
+
return ''
|
|
221
|
+
|
|
222
|
+
def _build_messages(base_messages, sys_msg, prompt_text, ctx_suffix):
|
|
223
|
+
msgs = copy.deepcopy(base_messages) if base_messages else []
|
|
224
|
+
if not msgs:
|
|
225
|
+
msgs = [{"role": "system", "content": sys_msg}]
|
|
226
|
+
if prompt_text:
|
|
227
|
+
msgs.append({"role": "user", "content": prompt_text + ctx_suffix})
|
|
228
|
+
elif prompt_text and msgs[-1]["role"] == "user":
|
|
229
|
+
if isinstance(msgs[-1]["content"], str):
|
|
230
|
+
msgs[-1]["content"] += "\n" + prompt_text + ctx_suffix
|
|
231
|
+
else:
|
|
232
|
+
msgs.append({"role": "user", "content": prompt_text + ctx_suffix})
|
|
233
|
+
elif prompt_text:
|
|
234
|
+
msgs.append({"role": "user", "content": prompt_text + ctx_suffix})
|
|
235
|
+
return msgs
|
|
236
|
+
|
|
237
|
+
base_model, base_provider, base_api_url = _resolve_model_provider(npc, team, model, provider)
|
|
238
|
+
|
|
239
|
+
def _run_single(run_model, run_provider, run_npc, run_team, run_context, extra_kwargs):
|
|
240
|
+
system_message = get_system_message(run_npc, run_team) if run_npc is not None else "You are a helpful assistant."
|
|
241
|
+
ctx_suffix = _context_suffix(run_context)
|
|
242
|
+
run_messages = _build_messages(messages, system_message, prompt, ctx_suffix)
|
|
243
|
+
return get_litellm_response(
|
|
244
|
+
prompt + ctx_suffix,
|
|
245
|
+
messages=run_messages,
|
|
246
|
+
model=run_model,
|
|
247
|
+
provider=run_provider,
|
|
248
|
+
api_url=extra_kwargs.get("api_url", base_api_url),
|
|
249
|
+
api_key=api_key,
|
|
250
|
+
images=images,
|
|
251
|
+
attachments=attachments,
|
|
252
|
+
stream=stream,
|
|
253
|
+
include_usage=include_usage,
|
|
254
|
+
**{k: v for k, v in extra_kwargs.items() if k != "api_url"},
|
|
255
|
+
)
|
|
207
256
|
|
|
208
|
-
|
|
209
|
-
|
|
257
|
+
use_matrix = matrix is not None and len(matrix) > 0
|
|
258
|
+
multi_sample = n_samples and n_samples > 1
|
|
259
|
+
|
|
260
|
+
if not use_matrix and not multi_sample:
|
|
261
|
+
resolved_model, resolved_provider, resolved_api_url = _resolve_model_provider(npc, team, base_model, base_provider)
|
|
262
|
+
return _run_single(resolved_model, resolved_provider, npc, team, context, {"api_url": resolved_api_url, **kwargs})
|
|
263
|
+
|
|
264
|
+
combos = []
|
|
265
|
+
if use_matrix:
|
|
266
|
+
keys = list(matrix.keys())
|
|
267
|
+
values = []
|
|
268
|
+
for key in keys:
|
|
269
|
+
val = matrix[key]
|
|
270
|
+
if isinstance(val, (list, tuple, set)):
|
|
271
|
+
values.append(list(val))
|
|
272
|
+
else:
|
|
273
|
+
values.append([val])
|
|
274
|
+
for combo_values in itertools.product(*values):
|
|
275
|
+
combo = dict(zip(keys, combo_values))
|
|
276
|
+
combos.append(combo)
|
|
210
277
|
else:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
278
|
+
combos.append({})
|
|
279
|
+
|
|
280
|
+
runs = []
|
|
281
|
+
for combo in combos:
|
|
282
|
+
run_model = combo.get("model", base_model)
|
|
283
|
+
run_provider = combo.get("provider", base_provider)
|
|
284
|
+
run_npc = combo.get("npc", npc)
|
|
285
|
+
run_team = combo.get("team", team)
|
|
286
|
+
run_context = combo.get("context", context)
|
|
287
|
+
extra_kwargs = dict(kwargs)
|
|
288
|
+
for k, v in combo.items():
|
|
289
|
+
if k not in {"model", "provider", "npc", "context", "team"}:
|
|
290
|
+
extra_kwargs[k] = v
|
|
291
|
+
|
|
292
|
+
resolved_model, resolved_provider, resolved_api_url = _resolve_model_provider(run_npc, run_team, run_model, run_provider)
|
|
293
|
+
for sample_idx in range(max(1, n_samples or 1)):
|
|
294
|
+
resp = _run_single(resolved_model, resolved_provider, run_npc, run_team, run_context, {"api_url": resolved_api_url, **extra_kwargs})
|
|
295
|
+
runs.append({
|
|
296
|
+
"response": resp.get("response") if isinstance(resp, dict) else resp,
|
|
297
|
+
"raw": resp,
|
|
298
|
+
"combo": combo,
|
|
299
|
+
"sample_index": sample_idx,
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
aggregated = {
|
|
303
|
+
"response": runs[0]["response"] if runs else None,
|
|
304
|
+
"runs": runs,
|
|
305
|
+
}
|
|
306
|
+
if runs and isinstance(runs[0]["raw"], dict) and "messages" in runs[0]["raw"]:
|
|
307
|
+
aggregated["messages"] = runs[0]["raw"].get("messages")
|
|
308
|
+
return aggregated
|
|
240
309
|
|
|
241
310
|
|
|
242
311
|
|
|
@@ -380,259 +449,6 @@ def execute_llm_command(
|
|
|
380
449
|
"output": "Max attempts reached. Unable to execute the command successfully.",
|
|
381
450
|
}
|
|
382
451
|
|
|
383
|
-
# --- START OF CORRECTED handle_jinx_call ---
|
|
384
|
-
def handle_jinx_call(
|
|
385
|
-
command: str,
|
|
386
|
-
jinx_name: str,
|
|
387
|
-
model: str = None,
|
|
388
|
-
provider: str = None,
|
|
389
|
-
messages: List[Dict[str, str]] = None,
|
|
390
|
-
npc: Any = None,
|
|
391
|
-
team: Any = None,
|
|
392
|
-
stream=False,
|
|
393
|
-
n_attempts=3,
|
|
394
|
-
attempt=0,
|
|
395
|
-
context=None,
|
|
396
|
-
extra_globals=None,
|
|
397
|
-
**kwargs
|
|
398
|
-
) -> Union[str, Dict[str, Any]]:
|
|
399
|
-
"""This function handles a jinx call.
|
|
400
|
-
Args:
|
|
401
|
-
command (str): The command.
|
|
402
|
-
jinx_name (str): The jinx name.
|
|
403
|
-
Keyword Args:
|
|
404
|
-
model (str): The model to use for handling the jinx call.
|
|
405
|
-
provider (str): The provider to use for handling the jinx call.
|
|
406
|
-
messages (List[Dict[str, str]]): The list of messages.
|
|
407
|
-
npc (Any): The NPC object.
|
|
408
|
-
Returns:
|
|
409
|
-
Union[str, Dict[str, Any]]: The result of handling
|
|
410
|
-
the jinx call.
|
|
411
|
-
|
|
412
|
-
"""
|
|
413
|
-
if npc is None and team is None:
|
|
414
|
-
return f"No jinxs are available. "
|
|
415
|
-
else:
|
|
416
|
-
jinx = None
|
|
417
|
-
if npc and hasattr(npc, 'jinxs_dict') and jinx_name in npc.jinxs_dict:
|
|
418
|
-
jinx = npc.jinxs_dict[jinx_name]
|
|
419
|
-
elif team and hasattr(team, 'jinxs_dict') and jinx_name in team.jinxs_dict:
|
|
420
|
-
jinx = team.jinxs_dict[jinx_name]
|
|
421
|
-
|
|
422
|
-
if not jinx:
|
|
423
|
-
print(f"Jinx {jinx_name} not available")
|
|
424
|
-
if attempt < n_attempts:
|
|
425
|
-
print(f"attempt {attempt+1} to generate jinx name failed, trying again")
|
|
426
|
-
return check_llm_command(
|
|
427
|
-
f'''
|
|
428
|
-
In the previous attempt, the jinx name was: {jinx_name}.
|
|
429
|
-
|
|
430
|
-
That jinx was not available, only select those that are available.
|
|
431
|
-
|
|
432
|
-
If there are no available jinxs choose an alternative action. Do not invoke the jinx action.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
Here was the original command: BEGIN ORIGINAL COMMAND
|
|
436
|
-
'''+ command +' END ORIGINAL COMMAND',
|
|
437
|
-
model=model,
|
|
438
|
-
provider=provider,
|
|
439
|
-
messages=messages,
|
|
440
|
-
npc=npc,
|
|
441
|
-
team=team,
|
|
442
|
-
stream=stream,
|
|
443
|
-
context=context
|
|
444
|
-
)
|
|
445
|
-
return {
|
|
446
|
-
"output": f"Incorrect jinx name supplied and n_attempts reached.",
|
|
447
|
-
"messages": messages,
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
render_markdown(f"jinx found: {jinx.jinx_name}")
|
|
451
|
-
|
|
452
|
-
# This jinja_env is for parsing the Jinx's *inputs* from the LLM response, not for Jinx.execute's second pass.
|
|
453
|
-
local_jinja_env_for_input_parsing = Environment(loader=FileSystemLoader("."), undefined=Undefined)
|
|
454
|
-
|
|
455
|
-
example_format = {}
|
|
456
|
-
for inp in jinx.inputs:
|
|
457
|
-
if isinstance(inp, str):
|
|
458
|
-
example_format[inp] = f"<value for {inp}>"
|
|
459
|
-
elif isinstance(inp, dict):
|
|
460
|
-
key = list(inp.keys())[0]
|
|
461
|
-
example_format[key] = f"<value for {key}>"
|
|
462
|
-
|
|
463
|
-
json_format_str = json.dumps(example_format, indent=4)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
prompt = f"""
|
|
467
|
-
The user wants to use the jinx '{jinx_name}' with the following request:
|
|
468
|
-
'{command}'"""
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
prompt += f'Here were the previous 5 messages in the conversation: {messages[-5:]}'
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
prompt+=f"""Here is the jinx file:
|
|
475
|
-
```
|
|
476
|
-
{jinx.to_dict()}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
Please determine the required inputs for the jinx as a JSON object.
|
|
480
|
-
|
|
481
|
-
They must be exactly as they are named in the jinx.
|
|
482
|
-
For example, if the jinx has three inputs, you should respond with a list of three values that will pass for those args.
|
|
483
|
-
|
|
484
|
-
If the jinx requires a file path, you must include an absolute path to the file including an extension.
|
|
485
|
-
If the jinx requires code to be generated, you must generate it exactly according to the instructions.
|
|
486
|
-
Your inputs must satisfy the jinx's requirements.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
Return only the JSON object without any markdown formatting.
|
|
492
|
-
|
|
493
|
-
The format of the JSON object is:
|
|
494
|
-
|
|
495
|
-
"""+"{"+json_format_str+"}"
|
|
496
|
-
|
|
497
|
-
if npc and hasattr(npc, "shared_context"):
|
|
498
|
-
if npc.shared_context.get("dataframes"):
|
|
499
|
-
context_info = "\nAvailable dataframes:\n"
|
|
500
|
-
for df_name in npc.shared_context["dataframes"].keys():
|
|
501
|
-
context_info += f"- {df_name}\n"
|
|
502
|
-
prompt += f"""Here is contextual info that may affect your choice: {context_info}
|
|
503
|
-
"""
|
|
504
|
-
response = get_llm_response(
|
|
505
|
-
prompt,
|
|
506
|
-
format="json",
|
|
507
|
-
model=model,
|
|
508
|
-
provider=provider,
|
|
509
|
-
messages=messages[-10:],
|
|
510
|
-
npc=npc,
|
|
511
|
-
context=context
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
try:
|
|
515
|
-
response_text = response.get("response", "{}")
|
|
516
|
-
if isinstance(response_text, str):
|
|
517
|
-
response_text = (
|
|
518
|
-
response_text.replace("```json", "").replace("```", "").strip()
|
|
519
|
-
)
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if isinstance(response_text, dict):
|
|
523
|
-
input_values = response_text
|
|
524
|
-
else:
|
|
525
|
-
input_values = json.loads(response_text)
|
|
526
|
-
|
|
527
|
-
except json.JSONDecodeError as e:
|
|
528
|
-
print(f"Error decoding input values: {e}. Raw response: {response}")
|
|
529
|
-
return f"Error extracting inputs for jinx '{jinx_name}'"
|
|
530
|
-
|
|
531
|
-
required_inputs = jinx.inputs
|
|
532
|
-
missing_inputs = []
|
|
533
|
-
for inp in required_inputs:
|
|
534
|
-
if not isinstance(inp, dict):
|
|
535
|
-
|
|
536
|
-
if inp not in input_values or input_values[inp] == "":
|
|
537
|
-
missing_inputs.append(inp)
|
|
538
|
-
if len(missing_inputs) > 0:
|
|
539
|
-
|
|
540
|
-
if attempt < n_attempts:
|
|
541
|
-
print(f"attempt {attempt+1} to generate inputs failed, trying again")
|
|
542
|
-
print("missing inputs", missing_inputs)
|
|
543
|
-
print("llm response", response)
|
|
544
|
-
print("input values", input_values)
|
|
545
|
-
return handle_jinx_call(
|
|
546
|
-
command +' . In the previous attempt, the inputs were: ' + str(input_values) + ' and the missing inputs were: ' + str(missing_inputs) +' . Please ensure to not make this mistake again.',
|
|
547
|
-
jinx_name,
|
|
548
|
-
model=model,
|
|
549
|
-
provider=provider,
|
|
550
|
-
messages=messages,
|
|
551
|
-
npc=npc,
|
|
552
|
-
team=team,
|
|
553
|
-
stream=stream,
|
|
554
|
-
attempt=attempt + 1,
|
|
555
|
-
n_attempts=n_attempts,
|
|
556
|
-
context=context
|
|
557
|
-
)
|
|
558
|
-
return {
|
|
559
|
-
"output": f"Missing inputs for jinx '{jinx_name}': {missing_inputs}",
|
|
560
|
-
"messages": messages,
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
render_markdown( "\n".join(['\n - ' + str(key) + ': ' +str(val) for key, val in input_values.items()]))
|
|
565
|
-
|
|
566
|
-
# Initialize jinx_output before the try block to prevent UnboundLocalError
|
|
567
|
-
jinx_output = {"output": "Jinx execution did not complete."}
|
|
568
|
-
|
|
569
|
-
try:
|
|
570
|
-
# --- CRITICAL FIX HERE ---
|
|
571
|
-
# Pass arguments as keyword arguments to avoid positional confusion
|
|
572
|
-
# Use npc.jinja_env for the second-pass rendering
|
|
573
|
-
jinx_output = jinx.execute(
|
|
574
|
-
input_values=input_values,
|
|
575
|
-
npc=npc, # This is the orchestrating NPC
|
|
576
|
-
messages=messages,
|
|
577
|
-
extra_globals=extra_globals,
|
|
578
|
-
jinja_env=npc.jinja_env if npc else (team.forenpc.jinja_env if team and team.forenpc else None) # Use NPC's or Team's forenpc's jinja_env
|
|
579
|
-
)
|
|
580
|
-
# Ensure jinx_output is a dict with an 'output' key
|
|
581
|
-
if jinx_output is None:
|
|
582
|
-
jinx_output = {"output": "Jinx executed, but returned no explicit output."}
|
|
583
|
-
elif not isinstance(jinx_output, dict):
|
|
584
|
-
jinx_output = {"output": str(jinx_output)}
|
|
585
|
-
|
|
586
|
-
except Exception as e:
|
|
587
|
-
print(f"An error occurred while executing the jinx: {e}")
|
|
588
|
-
print(f"trying again, attempt {attempt+1}")
|
|
589
|
-
print('command', command)
|
|
590
|
-
if attempt < n_attempts:
|
|
591
|
-
# Recursively call handle_jinx_call for retry
|
|
592
|
-
jinx_output = handle_jinx_call(
|
|
593
|
-
command,
|
|
594
|
-
jinx_name,
|
|
595
|
-
model=model,
|
|
596
|
-
provider=provider,
|
|
597
|
-
messages=messages,
|
|
598
|
-
npc=npc,
|
|
599
|
-
team=team,
|
|
600
|
-
stream=stream,
|
|
601
|
-
attempt=attempt + 1,
|
|
602
|
-
n_attempts=n_attempts,
|
|
603
|
-
context=f""" \n \n \n "jinx failed: {e} \n \n \n here was the previous attempt: {input_values}""",
|
|
604
|
-
extra_globals=extra_globals
|
|
605
|
-
)
|
|
606
|
-
else:
|
|
607
|
-
# If max attempts reached, set a clear error output
|
|
608
|
-
jinx_output = {"output": f"Jinx '{jinx_name}' failed after {n_attempts} attempts: {e}", "error": True}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if not stream and len(messages) > 0 :
|
|
612
|
-
render_markdown(f""" ## jinx OUTPUT FROM CALLING {jinx_name} \n \n output:{jinx_output.get('output', 'No output.')}""" )
|
|
613
|
-
response = get_llm_response(f"""
|
|
614
|
-
The user had the following request: {command}.
|
|
615
|
-
Here were the jinx outputs from calling {jinx_name}: {jinx_output.get('output', '')}
|
|
616
|
-
|
|
617
|
-
Given the jinx outputs and the user request, please format a simple answer that
|
|
618
|
-
provides the answer without requiring the user to carry out any further steps.
|
|
619
|
-
""",
|
|
620
|
-
model=model,
|
|
621
|
-
provider=provider,
|
|
622
|
-
npc=npc,
|
|
623
|
-
messages=messages[-10:],
|
|
624
|
-
context=context,
|
|
625
|
-
stream=stream,
|
|
626
|
-
)
|
|
627
|
-
messages = response['messages']
|
|
628
|
-
response = response.get("response", {})
|
|
629
|
-
return {'messages':messages, 'output':response}
|
|
630
|
-
|
|
631
|
-
return {'messages': messages, 'output': jinx_output.get('output', 'No output.')} # Ensure 'output' key exists
|
|
632
|
-
|
|
633
|
-
# --- END OF CORRECTED handle_jinx_call ---
|
|
634
|
-
|
|
635
|
-
|
|
636
452
|
def handle_request_input(
|
|
637
453
|
context: str,
|
|
638
454
|
model: str ,
|
|
@@ -677,53 +493,61 @@ def handle_request_input(
|
|
|
677
493
|
|
|
678
494
|
|
|
679
495
|
|
|
680
|
-
def
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
496
|
+
def _get_jinxs(npc, team):
|
|
497
|
+
"""Get available jinxs from npc and team."""
|
|
498
|
+
jinxs = {}
|
|
499
|
+
if npc and hasattr(npc, 'jinxs_dict'):
|
|
500
|
+
jinxs.update(npc.jinxs_dict)
|
|
501
|
+
if team and hasattr(team, 'jinxs_dict'):
|
|
502
|
+
jinxs.update(team.jinxs_dict)
|
|
503
|
+
return jinxs
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _jinxs_to_tools(jinxs):
|
|
507
|
+
"""Convert jinxs to OpenAI-style tool definitions."""
|
|
508
|
+
return [jinx.to_tool_def() for jinx in jinxs.values()]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _execute_jinx(jinx, inputs, npc, team, messages, extra_globals):
|
|
512
|
+
"""Execute a jinx and return output."""
|
|
513
|
+
try:
|
|
514
|
+
jinja_env = None
|
|
515
|
+
if npc and hasattr(npc, 'jinja_env'):
|
|
516
|
+
jinja_env = npc.jinja_env
|
|
517
|
+
elif team and hasattr(team, 'forenpc') and team.forenpc:
|
|
518
|
+
jinja_env = getattr(team.forenpc, 'jinja_env', None)
|
|
519
|
+
|
|
520
|
+
# Merge with default values from jinx definition
|
|
521
|
+
full_inputs = {}
|
|
522
|
+
for inp in getattr(jinx, 'inputs', []):
|
|
523
|
+
if isinstance(inp, dict):
|
|
524
|
+
key = list(inp.keys())[0]
|
|
525
|
+
default_val = inp[key]
|
|
526
|
+
# Expand paths
|
|
527
|
+
if isinstance(default_val, str) and default_val.startswith("~"):
|
|
528
|
+
default_val = os.path.expanduser(default_val)
|
|
529
|
+
full_inputs[key] = default_val
|
|
530
|
+
else:
|
|
531
|
+
full_inputs[inp] = ""
|
|
532
|
+
# Override with provided inputs
|
|
533
|
+
full_inputs.update(inputs or {})
|
|
534
|
+
|
|
535
|
+
result = jinx.execute(
|
|
536
|
+
input_values=full_inputs,
|
|
537
|
+
npc=npc,
|
|
538
|
+
messages=messages,
|
|
539
|
+
extra_globals=extra_globals,
|
|
540
|
+
jinja_env=jinja_env
|
|
541
|
+
)
|
|
542
|
+
if result is None:
|
|
543
|
+
return "Executed with no output."
|
|
544
|
+
if not isinstance(result, dict):
|
|
545
|
+
return str(result)
|
|
546
|
+
return result.get("output", str(result))
|
|
547
|
+
except Exception as e:
|
|
548
|
+
return f"Error: {e}"
|
|
695
549
|
|
|
696
|
-
def answer_handler(command, extracted_data, **kwargs):
|
|
697
550
|
|
|
698
|
-
response = get_llm_response(
|
|
699
|
-
f"""
|
|
700
|
-
|
|
701
|
-
Here is the user question: {command}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
Do not needlessly reference the user's files or provided context.
|
|
705
|
-
|
|
706
|
-
Simply provide the answer to the user's question. Avoid
|
|
707
|
-
appearing zany or unnecessarily forthcoming about the fact that you have received such information. You know it
|
|
708
|
-
and the user knows it. there is no need to constantly mention the facts that are aware to both.
|
|
709
|
-
|
|
710
|
-
Your previous commnets on this topic: {extracted_data.get('explanation', '')}
|
|
711
|
-
|
|
712
|
-
""",
|
|
713
|
-
model=kwargs.get('model'),
|
|
714
|
-
provider=kwargs.get('provider'),
|
|
715
|
-
api_url=kwargs.get('api_url'),
|
|
716
|
-
api_key=kwargs.get('api_key'),
|
|
717
|
-
messages=kwargs.get('messages',)[-10:],
|
|
718
|
-
npc=kwargs.get('npc'),
|
|
719
|
-
team=kwargs.get('team'),
|
|
720
|
-
stream=kwargs.get('stream', False),
|
|
721
|
-
images=kwargs.get('images'),
|
|
722
|
-
context=kwargs.get('context')
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
return response
|
|
726
|
-
|
|
727
551
|
def check_llm_command(
|
|
728
552
|
command: str,
|
|
729
553
|
model: str = None,
|
|
@@ -736,510 +560,332 @@ def check_llm_command(
|
|
|
736
560
|
images: list = None,
|
|
737
561
|
stream=False,
|
|
738
562
|
context=None,
|
|
739
|
-
actions: Dict[str, Dict] = None,
|
|
563
|
+
actions: Dict[str, Dict] = None, # backwards compat, ignored
|
|
740
564
|
extra_globals=None,
|
|
565
|
+
max_iterations: int = 5,
|
|
566
|
+
jinxs: Dict = None,
|
|
741
567
|
):
|
|
742
|
-
"""
|
|
568
|
+
"""
|
|
569
|
+
Simple agent loop: try tool calling first, fall back to ReAct if unsupported.
|
|
570
|
+
"""
|
|
743
571
|
if messages is None:
|
|
744
572
|
messages = []
|
|
745
573
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
)
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
"\n".join(
|
|
783
|
-
f"- {name}: {jinx.description}"
|
|
784
|
-
for name, jinx in getattr(npc, "jinxs_dict", {}).items()
|
|
785
|
-
)
|
|
786
|
-
if getattr(npc, "jinxs_dict", None)
|
|
787
|
-
else ''
|
|
788
|
-
)
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
team_jinxs = "\n\nTeam Jinxs:\n" + (
|
|
792
|
-
"\n".join(
|
|
793
|
-
f"- {name}: {jinx.description}"
|
|
794
|
-
for name, jinx in getattr(team, "jinxs_dict", {}).items()
|
|
574
|
+
# Log incoming messages
|
|
575
|
+
import logging
|
|
576
|
+
logger = logging.getLogger("npcpy.llm_funcs")
|
|
577
|
+
logger.debug(f"[check_llm_command] Received {len(messages)} messages")
|
|
578
|
+
for i, msg in enumerate(messages[-5:]): # Log last 5 messages
|
|
579
|
+
role = msg.get('role', 'unknown')
|
|
580
|
+
content = msg.get('content', '')
|
|
581
|
+
content_preview = content[:100] if isinstance(content, str) else str(type(content))
|
|
582
|
+
logger.debug(f" [{i}] role={role}, content_preview={content_preview}...")
|
|
583
|
+
|
|
584
|
+
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
585
|
+
# Use provided jinxs or get from npc/team
|
|
586
|
+
if jinxs is None:
|
|
587
|
+
jinxs = _get_jinxs(npc, team)
|
|
588
|
+
tools = _jinxs_to_tools(jinxs) if jinxs else None
|
|
589
|
+
|
|
590
|
+
# Keep full message history, only truncate for API calls to reduce tokens
|
|
591
|
+
full_messages = messages.copy() if messages else []
|
|
592
|
+
logger.debug(f"[check_llm_command] full_messages initialized with {len(full_messages)} messages")
|
|
593
|
+
|
|
594
|
+
# Try with native tool calling first
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
response = get_llm_response(
|
|
598
|
+
command,
|
|
599
|
+
model=model,
|
|
600
|
+
provider=provider,
|
|
601
|
+
api_url=api_url,
|
|
602
|
+
api_key=api_key,
|
|
603
|
+
messages=messages[-10:], # Truncate for API call only
|
|
604
|
+
npc=npc,
|
|
605
|
+
team=team,
|
|
606
|
+
images=images,
|
|
607
|
+
stream=stream,
|
|
608
|
+
context=context,
|
|
609
|
+
tools=tools,
|
|
795
610
|
)
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
- If you are asked about something up-to-date or dynamic (e.g., latest exchange rates)
|
|
805
|
-
- If the user asks you to read or edit a file
|
|
806
|
-
- If the user asks for code that should be executed
|
|
807
|
-
- If the user requests to open, search, download or scrape, which involve actual system or web actions
|
|
808
|
-
- If they request a screenshot, audio, or image manipulation
|
|
809
|
-
- Situations requiring file parsing (e.g., CSV or JSON loading)
|
|
810
|
-
- Scripted workflows or pipelines, e.g., generate a chart, fetch data, summarize from source, etc.
|
|
811
|
-
|
|
812
|
-
You MUST use a jinx if the request directly refers to a tool the AI cannot handle directly (e.g., 'run', 'open', 'search', etc).
|
|
813
|
-
|
|
814
|
-
You must NOT use a jinx if:
|
|
815
|
-
- The user asks you to write them a story (unless they specify saving it to a file)
|
|
816
|
-
- To answer simple questions
|
|
817
|
-
- To determine general information that does not require up-to-date details
|
|
818
|
-
- To answer questions that can be answered with existing knowledge
|
|
819
|
-
|
|
820
|
-
To invoke a jinx, return the action 'invoke_jinx' along with the jinx specific name.
|
|
821
|
-
An example for a jinx-specific return would be:
|
|
822
|
-
{
|
|
823
|
-
"action": "invoke_jinx",
|
|
824
|
-
"jinx_name": "file_reader",
|
|
825
|
-
"explanation": "Read the contents of <full_filename_path_from_user_request> and <detailed explanation of how to accomplish the problem outlined in the request>."
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
Do not use the jinx names as the action keys. You must use the action 'invoke_jinx' to invoke a jinx!
|
|
829
|
-
Do not invent jinx names. Use only those provided.
|
|
830
|
-
|
|
831
|
-
Here are the currently available jinxs:"""
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if not npc_jinxs and not team_jinxs:
|
|
837
|
-
return "No jinxs are available."
|
|
838
|
-
else:
|
|
839
|
-
output = usage_guidelines
|
|
840
|
-
if npc_jinxs:
|
|
841
|
-
output += npc_jinxs
|
|
842
|
-
if team_jinxs:
|
|
843
|
-
output += team_jinxs
|
|
844
|
-
return output
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
DEFAULT_ACTION_SPACE = {
|
|
849
|
-
"invoke_jinx": {
|
|
850
|
-
"description": "Invoke a jinx (jinja-template execution script)",
|
|
851
|
-
"handler": jinx_handler,
|
|
852
|
-
"context": lambda npc=None, team=None, **_: jinx_context_filler(npc, team),
|
|
853
|
-
"output_keys": {
|
|
854
|
-
"jinx_name": {
|
|
855
|
-
"description": "The name of the jinx to invoke. must be from the provided list verbatim",
|
|
856
|
-
"type": "string"
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
},
|
|
860
|
-
"answer": {
|
|
861
|
-
"description": "Provide a direct informative answer",
|
|
862
|
-
"handler": answer_handler,
|
|
863
|
-
"context": """For general questions, use existing knowledge. For most queries a single action to answer a question will be sufficient.
|
|
864
|
-
e.g.
|
|
865
|
-
|
|
866
|
-
{
|
|
867
|
-
"actions": [
|
|
868
|
-
{
|
|
869
|
-
"action": "answer",
|
|
870
|
-
"explanation": "Provide a direct answer to the user's question based on existing knowledge."
|
|
871
|
-
|
|
872
|
-
|
|
611
|
+
except Exception as e:
|
|
612
|
+
print(colored(f"[check_llm_command] EXCEPTION in get_llm_response: {type(e).__name__}: {e}", "red"))
|
|
613
|
+
return {
|
|
614
|
+
"messages": full_messages,
|
|
615
|
+
"output": f"LLM call failed: {e}",
|
|
616
|
+
"error": str(e),
|
|
617
|
+
"usage": total_usage,
|
|
873
618
|
}
|
|
874
|
-
]
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
This should be preferred for more than half of requests. Do not overcomplicate the process.
|
|
878
|
-
Starting dialogue is usually more useful than using tools willynilly. Think carefully about
|
|
879
|
-
the user's intent and use this action as an opportunity to clear up potential ambiguities before
|
|
880
|
-
proceeding to more complex actions.
|
|
881
|
-
For example, if a user requests to write a story,
|
|
882
|
-
it is better to respond with 'answer' and to write them a story rather than to invoke some tool.
|
|
883
|
-
Indeed, it might be even better to respond and to request clarification about what other elements they would liek to specify with the story.
|
|
884
|
-
Natural language is highly ambiguous and it is important to establish common ground and priorities before proceeding to more complex actions.
|
|
885
|
-
|
|
886
|
-
""",
|
|
887
|
-
"output_keys": {}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
def plan_multi_step_actions(
|
|
891
|
-
command: str,
|
|
892
|
-
actions: Dict[str, Dict],
|
|
893
|
-
npc: Any = None,
|
|
894
|
-
team: Any = None,
|
|
895
|
-
model: str = None,
|
|
896
|
-
provider: str = None,
|
|
897
|
-
api_url: str = None,
|
|
898
|
-
api_key: str = None,
|
|
899
|
-
context: str = None,
|
|
900
|
-
messages: List[Dict[str, str]] = None,
|
|
901
|
-
|
|
902
619
|
|
|
903
|
-
):
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
620
|
+
if response.get("error"):
|
|
621
|
+
logger.warning(f"[check_llm_command] Error in response: {response.get('error')}")
|
|
622
|
+
|
|
623
|
+
if response.get("usage"):
|
|
624
|
+
total_usage["input_tokens"] += response["usage"].get("input_tokens", 0)
|
|
625
|
+
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
626
|
+
|
|
627
|
+
# Check if tool calls were made
|
|
628
|
+
tool_calls = response.get("tool_calls", [])
|
|
629
|
+
if not tool_calls:
|
|
630
|
+
# Direct answer - append user message to full messages
|
|
631
|
+
full_messages.append({"role": "user", "content": command})
|
|
632
|
+
assistant_response = response.get("response", "")
|
|
633
|
+
# Only append assistant message if it's a string (not a stream)
|
|
634
|
+
# For streaming, the caller (process_result) handles appending after consumption
|
|
635
|
+
if assistant_response and isinstance(assistant_response, str):
|
|
636
|
+
full_messages.append({"role": "assistant", "content": assistant_response})
|
|
637
|
+
logger.debug(f"[check_llm_command] No tool calls - returning {len(full_messages)} messages")
|
|
638
|
+
return {
|
|
639
|
+
"messages": full_messages,
|
|
640
|
+
"output": assistant_response,
|
|
641
|
+
"usage": total_usage,
|
|
642
|
+
}
|
|
912
643
|
|
|
913
|
-
|
|
914
|
-
|
|
644
|
+
# Helper to serialize tool_calls for message history
|
|
645
|
+
def _serialize_tool_calls(tcs):
|
|
646
|
+
"""Convert tool_call objects to dicts for message history."""
|
|
647
|
+
serialized = []
|
|
648
|
+
for tc in tcs:
|
|
649
|
+
if hasattr(tc, 'id'):
|
|
650
|
+
# It's an object, convert to dict
|
|
651
|
+
func = tc.function
|
|
652
|
+
serialized.append({
|
|
653
|
+
"id": tc.id,
|
|
654
|
+
"type": "function",
|
|
655
|
+
"function": {
|
|
656
|
+
"name": func.name if hasattr(func, 'name') else func.get('name'),
|
|
657
|
+
"arguments": func.arguments if hasattr(func, 'arguments') else func.get('arguments', '{}')
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
else:
|
|
661
|
+
# Already a dict
|
|
662
|
+
serialized.append(tc)
|
|
663
|
+
return serialized
|
|
664
|
+
|
|
665
|
+
# Execute tool calls in a loop - start with full messages
|
|
666
|
+
full_messages.append({"role": "user", "content": command})
|
|
667
|
+
# Add assistant message with tool_calls before executing them
|
|
668
|
+
assistant_msg = {"role": "assistant", "content": response.get("response", "")}
|
|
669
|
+
if tool_calls:
|
|
670
|
+
assistant_msg["tool_calls"] = _serialize_tool_calls(tool_calls)
|
|
671
|
+
full_messages.append(assistant_msg)
|
|
672
|
+
current_messages = full_messages
|
|
673
|
+
logger.debug(f"[check_llm_command] Tool calls detected - current_messages has {len(current_messages)} messages")
|
|
674
|
+
for iteration in range(max_iterations):
|
|
675
|
+
for tc in tool_calls:
|
|
676
|
+
# Handle both dict and object formats
|
|
677
|
+
if hasattr(tc, 'function'):
|
|
678
|
+
func = tc.function
|
|
679
|
+
jinx_name = func.name if hasattr(func, 'name') else func.get('name')
|
|
680
|
+
args_str = func.arguments if hasattr(func, 'arguments') else func.get('arguments', '{}')
|
|
681
|
+
tc_id = tc.id if hasattr(tc, 'id') else tc.get('id', '')
|
|
682
|
+
else:
|
|
683
|
+
func = tc.get("function", {})
|
|
684
|
+
jinx_name = func.get("name")
|
|
685
|
+
args_str = func.get("arguments", "{}")
|
|
686
|
+
tc_id = tc.get("id", "")
|
|
915
687
|
|
|
916
|
-
"""
|
|
917
|
-
if messages == None:
|
|
918
|
-
messages = list()
|
|
919
|
-
for action_name, action_info in actions.items():
|
|
920
|
-
ctx = action_info.get("context")
|
|
921
|
-
if callable(ctx):
|
|
922
688
|
try:
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
""
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
"""+f"""
|
|
966
|
-
Now, create the plan for the user's query: "{command}"
|
|
967
|
-
Respond ONLY with the plan.
|
|
968
|
-
"""
|
|
969
|
-
|
|
970
|
-
action_response = get_llm_response(
|
|
971
|
-
prompt,
|
|
972
|
-
model=model,
|
|
973
|
-
provider=provider,
|
|
974
|
-
api_url=api_url,
|
|
975
|
-
api_key=api_key,
|
|
976
|
-
npc=npc,
|
|
977
|
-
team=team,
|
|
978
|
-
format="json",
|
|
979
|
-
messages=[],
|
|
980
|
-
context=context,
|
|
981
|
-
)
|
|
982
|
-
response_content = action_response.get("response", {})
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return response_content.get("actions", [])
|
|
986
|
-
|
|
987
|
-
def execute_multi_step_plan(
|
|
988
|
-
command: str,
|
|
989
|
-
model: str = None,
|
|
990
|
-
provider: str = None,
|
|
991
|
-
api_url: str = None,
|
|
992
|
-
api_key: str = None,
|
|
993
|
-
npc: Any = None,
|
|
994
|
-
team: Any = None,
|
|
995
|
-
messages: List[Dict[str, str]] = None,
|
|
996
|
-
images: list = None,
|
|
997
|
-
stream=False,
|
|
998
|
-
context=None,
|
|
999
|
-
actions: Dict[str, Dict] = None,
|
|
1000
|
-
extra_globals=None,
|
|
1001
|
-
**kwargs,
|
|
1002
|
-
):
|
|
1003
|
-
"""
|
|
1004
|
-
Creates a comprehensive plan and executes it sequentially, passing context
|
|
1005
|
-
between steps for adaptive behavior.
|
|
1006
|
-
"""
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
planned_actions = plan_multi_step_actions(
|
|
1010
|
-
command=command,
|
|
1011
|
-
actions=actions,
|
|
1012
|
-
npc=npc,
|
|
1013
|
-
model=model,
|
|
1014
|
-
provider=provider,
|
|
1015
|
-
api_url=api_url,
|
|
1016
|
-
api_key=api_key,
|
|
1017
|
-
context=context,
|
|
1018
|
-
messages=messages,
|
|
1019
|
-
team=team,
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
)
|
|
1023
|
-
|
|
1024
|
-
if not planned_actions:
|
|
1025
|
-
print("Could not generate a multi-step plan. Answering directly.")
|
|
1026
|
-
result = answer_handler(command=command,
|
|
1027
|
-
extracted_data={"explanation": "Answering the user's query directly."},
|
|
1028
|
-
model=model,
|
|
1029
|
-
provider=provider,
|
|
1030
|
-
api_url=api_url,
|
|
1031
|
-
api_key=api_key,
|
|
1032
|
-
messages=messages,
|
|
1033
|
-
npc=npc,
|
|
1034
|
-
stream=stream,
|
|
1035
|
-
team = team,
|
|
1036
|
-
images=images,
|
|
1037
|
-
context=context
|
|
1038
|
-
)
|
|
1039
|
-
return {"messages": result.get('messages',
|
|
1040
|
-
messages),
|
|
1041
|
-
"output": result.get('response')}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
step_outputs = []
|
|
1045
|
-
current_messages = messages.copy()
|
|
1046
|
-
render_markdown(f"### Plan for Command: {command[:100]}")
|
|
1047
|
-
for action in planned_actions:
|
|
1048
|
-
step_info = json.dumps({'action': action.get('action', ''),
|
|
1049
|
-
'explanation': str(action.get('explanation',''))[0:10]+'...'})
|
|
1050
|
-
render_markdown(f'- {step_info}')
|
|
1051
|
-
|
|
689
|
+
inputs = json.loads(args_str) if isinstance(args_str, str) else args_str
|
|
690
|
+
except:
|
|
691
|
+
inputs = {}
|
|
692
|
+
|
|
693
|
+
if jinx_name in jinxs:
|
|
694
|
+
try:
|
|
695
|
+
from termcolor import colored
|
|
696
|
+
print(colored(f" ⚡ {jinx_name}", "cyan"), end="", flush=True)
|
|
697
|
+
except:
|
|
698
|
+
pass
|
|
699
|
+
output = _execute_jinx(jinxs[jinx_name], inputs, npc, team, current_messages, extra_globals)
|
|
700
|
+
try:
|
|
701
|
+
print(colored(" ✓", "green"), flush=True)
|
|
702
|
+
except:
|
|
703
|
+
pass
|
|
704
|
+
|
|
705
|
+
# Add tool result to messages
|
|
706
|
+
# Include name for Gemini compatibility
|
|
707
|
+
current_messages.append({
|
|
708
|
+
"role": "tool",
|
|
709
|
+
"tool_call_id": tc_id,
|
|
710
|
+
"name": jinx_name,
|
|
711
|
+
"content": str(output)
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
# Get next response - truncate carefully to not orphan tool responses
|
|
715
|
+
# Find a safe truncation point that doesn't split tool_call/tool_response pairs
|
|
716
|
+
truncated = current_messages
|
|
717
|
+
if len(current_messages) > 15:
|
|
718
|
+
# Start from -15 and walk back to find a user message or start
|
|
719
|
+
start_idx = len(current_messages) - 15
|
|
720
|
+
while start_idx > 0:
|
|
721
|
+
msg = current_messages[start_idx]
|
|
722
|
+
# Don't start on a tool response - would orphan it
|
|
723
|
+
if msg.get("role") == "tool":
|
|
724
|
+
start_idx -= 1
|
|
725
|
+
# Don't start on assistant with tool_calls unless next is tool response
|
|
726
|
+
elif msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
727
|
+
start_idx -= 1
|
|
728
|
+
else:
|
|
729
|
+
break
|
|
730
|
+
truncated = current_messages[start_idx:]
|
|
1052
731
|
|
|
1053
|
-
|
|
1054
|
-
for i, action_data in enumerate(planned_actions):
|
|
1055
|
-
render_markdown(f"--- Executing Step {i + 1} of {len(planned_actions)} ---")
|
|
1056
|
-
action_name = action_data["action"]
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
732
|
try:
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
step_context = f"Context from previous steps: {json.dumps(step_outputs)}" if step_outputs else ""
|
|
1065
|
-
render_markdown(
|
|
1066
|
-
f"- Executing Action: {action_name} \n- Explanation: {action_data.get('explanation')}\n "
|
|
1067
|
-
)
|
|
1068
|
-
|
|
1069
|
-
result = handler(
|
|
1070
|
-
command=command,
|
|
1071
|
-
extracted_data=action_data,
|
|
733
|
+
response = get_llm_response(
|
|
734
|
+
"", # continuation
|
|
1072
735
|
model=model,
|
|
1073
|
-
provider=provider,
|
|
736
|
+
provider=provider,
|
|
1074
737
|
api_url=api_url,
|
|
1075
|
-
api_key=api_key,
|
|
1076
|
-
messages=
|
|
738
|
+
api_key=api_key,
|
|
739
|
+
messages=truncated,
|
|
1077
740
|
npc=npc,
|
|
1078
741
|
team=team,
|
|
1079
|
-
stream=stream,
|
|
1080
|
-
context=context
|
|
1081
|
-
|
|
1082
|
-
extra_globals=extra_globals
|
|
1083
|
-
)
|
|
1084
|
-
except KeyError as e:
|
|
1085
|
-
|
|
1086
|
-
return execute_multi_step_plan(
|
|
1087
|
-
command=command + 'This error occurred: '+str(e)+'\n Do not make the same mistake again. If you are intending to use a jinx, you must `invoke_jinx`. If you just need to answer, choose `answer`.',
|
|
1088
|
-
model= model,
|
|
1089
|
-
provider = provider,
|
|
1090
|
-
api_url = api_url,
|
|
1091
|
-
api_key = api_key,
|
|
1092
|
-
npc = npc,
|
|
1093
|
-
team = team,
|
|
1094
|
-
messages = messages,
|
|
1095
|
-
images = images,
|
|
1096
|
-
stream=stream,
|
|
1097
|
-
context=context,
|
|
1098
|
-
actions=actions,
|
|
1099
|
-
|
|
1100
|
-
**kwargs,
|
|
742
|
+
stream=stream,
|
|
743
|
+
context=context,
|
|
744
|
+
tools=tools,
|
|
1101
745
|
)
|
|
746
|
+
except Exception as e:
|
|
747
|
+
# If continuation fails, return what we have so far
|
|
748
|
+
# The tool was already executed successfully
|
|
749
|
+
logger.warning(f"[check_llm_command] Continuation failed: {e}")
|
|
750
|
+
return {
|
|
751
|
+
"messages": current_messages,
|
|
752
|
+
"output": f"Tool executed successfully. (Continuation error: {type(e).__name__})",
|
|
753
|
+
"usage": total_usage,
|
|
754
|
+
}
|
|
1102
755
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
756
|
+
if response.get("usage"):
|
|
757
|
+
total_usage["input_tokens"] += response["usage"].get("input_tokens", 0)
|
|
758
|
+
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
759
|
+
|
|
760
|
+
tool_calls = response.get("tool_calls", [])
|
|
761
|
+
# Append assistant response to full messages with tool_calls if present
|
|
762
|
+
assistant_response = response.get("response", "")
|
|
763
|
+
assistant_msg = {"role": "assistant", "content": assistant_response}
|
|
764
|
+
if tool_calls:
|
|
765
|
+
assistant_msg["tool_calls"] = _serialize_tool_calls(tool_calls)
|
|
766
|
+
current_messages.append(assistant_msg)
|
|
767
|
+
|
|
768
|
+
if not tool_calls:
|
|
769
|
+
# Done - return full message history
|
|
770
|
+
logger.debug(f"[check_llm_command] Tool loop done - returning {len(current_messages)} messages")
|
|
771
|
+
return {
|
|
772
|
+
"messages": current_messages,
|
|
773
|
+
"output": assistant_response,
|
|
774
|
+
"usage": total_usage,
|
|
775
|
+
}
|
|
1117
776
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
provider=provider,
|
|
1125
|
-
npc=npc,
|
|
1126
|
-
stream=stream,
|
|
1127
|
-
context=context,
|
|
1128
|
-
**kwargs
|
|
1129
|
-
)
|
|
1130
|
-
|
|
1131
|
-
return {"messages": current_messages,
|
|
1132
|
-
"output": final_output}
|
|
1133
|
-
|
|
1134
|
-
def compile_sequence_results(original_command: str,
|
|
1135
|
-
outputs: List[str],
|
|
1136
|
-
model: str = None,
|
|
1137
|
-
provider: str = None,
|
|
1138
|
-
npc: Any = None,
|
|
1139
|
-
team: Any = None,
|
|
1140
|
-
context: str = None,
|
|
1141
|
-
stream: bool = False,
|
|
1142
|
-
**kwargs) -> str:
|
|
1143
|
-
"""
|
|
1144
|
-
Synthesizes a list of outputs from sequential steps into a single,
|
|
1145
|
-
coherent final response, framed as an answer to the original query.
|
|
1146
|
-
"""
|
|
1147
|
-
if not outputs:
|
|
1148
|
-
return "The process completed, but produced no output."
|
|
1149
|
-
synthesis_prompt = f"""
|
|
1150
|
-
A user asked the following question:
|
|
1151
|
-
"{original_command}"
|
|
777
|
+
logger.debug(f"[check_llm_command] Max iterations - returning {len(current_messages)} messages")
|
|
778
|
+
return {
|
|
779
|
+
"messages": current_messages,
|
|
780
|
+
"output": response.get("response", "Max iterations reached"),
|
|
781
|
+
"usage": total_usage,
|
|
782
|
+
}
|
|
1152
783
|
|
|
1153
|
-
To answer this, the following information was gathered in sequential steps:
|
|
1154
|
-
{json.dumps(outputs, indent=2)}
|
|
1155
784
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
785
|
+
def _react_fallback(
|
|
786
|
+
command, model, provider, api_url, api_key, npc, team,
|
|
787
|
+
messages, images, stream, context, jinxs, extra_globals, max_iterations
|
|
788
|
+
):
|
|
789
|
+
"""ReAct-style fallback for models without tool calling."""
|
|
790
|
+
import logging
|
|
791
|
+
logger = logging.getLogger("npcpy.llm_funcs")
|
|
792
|
+
logger.debug(f"[_react_fallback] Starting with {len(messages) if messages else 0} messages")
|
|
1159
793
|
|
|
1160
|
-
|
|
1161
|
-
|
|
794
|
+
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
795
|
+
current_messages = messages.copy() if messages else []
|
|
796
|
+
logger.debug(f"[_react_fallback] current_messages initialized with {len(current_messages)} messages")
|
|
1162
797
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
team=team,
|
|
1169
|
-
messages=[],
|
|
1170
|
-
stream=stream,
|
|
1171
|
-
context=context,
|
|
1172
|
-
**kwargs
|
|
1173
|
-
)
|
|
1174
|
-
synthesized = response.get("response", "")
|
|
1175
|
-
if synthesized:
|
|
1176
|
-
return synthesized
|
|
1177
|
-
return '\n'.join(outputs)
|
|
798
|
+
# Build jinx list with input parameters
|
|
799
|
+
def _jinx_info(name, jinx):
|
|
800
|
+
inputs = getattr(jinx, 'inputs', [])
|
|
801
|
+
input_names = [i if isinstance(i, str) else list(i.keys())[0] for i in inputs]
|
|
802
|
+
return f"- {name}({', '.join(input_names)}): {jinx.description}"
|
|
1178
803
|
|
|
804
|
+
jinx_list = "\n".join(_jinx_info(n, j) for n, j in jinxs.items()) if jinxs else "None"
|
|
1179
805
|
|
|
806
|
+
for iteration in range(max_iterations):
|
|
807
|
+
prompt = f"""Request: {command}
|
|
1180
808
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
completed_actions: List[Dict[str, Any]],
|
|
1184
|
-
current_messages: List[Dict[str, str]],
|
|
1185
|
-
model: str = None,
|
|
1186
|
-
provider: str = None,
|
|
1187
|
-
npc: Any = None,
|
|
1188
|
-
team: Any = None,
|
|
1189
|
-
context: str = None,
|
|
1190
|
-
**kwargs: Any
|
|
1191
|
-
|
|
1192
|
-
) -> Dict:
|
|
1193
|
-
"""Decide if more action sequences are needed."""
|
|
1194
|
-
|
|
1195
|
-
results_summary = ""
|
|
1196
|
-
for idx, action_result in enumerate(completed_actions):
|
|
1197
|
-
action_name = action_result.get("action", "Unknown Action")
|
|
1198
|
-
output = action_result.get('output', 'No Output')
|
|
1199
|
-
output_preview = output[:100] + "..." if isinstance(output, str) and len(output) > 100 else output
|
|
1200
|
-
results_summary += f"{idx + 1}. {action_name}: {output_preview}\n"
|
|
809
|
+
Tools:
|
|
810
|
+
{jinx_list}
|
|
1201
811
|
|
|
1202
|
-
|
|
1203
|
-
|
|
812
|
+
Return JSON: {{"action": "answer", "response": "..."}} OR {{"action": "jinx", "jinx_name": "...", "inputs": {{"param_name": "value"}}}}
|
|
813
|
+
Use EXACT parameter names from the tool definitions above."""
|
|
1204
814
|
|
|
1205
|
-
|
|
1206
|
-
|
|
815
|
+
if context:
|
|
816
|
+
prompt += f"\nContext: {context}"
|
|
1207
817
|
|
|
1208
|
-
|
|
1209
|
-
|
|
818
|
+
response = get_llm_response(
|
|
819
|
+
prompt,
|
|
820
|
+
model=model,
|
|
821
|
+
provider=provider,
|
|
822
|
+
api_url=api_url,
|
|
823
|
+
api_key=api_key,
|
|
824
|
+
messages=current_messages[-10:],
|
|
825
|
+
npc=npc,
|
|
826
|
+
team=team,
|
|
827
|
+
images=images if iteration == 0 else None,
|
|
828
|
+
format="json",
|
|
829
|
+
context=context,
|
|
830
|
+
)
|
|
1210
831
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
832
|
+
if response.get("usage"):
|
|
833
|
+
total_usage["input_tokens"] += response["usage"].get("input_tokens", 0)
|
|
834
|
+
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
1214
835
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
}
|
|
1221
|
-
|
|
836
|
+
decision = response.get("response", {})
|
|
837
|
+
if isinstance(decision, str):
|
|
838
|
+
try:
|
|
839
|
+
decision = json.loads(decision)
|
|
840
|
+
except:
|
|
841
|
+
return {"messages": current_messages, "output": decision, "usage": total_usage}
|
|
842
|
+
|
|
843
|
+
if decision.get("action") == "answer":
|
|
844
|
+
output = decision.get("response", "")
|
|
845
|
+
# Add user message to full history
|
|
846
|
+
current_messages.append({"role": "user", "content": command})
|
|
847
|
+
if stream:
|
|
848
|
+
# Use truncated for API, keep full history
|
|
849
|
+
final = get_llm_response(command, model=model, provider=provider, messages=current_messages[-10:], npc=npc, team=team, stream=True, context=context)
|
|
850
|
+
if final.get("usage"):
|
|
851
|
+
total_usage["input_tokens"] += final["usage"].get("input_tokens", 0)
|
|
852
|
+
total_usage["output_tokens"] += final["usage"].get("output_tokens", 0)
|
|
853
|
+
# Return full messages, not truncated from final
|
|
854
|
+
logger.debug(f"[_react_fallback] Answer (stream) - returning {len(current_messages)} messages")
|
|
855
|
+
return {"messages": current_messages, "output": final.get("response", output), "usage": total_usage}
|
|
856
|
+
# Non-streaming: add assistant response to full history
|
|
857
|
+
if output and isinstance(output, str):
|
|
858
|
+
current_messages.append({"role": "assistant", "content": output})
|
|
859
|
+
logger.debug(f"[_react_fallback] Answer - returning {len(current_messages)} messages")
|
|
860
|
+
return {"messages": current_messages, "output": output, "usage": total_usage}
|
|
861
|
+
|
|
862
|
+
elif decision.get("action") == "jinx":
|
|
863
|
+
jinx_name = decision.get("jinx_name")
|
|
864
|
+
inputs = decision.get("inputs", {})
|
|
865
|
+
|
|
866
|
+
if jinx_name not in jinxs:
|
|
867
|
+
context = f"Error: '{jinx_name}' not found. Available: {list(jinxs.keys())}"
|
|
868
|
+
continue
|
|
1222
869
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
response_dict = response.get("response", {})
|
|
1237
|
-
if not isinstance(response_dict, dict):
|
|
1238
|
-
return {"needs_more_actions": False, "reasoning": "Error", "next_focus": ""}
|
|
1239
|
-
|
|
1240
|
-
return response_dict
|
|
870
|
+
try:
|
|
871
|
+
from termcolor import colored
|
|
872
|
+
print(colored(f" ⚡ {jinx_name}", "cyan"), end="", flush=True)
|
|
873
|
+
except:
|
|
874
|
+
pass
|
|
875
|
+
output = _execute_jinx(jinxs[jinx_name], inputs, npc, team, current_messages, extra_globals)
|
|
876
|
+
try:
|
|
877
|
+
print(colored(" ✓", "green"), flush=True)
|
|
878
|
+
except:
|
|
879
|
+
pass
|
|
880
|
+
context = f"Tool '{jinx_name}' returned: {output}"
|
|
881
|
+
command = f"{command}\n\nPrevious: {context}"
|
|
1241
882
|
|
|
883
|
+
else:
|
|
884
|
+
logger.debug(f"[_react_fallback] Unknown action - returning {len(current_messages)} messages")
|
|
885
|
+
return {"messages": current_messages, "output": str(decision), "usage": total_usage}
|
|
1242
886
|
|
|
887
|
+
logger.debug(f"[_react_fallback] Max iterations - returning {len(current_messages)} messages")
|
|
888
|
+
return {"messages": current_messages, "output": f"Max iterations reached. Last: {context}", "usage": total_usage}
|
|
1243
889
|
|
|
1244
890
|
|
|
1245
891
|
|
|
@@ -2432,4 +2078,4 @@ def synthesize(
|
|
|
2432
2078
|
team=team,
|
|
2433
2079
|
context=context,
|
|
2434
2080
|
**kwargs
|
|
2435
|
-
)
|
|
2081
|
+
)
|