npcsh 0.3.30__py3-none-any.whl → 0.3.32__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.
Files changed (53) hide show
  1. npcsh/audio.py +540 -181
  2. npcsh/audio_gen.py +1 -0
  3. npcsh/cli.py +37 -19
  4. npcsh/conversation.py +14 -251
  5. npcsh/dataframes.py +13 -5
  6. npcsh/helpers.py +5 -0
  7. npcsh/image.py +2 -4
  8. npcsh/image_gen.py +38 -38
  9. npcsh/knowledge_graph.py +4 -4
  10. npcsh/llm_funcs.py +517 -349
  11. npcsh/npc_compiler.py +44 -23
  12. npcsh/npc_sysenv.py +5 -0
  13. npcsh/npc_team/npcsh.ctx +8 -2
  14. npcsh/npc_team/tools/generic_search.tool +9 -1
  15. npcsh/plonk.py +2 -2
  16. npcsh/response.py +131 -482
  17. npcsh/search.py +20 -9
  18. npcsh/serve.py +210 -203
  19. npcsh/shell.py +78 -80
  20. npcsh/shell_helpers.py +513 -102
  21. npcsh/stream.py +87 -554
  22. npcsh/video.py +5 -2
  23. npcsh/video_gen.py +69 -0
  24. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/generic_search.tool +9 -1
  25. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/npcsh.ctx +8 -2
  26. npcsh-0.3.32.dist-info/METADATA +779 -0
  27. npcsh-0.3.32.dist-info/RECORD +78 -0
  28. npcsh-0.3.30.dist-info/METADATA +0 -1862
  29. npcsh-0.3.30.dist-info/RECORD +0 -76
  30. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/bash_executer.tool +0 -0
  31. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/calculator.tool +0 -0
  32. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/celona.npc +0 -0
  33. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/code_executor.tool +0 -0
  34. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/corca.npc +0 -0
  35. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/eriane.npc +0 -0
  36. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/foreman.npc +0 -0
  37. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/image_generation.tool +0 -0
  38. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/lineru.npc +0 -0
  39. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/local_search.tool +0 -0
  40. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/maurawa.npc +0 -0
  41. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/npcsh_executor.tool +0 -0
  42. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/raone.npc +0 -0
  43. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/screen_cap.tool +0 -0
  44. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  45. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/slean.npc +0 -0
  46. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/sql_executor.tool +0 -0
  47. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/test_pipeline.py +0 -0
  48. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/turnic.npc +0 -0
  49. {npcsh-0.3.30.data → npcsh-0.3.32.data}/data/npcsh/npc_team/welxor.npc +0 -0
  50. {npcsh-0.3.30.dist-info → npcsh-0.3.32.dist-info}/WHEEL +0 -0
  51. {npcsh-0.3.30.dist-info → npcsh-0.3.32.dist-info}/entry_points.txt +0 -0
  52. {npcsh-0.3.30.dist-info → npcsh-0.3.32.dist-info}/licenses/LICENSE +0 -0
  53. {npcsh-0.3.30.dist-info → npcsh-0.3.32.dist-info}/top_level.txt +0 -0
npcsh/llm_funcs.py CHANGED
@@ -19,7 +19,7 @@ from google.generativeai import types
19
19
  import google.generativeai as genai
20
20
  from sqlalchemy import create_engine
21
21
 
22
- from .npc_sysenv import (
22
+ from npcsh.npc_sysenv import (
23
23
  get_system_message,
24
24
  get_available_models,
25
25
  get_model_and_provider,
@@ -35,50 +35,41 @@ from .npc_sysenv import (
35
35
  NPCSH_REASONING_PROVIDER,
36
36
  NPCSH_IMAGE_GEN_MODEL,
37
37
  NPCSH_IMAGE_GEN_PROVIDER,
38
- NPCSH_API_URL,
38
+ NPCSH_VIDEO_GEN_MODEL,
39
+ NPCSH_VIDEO_GEN_PROVIDER,
39
40
  NPCSH_VISION_MODEL,
40
41
  NPCSH_VISION_PROVIDER,
41
42
  available_reasoning_models,
42
43
  available_chat_models,
43
44
  )
44
45
 
45
- from .stream import (
46
- get_ollama_stream,
47
- get_openai_stream,
48
- get_anthropic_stream,
49
- get_openai_like_stream,
50
- get_deepseek_stream,
51
- get_gemini_stream,
46
+ from npcsh.stream import get_litellm_stream
47
+ from npcsh.conversation import (
48
+ get_litellm_conversation,
52
49
  )
53
- from .conversation import (
54
- get_ollama_conversation,
55
- get_openai_conversation,
56
- get_openai_like_conversation,
57
- get_anthropic_conversation,
58
- get_deepseek_conversation,
59
- get_gemini_conversation,
50
+ from npcsh.response import (
51
+ get_litellm_response,
60
52
  )
61
-
62
- from .response import (
63
- get_ollama_response,
64
- get_openai_response,
65
- get_anthropic_response,
66
- get_openai_like_response,
67
- get_deepseek_response,
68
- get_gemini_response,
53
+ from npcsh.image_gen import (
54
+ generate_image_litellm,
69
55
  )
70
- from .image_gen import (
71
- generate_image_openai,
72
- generate_image_hf_diffusion,
56
+ from npcsh.video_gen import (
57
+ generate_video_diffusers,
73
58
  )
74
59
 
75
- from .embeddings import (
60
+ from npcsh.embeddings import (
76
61
  get_ollama_embeddings,
77
62
  get_openai_embeddings,
78
63
  get_anthropic_embeddings,
79
64
  store_embeddings_for_model,
80
65
  )
81
66
 
67
+ import asyncio
68
+ import sys
69
+ from queue import Queue
70
+ from threading import Thread
71
+ import select
72
+
82
73
 
83
74
  def generate_image(
84
75
  prompt: str,
@@ -87,9 +78,7 @@ def generate_image(
87
78
  filename: str = None,
88
79
  npc: Any = None,
89
80
  ):
90
- """
91
- Function Description:
92
- This function generates an image using the specified provider and model.
81
+ """This function generates an image using the specified provider and model.
93
82
  Args:
94
83
  prompt (str): The prompt for generating the image.
95
84
  Keyword Args:
@@ -118,23 +107,11 @@ def generate_image(
118
107
  os.path.expanduser("~/.npcsh/images/")
119
108
  + f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
120
109
  )
121
-
122
- # if provider == "ollama":
123
- # image = generate_image_ollama(prompt, model)
124
- if provider == "openai":
125
- image = generate_image_openai(
126
- prompt,
127
- model,
128
- npc=npc,
129
- )
130
- # elif provider == "anthropic":
131
- # image = generate_image_anthropic(prompt, model, anthropic_api_key)
132
- # elif provider == "openai-like":
133
- # image = generate_image_openai_like(prompt, model, npc.api_url, openai_api_key)
134
- elif provider == "diffusers":
135
- image = generate_image_hf_diffusion(prompt, model)
136
- else:
137
- image = None
110
+ generate_image_litellm(
111
+ prompt=prompt,
112
+ model=model,
113
+ provider=provider,
114
+ )
138
115
  # save image
139
116
  # check if image is a PIL image
140
117
  if isinstance(image, PIL.Image.Image):
@@ -191,9 +168,7 @@ def get_llm_response(
191
168
  context=None,
192
169
  **kwargs,
193
170
  ):
194
- """
195
- Function Description:
196
- This function generates a response using the specified provider and model.
171
+ """This function generates a response using the specified provider and model.
197
172
  Args:
198
173
  prompt (str): The prompt for generating the response.
199
174
  Keyword Args:
@@ -208,6 +183,7 @@ def get_llm_response(
208
183
  """
209
184
  if model is not None and provider is not None:
210
185
  pass
186
+
211
187
  elif provider is None and model is not None:
212
188
  provider = lookup_provider(model)
213
189
 
@@ -227,84 +203,17 @@ def get_llm_response(
227
203
  model = "llama3.2"
228
204
  # print(provider, model)
229
205
  # print(provider, model)
230
- if provider == "ollama":
231
- if model is None:
232
- if images is not None:
233
- model = "llama:7b"
234
- else:
235
- model = "llama3.2"
236
- elif images is not None and model not in [
237
- "x/llama3.2-vision",
238
- "llama3.2-vision",
239
- "llava-llama3",
240
- "bakllava",
241
- "moondream",
242
- "llava-phi3",
243
- "minicpm-v",
244
- "hhao/openbmb-minicpm-llama3-v-2_5",
245
- "aiden_lu/minicpm-v2.6",
246
- "xuxx/minicpm2.6",
247
- "benzie/llava-phi-3",
248
- "mskimomadto/chat-gph-vision",
249
- "xiayu/openbmb-minicpm-llama3-v-2_5",
250
- "0ssamaak0/xtuner-llava",
251
- "srizon/pixie",
252
- "jyan1/paligemma-mix-224",
253
- "qnguyen3/nanollava",
254
- "knoopx/llava-phi-2",
255
- "nsheth/llama-3-lumimaid-8b-v0.1-iq-imatrix",
256
- "bigbug/minicpm-v2.5",
257
- ]:
258
- model = "llava:7b"
259
- # print(model)
260
- return get_ollama_response(
261
- prompt, model, npc=npc, messages=messages, images=images, **kwargs
262
- )
263
- elif provider == "gemini":
264
- if model is None:
265
- model = "gemini-2.0-flash"
266
- return get_gemini_response(
267
- prompt, model, npc=npc, messages=messages, images=images, **kwargs
268
- )
269
206
 
270
- elif provider == "deepseek":
271
- if model is None:
272
- model = "deepseek-chat"
273
- # print(prompt, model, provider)
274
- return get_deepseek_response(
275
- prompt, model, npc=npc, messages=messages, images=images, **kwargs
276
- )
277
- elif provider == "openai":
278
- if model is None:
279
- model = "gpt-4o-mini"
280
- # print(model)
281
- return get_openai_response(
282
- prompt, model, npc=npc, messages=messages, images=images, **kwargs
283
- )
284
- elif provider == "openai-like":
285
- if api_url is None:
286
- raise ValueError("api_url is required for openai-like provider")
287
- return get_openai_like_response(
288
- prompt,
289
- model,
290
- api_url,
291
- api_key,
292
- npc=npc,
293
- messages=messages,
294
- images=images,
295
- **kwargs,
296
- )
297
-
298
- elif provider == "anthropic":
299
- if model is None:
300
- model = "claude-3-haiku-20240307"
301
- return get_anthropic_response(
302
- prompt, model, npc=npc, messages=messages, images=images, **kwargs
303
- )
304
- else:
305
- # print(provider)
306
- # print(model)
307
- return "Error: Invalid provider specified."
207
+ response = get_litellm_response(
208
+ prompt,
209
+ model=model,
210
+ provider=provider,
211
+ npc=npc,
212
+ api_url=api_url,
213
+ api_key=api_key,
214
+ **kwargs,
215
+ )
216
+ return response
308
217
 
309
218
 
310
219
  def get_stream(
@@ -318,9 +227,7 @@ def get_stream(
318
227
  context=None,
319
228
  **kwargs,
320
229
  ) -> List[Dict[str, str]]:
321
- """
322
- Function Description:
323
- This function generates a streaming response using the specified provider and model
230
+ """This function generates a streaming response using the specified provider and model
324
231
  Args:
325
232
  messages (List[Dict[str, str]]): The list of messages in the conversation.
326
233
  Keyword Args:
@@ -335,6 +242,7 @@ def get_stream(
335
242
  if model is not None and provider is not None:
336
243
  pass
337
244
  elif model is not None and provider is None:
245
+ print(provider)
338
246
  provider = lookup_provider(model)
339
247
  elif npc is not None:
340
248
  if npc.provider is not None:
@@ -347,32 +255,50 @@ def get_stream(
347
255
  provider = "ollama"
348
256
  model = "llama3.2"
349
257
  # print(model, provider)
350
- if provider == "ollama":
351
- return get_ollama_stream(messages, model, npc=npc, images=images, **kwargs)
352
- elif provider == "openai":
353
- return get_openai_stream(
354
- messages, model, npc=npc, api_key=api_key, images=images, **kwargs
355
- )
356
- elif provider == "anthropic":
357
- return get_anthropic_stream(
358
- messages, model, npc=npc, api_key=api_key, images=images, **kwargs
359
- )
360
- elif provider == "openai-like":
361
- return get_openai_like_stream(
362
- messages,
363
- model,
364
- api_url,
365
- npc=npc,
366
- api_key=api_key,
367
- images=images,
368
- **kwargs,
369
- )
370
- elif provider == "deepseek":
371
- return get_deepseek_stream(messages, model, npc=npc, api_key=api_key, **kwargs)
372
- elif provider == "gemini":
373
- return get_gemini_stream(messages, model, npc=npc, api_key=api_key, **kwargs)
374
- else:
375
- return "Error: Invalid provider specified."
258
+
259
+ return get_litellm_stream(
260
+ messages,
261
+ model=model,
262
+ provider=provider,
263
+ npc=npc,
264
+ api_url=api_url,
265
+ api_key=api_key,
266
+ images=images,
267
+ **kwargs,
268
+ )
269
+
270
+
271
+ def generate_video(
272
+ prompt,
273
+ model: str = NPCSH_VIDEO_GEN_MODEL,
274
+ provider: str = NPCSH_VIDEO_GEN_PROVIDER,
275
+ npc: Any = None,
276
+ device: str = "cpu",
277
+ output_path="",
278
+ num_inference_steps=10,
279
+ num_frames=10,
280
+ height=256,
281
+ width=256,
282
+ messages: list = None,
283
+ ):
284
+ """
285
+ Function Description:
286
+ This function generates a video using the Stable Diffusion API.
287
+ Args:
288
+ prompt (str): The prompt for generating the video.
289
+ model_id (str): The Hugging Face model ID to use for Stable Diffusion.
290
+ device (str): The device to run the model on ('cpu' or 'cuda').
291
+ Returns:
292
+ PIL.Image: The generated image.
293
+ """
294
+ output_path = generate_video_diffusers(
295
+ prompt,
296
+ model,
297
+ npc=npc,
298
+ device=device,
299
+ )
300
+ if provider == "diffusers":
301
+ return {"output": "output path at " + output_path, "messages": messages}
376
302
 
377
303
 
378
304
  def get_conversation(
@@ -385,9 +311,7 @@ def get_conversation(
385
311
  context=None,
386
312
  **kwargs,
387
313
  ) -> List[Dict[str, str]]:
388
- """
389
- Function Description:
390
- This function generates a conversation using the specified provider and model.
314
+ """This function generates a conversation using the specified provider and model.
391
315
  Args:
392
316
  messages (List[Dict[str, str]]): The list of messages in the conversation.
393
317
  Keyword Args:
@@ -397,7 +321,6 @@ def get_conversation(
397
321
  Returns:
398
322
  List[Dict[str, str]]: The list of messages in the conversation.
399
323
  """
400
-
401
324
  if model is not None and provider is not None:
402
325
  pass # Use explicitly provided model and provider
403
326
  elif model is not None and provider is None:
@@ -410,30 +333,15 @@ def get_conversation(
410
333
  provider = "ollama"
411
334
  model = "llava:7b" if images is not None else "llama3.2"
412
335
 
413
- # print(provider, model)
414
- if provider == "ollama":
415
- return get_ollama_conversation(
416
- messages, model, npc=npc, images=images, **kwargs
417
- )
418
- elif provider == "openai":
419
- return get_openai_conversation(
420
- messages, model, npc=npc, images=images, **kwargs
421
- )
422
- elif provider == "openai-like":
423
- return get_openai_like_conversation(
424
- messages, model, api_url, npc=npc, images=images, **kwargs
425
- )
426
- elif provider == "anthropic":
427
- return get_anthropic_conversation(
428
- messages, model, npc=npc, images=images, **kwargs
429
- )
430
- elif provider == "gemini":
431
- return get_gemini_conversation(messages, model, npc=npc, **kwargs)
432
- elif provider == "deepseek":
433
- return get_deepseek_conversation(messages, model, npc=npc, **kwargs)
434
-
435
- else:
436
- return "Error: Invalid provider specified."
336
+ return get_litellm_conversation(
337
+ messages,
338
+ model=model,
339
+ provider=provider,
340
+ npc=npc,
341
+ api_url=api_url,
342
+ images=images,
343
+ **kwargs,
344
+ )
437
345
 
438
346
 
439
347
  def execute_llm_question(
@@ -540,9 +448,7 @@ def execute_llm_command(
540
448
  stream=False,
541
449
  context=None,
542
450
  ) -> str:
543
- """
544
- Function Description:
545
- This function executes an LLM command.
451
+ """This function executes an LLM command.
546
452
  Args:
547
453
  command (str): The command to execute.
548
454
 
@@ -749,16 +655,16 @@ def check_llm_command(
749
655
  api_url: str = NPCSH_API_URL,
750
656
  api_key: str = None,
751
657
  npc: Any = None,
658
+ npc_team: Any = None,
752
659
  retrieved_docs=None,
753
660
  messages: List[Dict[str, str]] = None,
754
661
  images: list = None,
755
662
  n_docs=5,
756
663
  stream=False,
757
664
  context=None,
665
+ whisper=False,
758
666
  ):
759
- """
760
- Function Description:
761
- This function checks an LLM command.
667
+ """This function checks an LLM command.
762
668
  Args:
763
669
  command (str): The command to check.
764
670
  Keyword Args:
@@ -814,71 +720,73 @@ ReAct choices then will enter reasoning flow
814
720
 
815
721
  4. Is it a complex request that actually requires more than one
816
722
  tool to be called, perhaps in a sequence?
723
+ Sequences should only be used for more than one consecutive tool call. do not invoke seequences for single tool calls.
817
724
 
818
725
  5. is there a need for the user to provide additional input to fulfill the request?
819
726
 
820
727
 
821
728
 
822
- Available tools:
823
729
  """
824
730
 
825
- if (npc.tools_dict is None or npc.tools_dict == {}) & (
826
- npc.all_tools_dict is None or npc.all_tools_dict == {}
827
- ):
828
- prompt += "No tools available. Do not invoke tools."
829
- else:
830
- tools_set = {}
831
-
832
- if npc.tools_dict is not None:
833
- for tool_name, tool in npc.tools_dict.items():
834
- if tool_name not in tools_set:
835
- tools_set[tool_name] = tool.description
836
- if npc.all_tools_dict is not None:
837
- for tool_name, tool in npc.all_tools_dict.items():
838
- if tool_name not in tools_set:
839
- tools_set[tool_name] = tool.description
840
-
841
- for tool_name, tool_description in tools_set.items():
842
- prompt += f"""
843
-
844
- {tool_name} : {tool_description} \n
845
- """
731
+ if npc is not None or npc_team is not None:
732
+ if (npc.tools_dict is None or npc.tools_dict == {}) & (
733
+ npc.all_tools_dict is None or npc.all_tools_dict == {}
734
+ ):
735
+ prompt += "No tools available. Do not invoke tools."
736
+ else:
737
+ prompt += "Available tools: \n"
738
+ tools_set = {}
739
+
740
+ if npc.tools_dict is not None:
741
+ for tool_name, tool in npc.tools_dict.items():
742
+ if tool_name not in tools_set:
743
+ tools_set[tool_name] = tool.description
744
+ if npc.all_tools_dict is not None:
745
+ for tool_name, tool in npc.all_tools_dict.items():
746
+ if tool_name not in tools_set:
747
+ tools_set[tool_name] = tool.description
748
+
749
+ for tool_name, tool_description in tools_set.items():
750
+ prompt += f"""
846
751
 
847
- prompt += f"""
848
- Available NPCs for alternative answers:
752
+ {tool_name} : {tool_description} \n
753
+ """
849
754
 
850
- """
851
- if len(npc.resolved_npcs) == 0:
852
- prompt += "No NPCs available for alternative answers."
853
- else:
854
- print(npc.resolved_npcs)
855
- for i, npc_in_network in enumerate(npc.resolved_npcs):
856
- name = list(npc_in_network.keys())[0]
857
- npc_obj = npc_in_network[name]
858
-
859
- if hasattr(npc_obj, "name"):
860
- name_to_include = npc_obj.name
861
- elif "name " in npc_obj:
862
- name_to_include = npc_obj["name"]
863
-
864
- if hasattr(npc_obj, "primary_directive"):
865
- primary_directive_to_include = npc_obj.primary_directive
866
- elif "primary_directive" in npc_obj:
867
- primary_directive_to_include = npc_obj["primary_directive"]
755
+ if len(npc.resolved_npcs) == 0:
756
+ prompt += "No NPCs available for alternative answers."
757
+ else:
868
758
  prompt += f"""
869
- ({i})
759
+ Available NPCs for alternative answers:
870
760
 
871
- NPC: {name_to_include}
872
- Primary Directive : {primary_directive_to_include}
761
+ """
762
+ print(npc.resolved_npcs)
763
+ for i, npc_in_network in enumerate(npc.resolved_npcs):
764
+ name = list(npc_in_network.keys())[0]
765
+ npc_obj = npc_in_network[name]
766
+
767
+ if hasattr(npc_obj, "name"):
768
+ name_to_include = npc_obj.name
769
+ elif "name " in npc_obj:
770
+ name_to_include = npc_obj["name"]
771
+
772
+ if hasattr(npc_obj, "primary_directive"):
773
+ primary_directive_to_include = npc_obj.primary_directive
774
+ elif "primary_directive" in npc_obj:
775
+ primary_directive_to_include = npc_obj["primary_directive"]
776
+ prompt += f"""
777
+ ({i})
873
778
 
779
+ NPC: {name_to_include}
780
+ Primary Directive : {primary_directive_to_include}
781
+
782
+ """
783
+ if npc.shared_context:
784
+ prompt += f"""
785
+ Relevant shared context for the npc:
786
+ {npc.shared_context}
874
787
  """
875
- if npc.shared_context:
876
- prompt += f"""
877
- Relevant shared context for the npc:
878
- {npc.shared_context}
879
- """
880
- # print("shared_context: " + str(npc.shared_context))
881
- # print(prompt)
788
+ # print("shared_context: " + str(npc.shared_context))
789
+ # print(prompt)
882
790
 
883
791
  prompt += f"""
884
792
  In considering how to answer this, consider:
@@ -891,7 +799,7 @@ ReAct choices then will enter reasoning flow
891
799
  - Whether a tool should be used.
892
800
 
893
801
 
894
- Excluding time-sensitive phenomena,
802
+ Excluding time-sensitive phenomena or ones that require external data inputs /information,
895
803
  most general questions can be answered without any
896
804
  extra tools or agent passes.
897
805
  Only use tools or pass to other NPCs
@@ -926,7 +834,8 @@ ReAct choices then will enter reasoning flow
926
834
  }}
927
835
 
928
836
  If you execute a sequence, ensure that you have a specified NPC for each tool use.
929
-
837
+ question answering is not a tool use.
838
+ "invoke_tool" should never be used in the list of tools when executing a sequence.
930
839
  Remember, do not include ANY ADDITIONAL MARKDOWN FORMATTING.
931
840
  There should be no leading ```json.
932
841
 
@@ -946,6 +855,27 @@ ReAct choices then will enter reasoning flow
946
855
  {context}
947
856
 
948
857
  """
858
+ if whisper:
859
+ prompt += f"""
860
+ IMPORTANT!!!
861
+
862
+ This check is part of a npcsh whisper mode conversation.
863
+
864
+ This mode is a mode wherein the user speaks and receives
865
+ audio that has been played through TTS.
866
+ Thus, consider it to be a more casual conversation
867
+ and engage in regular conversation
868
+ unless they specifically mention in their request.
869
+ And if something is confusing or seems like it needs
870
+ additional context,
871
+ do not worry too mucuh about it because this
872
+ information is likely contained within the historical messages between
873
+ the user and the LLM and you can let the downstream
874
+ agent navigate asking followup questions .
875
+
876
+
877
+ """
878
+
949
879
  action_response = get_llm_response(
950
880
  prompt,
951
881
  model=model,
@@ -975,10 +905,11 @@ ReAct choices then will enter reasoning flow
975
905
  response_content_parsed = response_content
976
906
 
977
907
  action = response_content_parsed.get("action")
978
- explanation = response_content["explanation"]
908
+ explanation = response_content_parsed.get("explanation")
979
909
  print(f"action chosen: {action}")
980
910
  print(f"explanation given: {explanation}")
981
911
 
912
+ # print(response_content)
982
913
  if response_content_parsed.get("tool_name"):
983
914
  print(f"tool name: {response_content_parsed.get('tool_name')}")
984
915
 
@@ -1110,15 +1041,22 @@ ReAct choices then will enter reasoning flow
1110
1041
  elif action == "execute_sequence":
1111
1042
  tool_names = response_content_parsed.get("tool_name")
1112
1043
  npc_names = response_content_parsed.get("npc_name")
1044
+
1113
1045
  # print(npc_names)
1114
1046
  npcs = []
1115
1047
  # print(tool_names, npc_names)
1116
1048
  if isinstance(npc_names, list):
1049
+ if len(npc_names) == 0:
1050
+ # if no npcs are specified, just have the npc take care of it itself instead of trying to force it to generate npc names for sequences all the time
1051
+
1052
+ npcs = [npc] * len(tool_names)
1117
1053
  for npc_name in npc_names:
1118
1054
  for npc_obj in npc.resolved_npcs:
1119
1055
  if npc_name in npc_obj:
1120
1056
  npcs.append(npc_obj[npc_name])
1121
1057
  break
1058
+ if len(npcs) < len(tool_names):
1059
+ npcs.append(npc)
1122
1060
 
1123
1061
  output = ""
1124
1062
  results_tool_calls = []
@@ -1137,10 +1075,11 @@ ReAct choices then will enter reasoning flow
1137
1075
  retrieved_docs=retrieved_docs,
1138
1076
  stream=stream,
1139
1077
  )
1078
+ # print(result)
1140
1079
  results_tool_calls.append(result)
1141
1080
  messages = result.get("messages", messages)
1142
1081
  output += result.get("output", "")
1143
- print(output)
1082
+ # print(results_tool_calls)
1144
1083
  else:
1145
1084
  for npc_obj in npcs:
1146
1085
  result = npc.handle_agent_pass(
@@ -1154,7 +1093,7 @@ ReAct choices then will enter reasoning flow
1154
1093
 
1155
1094
  messages = result.get("messages", messages)
1156
1095
  results_tool_calls.append(result.get("response"))
1157
- print(messages[-1])
1096
+ # print(messages[-1])
1158
1097
  # import pdb
1159
1098
 
1160
1099
  # pdb.set_trace()
@@ -1181,9 +1120,7 @@ def handle_tool_call(
1181
1120
  attempt=0,
1182
1121
  context=None,
1183
1122
  ) -> Union[str, Dict[str, Any]]:
1184
- """
1185
- Function Description:
1186
- This function handles a tool call.
1123
+ """This function handles a tool call.
1187
1124
  Args:
1188
1125
  command (str): The command.
1189
1126
  tool_name (str): The tool name.
@@ -1312,22 +1249,22 @@ def handle_tool_call(
1312
1249
  # try:
1313
1250
  print("Executing tool with input values:", input_values)
1314
1251
 
1315
- try:
1316
- tool_output = tool.execute(
1317
- input_values,
1318
- npc.all_tools_dict,
1319
- jinja_env,
1320
- command,
1321
- model=model,
1322
- provider=provider,
1323
- npc=npc,
1324
- stream=stream,
1325
- messages=messages,
1326
- )
1327
- if not stream:
1328
- if "Error" in tool_output:
1329
- raise Exception(tool_output)
1330
- except Exception as e:
1252
+ # try:
1253
+ tool_output = tool.execute(
1254
+ input_values,
1255
+ npc.all_tools_dict,
1256
+ jinja_env,
1257
+ command,
1258
+ model=model,
1259
+ provider=provider,
1260
+ npc=npc,
1261
+ stream=stream,
1262
+ messages=messages,
1263
+ )
1264
+ if not stream:
1265
+ if "Error" in tool_output:
1266
+ raise Exception(tool_output)
1267
+ # except Exception as e:
1331
1268
  # diagnose_problem = get_llm_response(
1332
1269
  ## f"""a problem has occurred.
1333
1270
  # Please provide a diagnosis of the problem and a suggested #fix.
@@ -1352,24 +1289,32 @@ def handle_tool_call(
1352
1289
  # suggested_solution = diagnose_problem.get("response", {}).get(
1353
1290
  # "suggested_solution"
1354
1291
  # )
1355
-
1356
- tool_output = handle_tool_call(
1357
- command,
1358
- tool_name,
1359
- model=model,
1360
- provider=provider,
1361
- messages=messages,
1362
- npc=npc,
1363
- api_url=api_url,
1364
- api_key=api_key,
1365
- retrieved_docs=retrieved_docs,
1366
- n_docs=n_docs,
1367
- stream=stream,
1368
- attempt=attempt + 1,
1369
- n_attempts=n_attempts,
1370
- context=f""" \n \n \n "tool failed: {e} \n \n \n here was the previous attempt: {input_values}""",
1371
- )
1372
-
1292
+ '''
1293
+ print(f"An error occurred while executing the tool: {e}")
1294
+ print(f"trying again, attempt {attempt+1}")
1295
+ if attempt < n_attempts:
1296
+ tool_output = handle_tool_call(
1297
+ command,
1298
+ tool_name,
1299
+ model=model,
1300
+ provider=provider,
1301
+ messages=messages,
1302
+ npc=npc,
1303
+ api_url=api_url,
1304
+ api_key=api_key,
1305
+ retrieved_docs=retrieved_docs,
1306
+ n_docs=n_docs,
1307
+ stream=stream,
1308
+ attempt=attempt + 1,
1309
+ n_attempts=n_attempts,
1310
+ context=f""" \n \n \n "tool failed: {e} \n \n \n here was the previous attempt: {input_values}""",
1311
+ )
1312
+ else:
1313
+ user_input = input(
1314
+ "the tool execution has failed after three tries, can you add more context to help or would you like to run again?"
1315
+ )
1316
+ return
1317
+ '''
1373
1318
  if stream:
1374
1319
  return tool_output
1375
1320
  # print(f"Tool output: {tool_output}")
@@ -1388,9 +1333,7 @@ def execute_data_operations(
1388
1333
  npc: Any = None,
1389
1334
  db_path: str = "~/npcsh_history.db",
1390
1335
  ):
1391
- """
1392
- Function Description:
1393
- This function executes data operations.
1336
+ """This function executes data operations.
1394
1337
  Args:
1395
1338
  query (str): The query to execute.
1396
1339
 
@@ -1777,7 +1720,7 @@ def get_data_response(
1777
1720
  # failures.append(str(e))
1778
1721
 
1779
1722
 
1780
- def enter_reasoning_human_in_the_loop(
1723
+ def enter_chat_human_in_the_loop(
1781
1724
  messages: List[Dict[str, str]],
1782
1725
  reasoning_model: str = NPCSH_REASONING_MODEL,
1783
1726
  reasoning_provider: str = NPCSH_REASONING_PROVIDER,
@@ -1826,7 +1769,35 @@ def enter_reasoning_human_in_the_loop(
1826
1769
  in_think_block = False
1827
1770
 
1828
1771
  for chunk in response_stream:
1829
- # Extract content based on provider/model type
1772
+ # Check for user interrupt
1773
+ if not input_queue.empty():
1774
+ user_interrupt = input_queue.get()
1775
+ yield "\n[Stream interrupted by user]\n"
1776
+ yield "Enter your additional input: "
1777
+
1778
+ # Get the full interrupt message
1779
+ full_interrupt = user_interrupt
1780
+ while not input_queue.empty():
1781
+ full_interrupt += "\n" + input_queue.get()
1782
+
1783
+ # Add the interruption to messages and restart stream
1784
+ messages.append(
1785
+ {"role": "user", "content": f"[INTERRUPT] {full_interrupt}"}
1786
+ )
1787
+
1788
+ yield f"\n[Continuing with added context...]\n"
1789
+ yield from enter_chat_human_in_the_loop(
1790
+ messages,
1791
+ reasoning_model=reasoning_model,
1792
+ reasoning_provider=reasoning_provider,
1793
+ chat_model=chat_model,
1794
+ chat_provider=chat_provider,
1795
+ npc=npc,
1796
+ answer_only=True,
1797
+ )
1798
+ return
1799
+
1800
+ # Extract content based on provider
1830
1801
  if reasoning_provider == "ollama":
1831
1802
  chunk_content = chunk.get("message", {}).get("content", "")
1832
1803
  elif reasoning_provider == "openai" or reasoning_provider == "deepseek":
@@ -1841,80 +1812,191 @@ def enter_reasoning_human_in_the_loop(
1841
1812
  else:
1842
1813
  chunk_content = ""
1843
1814
  else:
1844
- # Default extraction
1845
1815
  chunk_content = str(chunk)
1846
1816
 
1847
- # Always yield the chunk whether in think block or not
1848
1817
  response_chunks.append(chunk_content)
1849
- # Track think block state and accumulate thoughts
1850
- if answer_only:
1818
+ combined_text = "".join(response_chunks)
1819
+
1820
+ # Check for LLM request block
1821
+ if (
1822
+ "<request_for_input>" in combined_text
1823
+ and "</request_for_input>" not in combined_text
1824
+ ):
1825
+ in_think_block = True
1826
+
1827
+ if in_think_block:
1828
+ thoughts.append(chunk_content)
1851
1829
  yield chunk
1852
- else:
1853
- if "<th" in "".join(response_chunks) and "/th" not in "".join(
1854
- response_chunks
1855
- ):
1856
- in_think_block = True
1857
-
1858
- if in_think_block:
1859
- thoughts.append(chunk_content)
1860
- yield chunk # Show the thoughts as they come
1861
-
1862
- if "</th" in "".join(response_chunks):
1863
- thought_text = "".join(thoughts)
1864
- # Analyze thoughts before stopping
1865
- input_needed = analyze_thoughts_for_input(
1866
- thought_text, model=chat_model, provider=chat_provider
1867
- )
1868
1830
 
1869
- if input_needed:
1870
- # If input needed, get it and restart with new context
1871
- user_input = request_user_input(input_needed)
1831
+ if "</request_for_input>" in combined_text:
1832
+ # Process the LLM's input request
1833
+ request_text = "".join(thoughts)
1834
+ yield "\nPlease provide the requested information: "
1835
+
1836
+ # Wait for user input (blocking here is OK since we explicitly asked)
1837
+ user_input = input()
1838
+
1839
+ # Add the interaction to messages and restart stream
1840
+ messages.append({"role": "assistant", "content": request_text})
1841
+ messages.append({"role": "user", "content": user_input})
1842
+
1843
+ yield "\n[Continuing with provided information...]\n"
1844
+ yield from enter_chat_human_in_the_loop(
1845
+ messages,
1846
+ reasoning_model=reasoning_model,
1847
+ reasoning_provider=reasoning_provider,
1848
+ chat_model=chat_model,
1849
+ chat_provider=chat_provider,
1850
+ npc=npc,
1851
+ answer_only=True,
1852
+ )
1853
+ return
1872
1854
 
1873
- messages.append(
1874
- {
1875
- "role": "assistant",
1876
- "content": f"""its clear that extra input is required.
1877
- could you please provide it? Here is the reason:
1855
+ if not in_think_block:
1856
+ yield chunk
1878
1857
 
1879
- {input_needed['reason']},
1880
1858
 
1881
- and the prompt: {input_needed['prompt']}""",
1882
- }
1883
- )
1859
+ def enter_reasoning_human_in_the_loop(
1860
+ messages: List[Dict[str, str]],
1861
+ reasoning_model: str = NPCSH_REASONING_MODEL,
1862
+ reasoning_provider: str = NPCSH_REASONING_PROVIDER,
1863
+ chat_model: str = NPCSH_CHAT_MODEL,
1864
+ chat_provider: str = NPCSH_CHAT_PROVIDER,
1865
+ npc: Any = None,
1866
+ answer_only: bool = False,
1867
+ context=None,
1868
+ ) -> Generator[str, None, None]:
1869
+ """
1870
+ Stream responses while checking for think tokens and handling human input when needed.
1884
1871
 
1885
- messages.append({"role": "user", "content": user_input})
1886
- yield from enter_reasoning_human_in_the_loop(
1887
- messages,
1888
- reasoning_model=reasoning_model,
1889
- reasoning_provider=reasoning_provider,
1890
- chat_model=chat_model,
1891
- chat_provider=chat_provider,
1892
- npc=npc,
1893
- answer_only=True,
1894
- )
1895
- else:
1896
- # If no input needed, just get the answer
1897
- messages.append({"role": "assistant", "content": thought_text})
1898
- messages.append(
1899
- {"role": "user", "content": messages[-2]["content"]}
1900
- )
1901
- yield from enter_reasoning_human_in_the_loop( # Restart with new context
1902
- messages,
1903
- reasoning_model=reasoning_model,
1904
- reasoning_provider=reasoning_provider,
1905
- chat_model=chat_model,
1906
- chat_provider=chat_provider,
1907
- npc=npc,
1908
- answer_only=True,
1909
- )
1872
+ Args:
1873
+ messages: List of conversation messages
1874
+ model: LLM model to use
1875
+ provider: Model provider
1876
+ npc: NPC instance if applicable
1910
1877
 
1911
- return # Stop the original stream in either case
1878
+ Yields:
1879
+ Streamed response chunks
1880
+ """
1881
+ # Get the initial stream
1882
+ if answer_only:
1883
+ messages[-1]["content"] = (
1884
+ messages[-1]["content"].replace(
1885
+ "Think first though and use <think> tags", ""
1886
+ )
1887
+ + " Do not think just answer. "
1888
+ )
1889
+ else:
1890
+ messages[-1]["content"] = (
1891
+ messages[-1]["content"]
1892
+ + " Think first though and use <think> tags. "
1893
+ )
1894
+
1895
+ response_stream = get_stream(
1896
+ messages,
1897
+ model=reasoning_model,
1898
+ provider=reasoning_provider,
1899
+ npc=npc,
1900
+ context=context,
1901
+ )
1902
+
1903
+ thoughts = []
1904
+ response_chunks = []
1905
+ in_think_block = False
1906
+
1907
+ for chunk in response_stream:
1908
+ # Check for user interrupt
1909
+ if not input_queue.empty():
1910
+ user_interrupt = input_queue.get()
1911
+ yield "\n[Stream interrupted by user]\n"
1912
+ yield "Enter your additional input: "
1913
+
1914
+ # Get the full interrupt message
1915
+ full_interrupt = user_interrupt
1916
+ while not input_queue.empty():
1917
+ full_interrupt += "\n" + input_queue.get()
1918
+
1919
+ # Add the interruption to messages and restart stream
1920
+ messages.append(
1921
+ {"role": "user", "content": f"[INTERRUPT] {full_interrupt}"}
1922
+ )
1923
+
1924
+ yield f"\n[Continuing with added context...]\n"
1925
+ yield from enter_reasoning_human_in_the_loop(
1926
+ messages,
1927
+ reasoning_model=reasoning_model,
1928
+ reasoning_provider=reasoning_provider,
1929
+ chat_model=chat_model,
1930
+ chat_provider=chat_provider,
1931
+ npc=npc,
1932
+ answer_only=True,
1933
+ )
1934
+ return
1935
+
1936
+ # Extract content based on provider
1937
+ if reasoning_provider == "ollama":
1938
+ chunk_content = chunk.get("message", {}).get("content", "")
1939
+ elif reasoning_provider == "openai" or reasoning_provider == "deepseek":
1940
+ chunk_content = "".join(
1941
+ choice.delta.content
1942
+ for choice in chunk.choices
1943
+ if choice.delta.content is not None
1944
+ )
1945
+ elif reasoning_provider == "anthropic":
1946
+ if chunk.type == "content_block_delta":
1947
+ chunk_content = chunk.delta.text
1948
+ else:
1949
+ chunk_content = ""
1950
+ else:
1951
+ chunk_content = str(chunk)
1952
+
1953
+ response_chunks.append(chunk_content)
1954
+ combined_text = "".join(response_chunks)
1955
+
1956
+ # Check for LLM request block
1957
+ if (
1958
+ "<request_for_input>" in combined_text
1959
+ and "</request_for_input>" not in combined_text
1960
+ ):
1961
+ in_think_block = True
1962
+
1963
+ if in_think_block:
1964
+ thoughts.append(chunk_content)
1965
+ yield chunk
1966
+
1967
+ if "</request_for_input>" in combined_text:
1968
+ # Process the LLM's input request
1969
+ request_text = "".join(thoughts)
1970
+ yield "\nPlease provide the requested information: "
1971
+
1972
+ # Wait for user input (blocking here is OK since we explicitly asked)
1973
+ user_input = input()
1974
+
1975
+ # Add the interaction to messages and restart stream
1976
+ messages.append({"role": "assistant", "content": request_text})
1977
+ messages.append({"role": "user", "content": user_input})
1978
+
1979
+ yield "\n[Continuing with provided information...]\n"
1980
+ yield from enter_reasoning_human_in_the_loop(
1981
+ messages,
1982
+ reasoning_model=reasoning_model,
1983
+ reasoning_provider=reasoning_provider,
1984
+ chat_model=chat_model,
1985
+ chat_provider=chat_provider,
1986
+ npc=npc,
1987
+ answer_only=True,
1988
+ )
1989
+ return
1990
+
1991
+ if not in_think_block:
1992
+ yield chunk
1912
1993
 
1913
1994
 
1914
1995
  def handle_request_input(
1915
1996
  context: str,
1916
1997
  model: str = NPCSH_CHAT_MODEL,
1917
1998
  provider: str = NPCSH_CHAT_PROVIDER,
1999
+ whisper: bool = False,
1918
2000
  ):
1919
2001
  """
1920
2002
  Analyze text and decide what to request from the user
@@ -1947,7 +2029,7 @@ def handle_request_input(
1947
2029
  result = json.loads(result)
1948
2030
 
1949
2031
  user_input = request_user_input(
1950
- {"reason": result["request_reason"], "prompt": result["request_prompt"]}
2032
+ {"reason": result["request_reason"], "prompt": result["request_prompt"]},
1951
2033
  )
1952
2034
  return user_input
1953
2035
 
@@ -2025,3 +2107,89 @@ def request_user_input(input_request: Dict[str, str]) -> str:
2025
2107
  """
2026
2108
  print(f"\nAdditional input needed: {input_request['reason']}")
2027
2109
  return input(f"{input_request['prompt']}: ")
2110
+
2111
+
2112
+ def check_user_input() -> Optional[str]:
2113
+ """
2114
+ Non-blocking check for user input.
2115
+ Returns None if no input is available, otherwise returns the input string.
2116
+ """
2117
+ if select.select([sys.stdin], [], [], 0.0)[0]:
2118
+ return input()
2119
+ return None
2120
+
2121
+
2122
+ def input_listener(input_queue: Queue):
2123
+ """
2124
+ Continuously listen for user input in a separate thread.
2125
+ """
2126
+ while True:
2127
+ try:
2128
+ user_input = input()
2129
+ input_queue.put(user_input)
2130
+ except EOFError:
2131
+ break
2132
+
2133
+
2134
+ def stream_with_interrupts(
2135
+ messages: List[Dict[str, str]],
2136
+ model: str,
2137
+ provider: str,
2138
+ npc: Any = None,
2139
+ context=None,
2140
+ ) -> Generator[str, None, None]:
2141
+ """Stream responses with basic Ctrl+C handling and recursive conversation loop."""
2142
+ response_stream = get_stream(
2143
+ messages, model=model, provider=provider, npc=npc, context=context
2144
+ )
2145
+
2146
+ try:
2147
+ # Flag to track if streaming is complete
2148
+ streaming_complete = False
2149
+
2150
+ for chunk in response_stream:
2151
+ if provider == "ollama":
2152
+ chunk_content = chunk.get("message", {}).get("content", "")
2153
+ elif provider in ["openai", "deepseek"]:
2154
+ chunk_content = "".join(
2155
+ choice.delta.content
2156
+ for choice in chunk.choices
2157
+ if choice.delta.content is not None
2158
+ )
2159
+ elif provider == "anthropic":
2160
+ chunk_content = (
2161
+ chunk.delta.text if chunk.type == "content_block_delta" else ""
2162
+ )
2163
+ else:
2164
+ chunk_content = str(chunk)
2165
+
2166
+ yield chunk_content
2167
+
2168
+ # Optional: Mark streaming as complete when no more chunks
2169
+ if not chunk_content:
2170
+ streaming_complete = True
2171
+
2172
+ except KeyboardInterrupt:
2173
+ # Handle keyboard interrupt by getting user input
2174
+ user_input = input("\n> ")
2175
+ messages.append({"role": "user", "content": user_input})
2176
+ yield from stream_with_interrupts(
2177
+ messages, model=model, provider=provider, npc=npc, context=context
2178
+ )
2179
+
2180
+ finally:
2181
+ # Prompt for next input and continue conversation
2182
+ while True:
2183
+ user_input = input("\n> ")
2184
+
2185
+ # Option to exit the loop
2186
+ if user_input.lower() in ["exit", "quit", "q"]:
2187
+ break
2188
+
2189
+ # Add user input to messages
2190
+ messages.append({"role": "user", "content": user_input})
2191
+
2192
+ # Recursively continue the conversation
2193
+ yield from stream_with_interrupts(
2194
+ messages, model=model, provider=provider, npc=npc, context=context
2195
+ )