npcsh 0.3.32__py3-none-any.whl → 1.0.0__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 (93) hide show
  1. npcsh/_state.py +942 -0
  2. npcsh/alicanto.py +1074 -0
  3. npcsh/guac.py +785 -0
  4. npcsh/mcp_helpers.py +357 -0
  5. npcsh/mcp_npcsh.py +822 -0
  6. npcsh/mcp_server.py +184 -0
  7. npcsh/npc.py +218 -0
  8. npcsh/npcsh.py +1161 -0
  9. npcsh/plonk.py +387 -269
  10. npcsh/pti.py +234 -0
  11. npcsh/routes.py +958 -0
  12. npcsh/spool.py +315 -0
  13. npcsh/wander.py +550 -0
  14. npcsh/yap.py +573 -0
  15. npcsh-1.0.0.dist-info/METADATA +596 -0
  16. npcsh-1.0.0.dist-info/RECORD +21 -0
  17. {npcsh-0.3.32.dist-info → npcsh-1.0.0.dist-info}/WHEEL +1 -1
  18. npcsh-1.0.0.dist-info/entry_points.txt +9 -0
  19. {npcsh-0.3.32.dist-info → npcsh-1.0.0.dist-info}/licenses/LICENSE +1 -1
  20. npcsh/audio.py +0 -569
  21. npcsh/audio_gen.py +0 -1
  22. npcsh/cli.py +0 -543
  23. npcsh/command_history.py +0 -566
  24. npcsh/conversation.py +0 -54
  25. npcsh/data_models.py +0 -46
  26. npcsh/dataframes.py +0 -171
  27. npcsh/embeddings.py +0 -168
  28. npcsh/helpers.py +0 -646
  29. npcsh/image.py +0 -298
  30. npcsh/image_gen.py +0 -79
  31. npcsh/knowledge_graph.py +0 -1006
  32. npcsh/llm_funcs.py +0 -2195
  33. npcsh/load_data.py +0 -83
  34. npcsh/main.py +0 -5
  35. npcsh/model_runner.py +0 -189
  36. npcsh/npc_compiler.py +0 -2879
  37. npcsh/npc_sysenv.py +0 -388
  38. npcsh/npc_team/assembly_lines/test_pipeline.py +0 -181
  39. npcsh/npc_team/corca.npc +0 -13
  40. npcsh/npc_team/foreman.npc +0 -7
  41. npcsh/npc_team/npcsh.ctx +0 -11
  42. npcsh/npc_team/sibiji.npc +0 -4
  43. npcsh/npc_team/templates/analytics/celona.npc +0 -0
  44. npcsh/npc_team/templates/hr_support/raone.npc +0 -0
  45. npcsh/npc_team/templates/humanities/eriane.npc +0 -4
  46. npcsh/npc_team/templates/it_support/lineru.npc +0 -0
  47. npcsh/npc_team/templates/marketing/slean.npc +0 -4
  48. npcsh/npc_team/templates/philosophy/maurawa.npc +0 -0
  49. npcsh/npc_team/templates/sales/turnic.npc +0 -4
  50. npcsh/npc_team/templates/software/welxor.npc +0 -0
  51. npcsh/npc_team/tools/bash_executer.tool +0 -32
  52. npcsh/npc_team/tools/calculator.tool +0 -8
  53. npcsh/npc_team/tools/code_executor.tool +0 -16
  54. npcsh/npc_team/tools/generic_search.tool +0 -27
  55. npcsh/npc_team/tools/image_generation.tool +0 -25
  56. npcsh/npc_team/tools/local_search.tool +0 -149
  57. npcsh/npc_team/tools/npcsh_executor.tool +0 -9
  58. npcsh/npc_team/tools/screen_cap.tool +0 -27
  59. npcsh/npc_team/tools/sql_executor.tool +0 -26
  60. npcsh/response.py +0 -272
  61. npcsh/search.py +0 -252
  62. npcsh/serve.py +0 -1467
  63. npcsh/shell.py +0 -524
  64. npcsh/shell_helpers.py +0 -3919
  65. npcsh/stream.py +0 -233
  66. npcsh/video.py +0 -52
  67. npcsh/video_gen.py +0 -69
  68. npcsh-0.3.32.data/data/npcsh/npc_team/bash_executer.tool +0 -32
  69. npcsh-0.3.32.data/data/npcsh/npc_team/calculator.tool +0 -8
  70. npcsh-0.3.32.data/data/npcsh/npc_team/celona.npc +0 -0
  71. npcsh-0.3.32.data/data/npcsh/npc_team/code_executor.tool +0 -16
  72. npcsh-0.3.32.data/data/npcsh/npc_team/corca.npc +0 -13
  73. npcsh-0.3.32.data/data/npcsh/npc_team/eriane.npc +0 -4
  74. npcsh-0.3.32.data/data/npcsh/npc_team/foreman.npc +0 -7
  75. npcsh-0.3.32.data/data/npcsh/npc_team/generic_search.tool +0 -27
  76. npcsh-0.3.32.data/data/npcsh/npc_team/image_generation.tool +0 -25
  77. npcsh-0.3.32.data/data/npcsh/npc_team/lineru.npc +0 -0
  78. npcsh-0.3.32.data/data/npcsh/npc_team/local_search.tool +0 -149
  79. npcsh-0.3.32.data/data/npcsh/npc_team/maurawa.npc +0 -0
  80. npcsh-0.3.32.data/data/npcsh/npc_team/npcsh.ctx +0 -11
  81. npcsh-0.3.32.data/data/npcsh/npc_team/npcsh_executor.tool +0 -9
  82. npcsh-0.3.32.data/data/npcsh/npc_team/raone.npc +0 -0
  83. npcsh-0.3.32.data/data/npcsh/npc_team/screen_cap.tool +0 -27
  84. npcsh-0.3.32.data/data/npcsh/npc_team/sibiji.npc +0 -4
  85. npcsh-0.3.32.data/data/npcsh/npc_team/slean.npc +0 -4
  86. npcsh-0.3.32.data/data/npcsh/npc_team/sql_executor.tool +0 -26
  87. npcsh-0.3.32.data/data/npcsh/npc_team/test_pipeline.py +0 -181
  88. npcsh-0.3.32.data/data/npcsh/npc_team/turnic.npc +0 -4
  89. npcsh-0.3.32.data/data/npcsh/npc_team/welxor.npc +0 -0
  90. npcsh-0.3.32.dist-info/METADATA +0 -779
  91. npcsh-0.3.32.dist-info/RECORD +0 -78
  92. npcsh-0.3.32.dist-info/entry_points.txt +0 -3
  93. {npcsh-0.3.32.dist-info → npcsh-1.0.0.dist-info}/top_level.txt +0 -0
npcsh/llm_funcs.py DELETED
@@ -1,2195 +0,0 @@
1
- # Remove duplicate imports
2
- import subprocess
3
- import requests
4
- import os
5
- import json
6
- import PIL
7
-
8
- import sqlite3
9
- from datetime import datetime
10
- from typing import List, Dict, Any, Optional, Union, Generator
11
-
12
-
13
- from jinja2 import Environment, FileSystemLoader, Template, Undefined
14
-
15
- import pandas as pd
16
- import numpy as np
17
-
18
- from google.generativeai import types
19
- import google.generativeai as genai
20
- from sqlalchemy import create_engine
21
-
22
- from npcsh.npc_sysenv import (
23
- get_system_message,
24
- get_available_models,
25
- get_model_and_provider,
26
- lookup_provider,
27
- NPCSH_CHAT_PROVIDER,
28
- NPCSH_CHAT_MODEL,
29
- NPCSH_API_URL,
30
- EMBEDDINGS_DB_PATH,
31
- NPCSH_EMBEDDING_MODEL,
32
- NPCSH_EMBEDDING_PROVIDER,
33
- NPCSH_DEFAULT_MODE,
34
- NPCSH_REASONING_MODEL,
35
- NPCSH_REASONING_PROVIDER,
36
- NPCSH_IMAGE_GEN_MODEL,
37
- NPCSH_IMAGE_GEN_PROVIDER,
38
- NPCSH_VIDEO_GEN_MODEL,
39
- NPCSH_VIDEO_GEN_PROVIDER,
40
- NPCSH_VISION_MODEL,
41
- NPCSH_VISION_PROVIDER,
42
- available_reasoning_models,
43
- available_chat_models,
44
- )
45
-
46
- from npcsh.stream import get_litellm_stream
47
- from npcsh.conversation import (
48
- get_litellm_conversation,
49
- )
50
- from npcsh.response import (
51
- get_litellm_response,
52
- )
53
- from npcsh.image_gen import (
54
- generate_image_litellm,
55
- )
56
- from npcsh.video_gen import (
57
- generate_video_diffusers,
58
- )
59
-
60
- from npcsh.embeddings import (
61
- get_ollama_embeddings,
62
- get_openai_embeddings,
63
- get_anthropic_embeddings,
64
- store_embeddings_for_model,
65
- )
66
-
67
- import asyncio
68
- import sys
69
- from queue import Queue
70
- from threading import Thread
71
- import select
72
-
73
-
74
- def generate_image(
75
- prompt: str,
76
- model: str = NPCSH_IMAGE_GEN_MODEL,
77
- provider: str = NPCSH_IMAGE_GEN_PROVIDER,
78
- filename: str = None,
79
- npc: Any = None,
80
- ):
81
- """This function generates an image using the specified provider and model.
82
- Args:
83
- prompt (str): The prompt for generating the image.
84
- Keyword Args:
85
- model (str): The model to use for generating the image.
86
- provider (str): The provider to use for generating the image.
87
- filename (str): The filename to save the image to.
88
- npc (Any): The NPC object.
89
- Returns:
90
- str: The filename of the saved image.
91
- """
92
- if model is not None and provider is not None:
93
- pass
94
- elif model is not None and provider is None:
95
- provider = lookup_provider(model)
96
- elif npc is not None:
97
- if npc.provider is not None:
98
- provider = npc.provider
99
- if npc.model is not None:
100
- model = npc.model
101
- if npc.api_url is not None:
102
- api_url = npc.api_url
103
- if filename is None:
104
- # Generate a filename based on the prompt and the date time
105
- os.makedirs(os.path.expanduser("~/.npcsh/images/"), exist_ok=True)
106
- filename = (
107
- os.path.expanduser("~/.npcsh/images/")
108
- + f"image_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png"
109
- )
110
- generate_image_litellm(
111
- prompt=prompt,
112
- model=model,
113
- provider=provider,
114
- )
115
- # save image
116
- # check if image is a PIL image
117
- if isinstance(image, PIL.Image.Image):
118
- image.save(filename)
119
- return filename
120
-
121
- else:
122
- try:
123
- # image is at a private url
124
- response = requests.get(image.data[0].url)
125
- with open(filename, "wb") as file:
126
- file.write(response.content)
127
- from PIL import Image
128
-
129
- img = Image.open(filename)
130
- img.show()
131
- # console = Console()
132
- # console.print(Image.from_path(filename))
133
- return filename
134
-
135
- except AttributeError as e:
136
- print(f"Error saving image: {e}")
137
-
138
-
139
- def get_embeddings(
140
- texts: List[str],
141
- model: str = NPCSH_EMBEDDING_MODEL,
142
- provider: str = NPCSH_EMBEDDING_PROVIDER,
143
- ) -> List[List[float]]:
144
- """Generate embeddings using the specified provider and store them in Chroma."""
145
- if provider == "ollama":
146
- embeddings = get_ollama_embeddings(texts, model)
147
- elif provider == "openai":
148
- embeddings = get_openai_embeddings(texts, model)
149
- elif provider == "anthropic":
150
- embeddings = get_anthropic_embeddings(texts, model)
151
- else:
152
- raise ValueError(f"Unsupported provider: {provider}")
153
-
154
- # Store the embeddings in the relevant Chroma collection
155
- # store_embeddings_for_model(texts, embeddings, model, provider)
156
- return embeddings
157
-
158
-
159
- def get_llm_response(
160
- prompt: str,
161
- provider: str = NPCSH_CHAT_PROVIDER,
162
- model: str = NPCSH_CHAT_MODEL,
163
- images: List[Dict[str, str]] = None,
164
- npc: Any = None,
165
- messages: List[Dict[str, str]] = None,
166
- api_url: str = NPCSH_API_URL,
167
- api_key: str = None,
168
- context=None,
169
- **kwargs,
170
- ):
171
- """This function generates a response using the specified provider and model.
172
- Args:
173
- prompt (str): The prompt for generating the response.
174
- Keyword Args:
175
- provider (str): The provider to use for generating the response.
176
- model (str): The model to use for generating the response.
177
- images (List[Dict[str, str]]): The list of images.
178
- npc (Any): The NPC object.
179
- messages (List[Dict[str, str]]): The list of messages.
180
- api_url (str): The URL of the API endpoint.
181
- Returns:
182
- Any: The response generated by the specified provider and model.
183
- """
184
- if model is not None and provider is not None:
185
- pass
186
-
187
- elif provider is None and model is not None:
188
- provider = lookup_provider(model)
189
-
190
- elif npc is not None:
191
- if npc.provider is not None:
192
- provider = npc.provider
193
- if npc.model is not None:
194
- model = npc.model
195
- if npc.api_url is not None:
196
- api_url = npc.api_url
197
-
198
- else:
199
- provider = "ollama"
200
- if images is not None:
201
- model = "llava:7b"
202
- else:
203
- model = "llama3.2"
204
- # print(provider, model)
205
- # print(provider, model)
206
-
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
217
-
218
-
219
- def get_stream(
220
- messages: List[Dict[str, str]],
221
- provider: str = NPCSH_CHAT_PROVIDER,
222
- model: str = NPCSH_CHAT_MODEL,
223
- npc: Any = None,
224
- images: List[Dict[str, str]] = None,
225
- api_url: str = NPCSH_API_URL,
226
- api_key: str = None,
227
- context=None,
228
- **kwargs,
229
- ) -> List[Dict[str, str]]:
230
- """This function generates a streaming response using the specified provider and model
231
- Args:
232
- messages (List[Dict[str, str]]): The list of messages in the conversation.
233
- Keyword Args:
234
- provider (str): The provider to use for the conversation.
235
- model (str): The model to use for the conversation.
236
- npc (Any): The NPC object.
237
- api_url (str): The URL of the API endpoint.
238
- api_key (str): The API key for accessing the API.
239
- Returns:
240
- List[Dict[str, str]]: The list of messages in the conversation.
241
- """
242
- if model is not None and provider is not None:
243
- pass
244
- elif model is not None and provider is None:
245
- print(provider)
246
- provider = lookup_provider(model)
247
- elif npc is not None:
248
- if npc.provider is not None:
249
- provider = npc.provider
250
- if npc.model is not None:
251
- model = npc.model
252
- if npc.api_url is not None:
253
- api_url = npc.api_url
254
- else:
255
- provider = "ollama"
256
- model = "llama3.2"
257
- # print(model, provider)
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}
302
-
303
-
304
- def get_conversation(
305
- messages: List[Dict[str, str]],
306
- provider: str = NPCSH_CHAT_PROVIDER,
307
- model: str = NPCSH_CHAT_MODEL,
308
- images: List[Dict[str, str]] = None,
309
- npc: Any = None,
310
- api_url: str = NPCSH_API_URL,
311
- context=None,
312
- **kwargs,
313
- ) -> List[Dict[str, str]]:
314
- """This function generates a conversation using the specified provider and model.
315
- Args:
316
- messages (List[Dict[str, str]]): The list of messages in the conversation.
317
- Keyword Args:
318
- provider (str): The provider to use for the conversation.
319
- model (str): The model to use for the conversation.
320
- npc (Any): The NPC object.
321
- Returns:
322
- List[Dict[str, str]]: The list of messages in the conversation.
323
- """
324
- if model is not None and provider is not None:
325
- pass # Use explicitly provided model and provider
326
- elif model is not None and provider is None:
327
- provider = lookup_provider(model)
328
- elif npc is not None and (npc.provider is not None or npc.model is not None):
329
- provider = npc.provider if npc.provider else provider
330
- model = npc.model if npc.model else model
331
- api_url = npc.api_url if npc.api_url else api_url
332
- else:
333
- provider = "ollama"
334
- model = "llava:7b" if images is not None else "llama3.2"
335
-
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
- )
345
-
346
-
347
- def execute_llm_question(
348
- command: str,
349
- model: str = NPCSH_CHAT_MODEL,
350
- provider: str = NPCSH_CHAT_PROVIDER,
351
- api_url: str = NPCSH_API_URL,
352
- api_key: str = None,
353
- npc: Any = None,
354
- messages: List[Dict[str, str]] = None,
355
- retrieved_docs=None,
356
- n_docs: int = 5,
357
- stream: bool = False,
358
- images: List[Dict[str, str]] = None,
359
- context=None,
360
- ):
361
- location = os.getcwd()
362
- if messages is None or len(messages) == 0:
363
- messages = []
364
- messages.append({"role": "user", "content": command})
365
-
366
- # Build context from retrieved documents
367
- if retrieved_docs:
368
- context = ""
369
- for filename, content in retrieved_docs[:n_docs]:
370
- context += f"Document: {filename}\n{content}\n\n"
371
- context_message = f"""
372
- What follows is the context of the text files in the user's directory that are potentially relevant to their request:
373
- {context}
374
-
375
- if the user has asked for code, be sure to include markdown formatting
376
- blocks starting and stopping with ``` to ensure the code is formatted correctly.
377
- """
378
- # Add context as a system message
379
- # messages.append({"role": "system", "content": context_message})
380
-
381
- # Append the user's message to messages
382
-
383
- # Print messages before calling get_conversation for debugging
384
- # print("Messages before get_conversation:", messages)
385
-
386
- # Use the existing messages list
387
- if stream:
388
- # print("beginning stream")
389
- response = get_stream(
390
- messages,
391
- model=model,
392
- provider=provider,
393
- npc=npc,
394
- images=images,
395
- api_url=api_url,
396
- api_key=api_key,
397
- )
398
- # let streamer deal with the diff response data and messages
399
- return response
400
- # print("Response from get_stream:", response)
401
- # full_response = ""
402
- # for chunk in response:
403
- # full_response += chunk
404
- # print(chunk, end="")
405
- # print("end of stream")
406
- # output = full_response
407
- # messages.append({"role": "assistant", "content": output})
408
-
409
- else:
410
- response = get_conversation(
411
- messages,
412
- model=model,
413
- provider=provider,
414
- npc=npc,
415
- images=images,
416
- api_url=api_url,
417
- api_key=api_key,
418
- )
419
-
420
- # Print response from get_conversation for debugging
421
- # print("Response from get_conversation:", response)
422
-
423
- if isinstance(response, str) and "Error" in response:
424
- output = response
425
- elif isinstance(response, list) and len(response) > 0:
426
- messages = response # Update messages with the new conversation
427
- output = response[-1]["content"]
428
- else:
429
- output = "Error: Invalid response from conversation function"
430
-
431
- # render_markdown(output)
432
- # print(f"LLM response: {output}")
433
- # print(f"Messages: {messages}")
434
- # print("type of output", type(output))
435
- return {"messages": messages, "output": output}
436
-
437
-
438
- def execute_llm_command(
439
- command: str,
440
- model: Optional[str] = None,
441
- provider: Optional[str] = None,
442
- api_url: str = NPCSH_API_URL,
443
- api_key: str = None,
444
- npc: Optional[Any] = None,
445
- messages: Optional[List[Dict[str, str]]] = None,
446
- retrieved_docs=None,
447
- n_docs=5,
448
- stream=False,
449
- context=None,
450
- ) -> str:
451
- """This function executes an LLM command.
452
- Args:
453
- command (str): The command to execute.
454
-
455
- Keyword Args:
456
- model (Optional[str]): The model to use for executing the command.
457
- provider (Optional[str]): The provider to use for executing the command.
458
- npc (Optional[Any]): The NPC object.
459
- messages (Optional[List[Dict[str, str]]): The list of messages.
460
- retrieved_docs (Optional): The retrieved documents.
461
- n_docs (int): The number of documents.
462
- Returns:
463
- str: The result of the LLM command.
464
- """
465
-
466
- max_attempts = 5
467
- attempt = 0
468
- subcommands = []
469
- npc_name = npc.name if npc else "sibiji"
470
- location = os.getcwd()
471
- print(f"{npc_name} generating command")
472
- # Create context from retrieved documents
473
- context = ""
474
- if retrieved_docs:
475
- for filename, content in retrieved_docs[:n_docs]:
476
- # print(f"Document: {filename}")
477
- # print(content)
478
- context += f"Document: {filename}\n{content}\n\n"
479
- context = f"Refer to the following documents for context:\n{context}\n\n"
480
- while attempt < max_attempts:
481
- prompt = f"""
482
- A user submitted this query: {command}.
483
- You need to generate a bash command that will accomplish the user's intent.
484
- Respond ONLY with the command that should be executed.
485
- in the json key "bash_command".
486
- You must reply with valid json and nothing else. Do not include markdown formatting
487
- """
488
- if len(context) > 0:
489
- prompt += f"""
490
- What follows is the context of the text files in the user's directory that are potentially relevant to their request
491
- Use these to help inform your decision.
492
- {context}
493
- """
494
- if len(messages) > 0:
495
- prompt += f"""
496
- The following messages have been exchanged between the user and the assistant:
497
- {messages}
498
- """
499
-
500
- response = get_llm_response(
501
- prompt,
502
- model=model,
503
- provider=provider,
504
- api_url=api_url,
505
- api_key=api_key,
506
- messages=[],
507
- npc=npc,
508
- format="json",
509
- context=context,
510
- )
511
-
512
- llm_response = response.get("response", {})
513
- # messages.append({"role": "assistant", "content": llm_response})
514
- # print(f"LLM response type: {type(llm_response)}")
515
- # print(f"LLM response: {llm_response}")
516
-
517
- try:
518
- if isinstance(llm_response, str):
519
- llm_response = json.loads(llm_response)
520
-
521
- if isinstance(llm_response, dict) and "bash_command" in llm_response:
522
- bash_command = llm_response["bash_command"]
523
- else:
524
- raise ValueError("Invalid response format from LLM")
525
- except (json.JSONDecodeError, ValueError) as e:
526
- print(f"Error parsing LLM response: {e}")
527
- attempt += 1
528
- continue
529
-
530
- print(f"LLM suggests the following bash command: {bash_command}")
531
- subcommands.append(bash_command)
532
-
533
- try:
534
- print(f"Running command: {bash_command}")
535
- result = subprocess.run(
536
- bash_command, shell=True, text=True, capture_output=True, check=True
537
- )
538
- print(f"Command executed with output: {result.stdout}")
539
-
540
- prompt = f"""
541
- Here was the output of the result for the {command} inquiry
542
- which ran this bash command {bash_command}:
543
-
544
- {result.stdout}
545
-
546
- Provide a simple response to the user that explains to them
547
- what you did and how it accomplishes what they asked for.
548
- """
549
- if len(context) > 0:
550
- prompt += f"""
551
- What follows is the context of the text files in the user's directory that are potentially relevant to their request
552
- Use these to help inform how you respond.
553
- You must read the context and use it to provide the user with a more helpful answer related to their specific text data.
554
-
555
- CONTEXT:
556
-
557
- {context}
558
- """
559
- messages.append({"role": "user", "content": prompt})
560
- # print(messages, stream)
561
- if stream:
562
- response = get_stream(
563
- messages,
564
- model=model,
565
- provider=provider,
566
- api_url=api_url,
567
- api_key=api_key,
568
- npc=npc,
569
- )
570
- return response
571
-
572
- else:
573
- response = get_llm_response(
574
- prompt,
575
- model=model,
576
- provider=provider,
577
- api_url=api_url,
578
- api_key=api_key,
579
- npc=npc,
580
- messages=messages,
581
- context=context,
582
- )
583
- output = response.get("response", "")
584
-
585
- # render_markdown(output)
586
-
587
- return {"messages": messages, "output": output}
588
- except subprocess.CalledProcessError as e:
589
- print(f"Command failed with error:")
590
- print(e.stderr)
591
-
592
- error_prompt = f"""
593
- The command '{bash_command}' failed with the following error:
594
- {e.stderr}
595
- Please suggest a fix or an alternative command.
596
- Respond with a JSON object containing the key "bash_command" with the suggested command.
597
- Do not include any additional markdown formatting.
598
-
599
- """
600
-
601
- if len(context) > 0:
602
- error_prompt += f"""
603
- What follows is the context of the text files in the user's directory that are potentially relevant to their request
604
- Use these to help inform your decision.
605
- {context}
606
- """
607
-
608
- fix_suggestion = get_llm_response(
609
- error_prompt,
610
- model=model,
611
- provider=provider,
612
- npc=npc,
613
- api_url=api_url,
614
- api_key=api_key,
615
- format="json",
616
- messages=messages,
617
- context=context,
618
- )
619
-
620
- fix_suggestion_response = fix_suggestion.get("response", {})
621
-
622
- try:
623
- if isinstance(fix_suggestion_response, str):
624
- fix_suggestion_response = json.loads(fix_suggestion_response)
625
-
626
- if (
627
- isinstance(fix_suggestion_response, dict)
628
- and "bash_command" in fix_suggestion_response
629
- ):
630
- print(
631
- f"LLM suggests fix: {fix_suggestion_response['bash_command']}"
632
- )
633
- command = fix_suggestion_response["bash_command"]
634
- else:
635
- raise ValueError(
636
- "Invalid response format from LLM for fix suggestion"
637
- )
638
- except (json.JSONDecodeError, ValueError) as e:
639
- print(f"Error parsing LLM fix suggestion: {e}")
640
-
641
- attempt += 1
642
-
643
- return {
644
- "messages": messages,
645
- "output": "Max attempts reached. Unable to execute the command successfully.",
646
- }
647
-
648
-
649
- def check_llm_command(
650
- command: str,
651
- model: str = NPCSH_CHAT_MODEL,
652
- provider: str = NPCSH_CHAT_PROVIDER,
653
- reasoning_model: str = NPCSH_REASONING_MODEL,
654
- reasoning_provider: str = NPCSH_REASONING_PROVIDER,
655
- api_url: str = NPCSH_API_URL,
656
- api_key: str = None,
657
- npc: Any = None,
658
- npc_team: Any = None,
659
- retrieved_docs=None,
660
- messages: List[Dict[str, str]] = None,
661
- images: list = None,
662
- n_docs=5,
663
- stream=False,
664
- context=None,
665
- whisper=False,
666
- ):
667
- """This function checks an LLM command.
668
- Args:
669
- command (str): The command to check.
670
- Keyword Args:
671
- model (str): The model to use for checking the command.
672
- provider (str): The provider to use for checking the command.
673
- npc (Any): The NPC object.
674
- retrieved_docs (Any): The retrieved documents.
675
- n_docs (int): The number of documents.
676
- Returns:
677
- Any: The result of checking the LLM command.
678
- """
679
-
680
- ENTER_REASONING_FLOW = False
681
- if NPCSH_DEFAULT_MODE == "reasoning":
682
- ENTER_REASONING_FLOW = True
683
- if model in available_reasoning_models:
684
- print(
685
- """
686
- Model provided is a reasoning model, defaulting to non reasoning model for
687
- ReAct choices then will enter reasoning flow
688
- """
689
- )
690
- reasoning_model = model
691
- reasoning_provider = provider
692
-
693
- model = NPCSH_CHAT_MODEL
694
- provider = NPCSH_CHAT_PROVIDER
695
- if messages is None:
696
- messages = []
697
-
698
- # print(model, provider, npc)
699
- # Create context from retrieved documents
700
- docs_context = ""
701
-
702
- if retrieved_docs:
703
- for filename, content in retrieved_docs[:n_docs]:
704
- docs_context += f"Document: {filename}\n{content}\n\n"
705
- docs_context = (
706
- f"Refer to the following documents for context:\n{docs_context}\n\n"
707
- )
708
-
709
- prompt = f"""
710
- A user submitted this query: {command}
711
-
712
- Determine the nature of the user's request:
713
-
714
- 1. Should a tool be invoked to fulfill the request?
715
-
716
- 2. Is it a general question that requires an informative answer or a highly specific question that
717
- requires inforrmation on the web?
718
-
719
- 3. Would this question be best answered by an alternative NPC?
720
-
721
- 4. Is it a complex request that actually requires more than one
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.
724
-
725
- 5. is there a need for the user to provide additional input to fulfill the request?
726
-
727
-
728
-
729
- """
730
-
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"""
751
-
752
- {tool_name} : {tool_description} \n
753
- """
754
-
755
- if len(npc.resolved_npcs) == 0:
756
- prompt += "No NPCs available for alternative answers."
757
- else:
758
- prompt += f"""
759
- Available NPCs for alternative answers:
760
-
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})
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}
787
- """
788
- # print("shared_context: " + str(npc.shared_context))
789
- # print(prompt)
790
-
791
- prompt += f"""
792
- In considering how to answer this, consider:
793
-
794
- - Whether more context from the user is required to adequately answer the question.
795
- e.g. if a user asks for a joke about their favorite city but they don't include the city ,
796
- it would be helpful to ask for that information. Similarly, if a user asks to open a browser
797
- and to check the weather in a city, it would be helpful to ask for the city and which website
798
- or source to use.
799
- - Whether a tool should be used.
800
-
801
-
802
- Excluding time-sensitive phenomena or ones that require external data inputs /information,
803
- most general questions can be answered without any
804
- extra tools or agent passes.
805
- Only use tools or pass to other NPCs
806
- when it is obvious that the answer needs to be as up-to-date as possible. For example,
807
- a question about where mount everest is does not necessarily need to be answered by a tool call or an agent pass.
808
- Similarly, if a user asks to explain the plot of the aeneid, this can be answered without a tool call or agent pass.
809
-
810
- If a user were to ask for the current weather in tokyo or the current price of bitcoin or who the mayor of a city is,
811
- then a tool call or agent pass may be appropriate.
812
-
813
- Tools are valuable but their use should be limited and purposeful to
814
- ensure the best user experience.
815
-
816
- Respond with a JSON object containing:
817
- - "action": one of ["invoke_tool", "answer_question", "pass_to_npc", "execute_sequence", "request_input"]
818
- - "tool_name": : if action is "invoke_tool": the name of the tool to use.
819
- else if action is "execute_sequence", a list of tool names to use.
820
- - "explanation": a brief explanation of why you chose this action.
821
- - "npc_name": (if action is "pass_to_npc") the name of the NPC to pass the question , else if action is "execute_sequence", a list of
822
- npcs to pass the question to in order.
823
-
824
-
825
-
826
- Return only the JSON object. Do not include any additional text.
827
-
828
- The format of the JSON object is:
829
- {{
830
- "action": "invoke_tool" | "answer_question" | "pass_to_npc" | "execute_sequence" | "request_input",
831
- "tool_name": "<tool_name(s)_if_applicable>",
832
- "explanation": "<your_explanation>",
833
- "npc_name": "<npc_name(s)_if_applicable>"
834
- }}
835
-
836
- If you execute a sequence, ensure that you have a specified NPC for each tool use.
837
- question answering is not a tool use.
838
- "invoke_tool" should never be used in the list of tools when executing a sequence.
839
- Remember, do not include ANY ADDITIONAL MARKDOWN FORMATTING.
840
- There should be no leading ```json.
841
-
842
- """
843
-
844
- if docs_context:
845
- prompt += f"""
846
- Relevant context from user files.
847
-
848
- {docs_context}
849
-
850
- """
851
- if context:
852
- prompt += f"""
853
- Relevant context from users:
854
-
855
- {context}
856
-
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
-
879
- action_response = get_llm_response(
880
- prompt,
881
- model=model,
882
- provider=provider,
883
- api_url=api_url,
884
- api_key=api_key,
885
- npc=npc,
886
- format="json",
887
- messages=[],
888
- context=None,
889
- )
890
- if "Error" in action_response:
891
- print(f"LLM Error: {action_response['error']}")
892
- return action_response["error"]
893
-
894
- response_content = action_response.get("response", {})
895
-
896
- if isinstance(response_content, str):
897
- try:
898
- response_content_parsed = json.loads(response_content)
899
- except json.JSONDecodeError as e:
900
- print(
901
- f"Invalid JSON received from LLM: {e}. Response was: {response_content}"
902
- )
903
- return f"Error: Invalid JSON from LLM: {response_content}"
904
- else:
905
- response_content_parsed = response_content
906
-
907
- action = response_content_parsed.get("action")
908
- explanation = response_content_parsed.get("explanation")
909
- print(f"action chosen: {action}")
910
- print(f"explanation given: {explanation}")
911
-
912
- # print(response_content)
913
- if response_content_parsed.get("tool_name"):
914
- print(f"tool name: {response_content_parsed.get('tool_name')}")
915
-
916
- if action == "execute_command":
917
- # Pass messages to execute_llm_command
918
- result = execute_llm_command(
919
- command,
920
- model=model,
921
- provider=provider,
922
- api_url=api_url,
923
- api_key=api_key,
924
- messages=[],
925
- npc=npc,
926
- retrieved_docs=retrieved_docs,
927
- stream=stream,
928
- )
929
- if stream:
930
- return result
931
-
932
- output = result.get("output", "")
933
- messages = result.get("messages", messages)
934
- return {"messages": messages, "output": output}
935
-
936
- elif action == "invoke_tool":
937
- tool_name = response_content_parsed.get("tool_name")
938
- # print(npc)
939
- print(f"tool name: {tool_name}")
940
- result = handle_tool_call(
941
- command,
942
- tool_name,
943
- model=model,
944
- provider=provider,
945
- api_url=api_url,
946
- api_key=api_key,
947
- messages=messages,
948
- npc=npc,
949
- retrieved_docs=retrieved_docs,
950
- stream=stream,
951
- )
952
- if stream:
953
- return result
954
- messages = result.get("messages", messages)
955
- output = result.get("output", "")
956
- return {"messages": messages, "output": output}
957
-
958
- elif action == "answer_question":
959
- if ENTER_REASONING_FLOW:
960
- print("entering reasoning flow")
961
- result = enter_reasoning_human_in_the_loop(
962
- messages, reasoning_model, reasoning_provider
963
- )
964
- else:
965
- result = execute_llm_question(
966
- command,
967
- model=model,
968
- provider=provider,
969
- api_url=api_url,
970
- api_key=api_key,
971
- messages=messages,
972
- npc=npc,
973
- retrieved_docs=retrieved_docs,
974
- stream=stream,
975
- images=images,
976
- )
977
-
978
- if stream:
979
- return result
980
- messages = result.get("messages", messages)
981
- output = result.get("output", "")
982
- return {"messages": messages, "output": output}
983
- elif action == "pass_to_npc":
984
- npc_to_pass = response_content_parsed.get("npc_name")
985
- # print(npc)
986
- # get tge actual npc object from the npc.resolved_npcs
987
- npc_to_pass_obj = None
988
- for npc_obj in npc.resolved_npcs:
989
- if npc_to_pass in npc_obj:
990
- npc_to_pass_obj = npc_obj[npc_to_pass]
991
- break
992
- return npc.handle_agent_pass(
993
- npc_to_pass_obj,
994
- command,
995
- messages=messages,
996
- retrieved_docs=retrieved_docs,
997
- n_docs=n_docs,
998
- )
999
- elif action == "request_input":
1000
- explanation = response_content_parsed.get("explanation")
1001
-
1002
- request_input = handle_request_input(
1003
- f"Explanation from check_llm_command: {explanation} \n for the user input command: {command}",
1004
- model=model,
1005
- provider=provider,
1006
- )
1007
- # pass it back through with the request input added to the end of the messages
1008
- # so that we can re-pass the result through the check_llm_command.
1009
-
1010
- messages.append(
1011
- {
1012
- "role": "assistant",
1013
- "content": f"""its clear that extra input is required.
1014
- could you please provide it? Here is the reason:
1015
-
1016
- {explanation},
1017
-
1018
- and the prompt: {command}""",
1019
- }
1020
- )
1021
- messages.append(
1022
- {
1023
- "role": "user",
1024
- "content": command + " \n \n \n extra context: " + request_input,
1025
- }
1026
- )
1027
-
1028
- return check_llm_command(
1029
- command + " \n \n \n extra context: " + request_input,
1030
- model=model,
1031
- provider=provider,
1032
- api_url=api_url,
1033
- api_key=api_key,
1034
- npc=npc,
1035
- messages=messages,
1036
- retrieved_docs=retrieved_docs,
1037
- n_docs=n_docs,
1038
- stream=stream,
1039
- )
1040
-
1041
- elif action == "execute_sequence":
1042
- tool_names = response_content_parsed.get("tool_name")
1043
- npc_names = response_content_parsed.get("npc_name")
1044
-
1045
- # print(npc_names)
1046
- npcs = []
1047
- # print(tool_names, npc_names)
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)
1053
- for npc_name in npc_names:
1054
- for npc_obj in npc.resolved_npcs:
1055
- if npc_name in npc_obj:
1056
- npcs.append(npc_obj[npc_name])
1057
- break
1058
- if len(npcs) < len(tool_names):
1059
- npcs.append(npc)
1060
-
1061
- output = ""
1062
- results_tool_calls = []
1063
-
1064
- if len(tool_names) > 0:
1065
- for npc_obj, tool_name in zip(npcs, tool_names):
1066
- result = handle_tool_call(
1067
- command,
1068
- tool_name,
1069
- model=model,
1070
- provider=provider,
1071
- api_url=api_url,
1072
- api_key=api_key,
1073
- messages=messages,
1074
- npc=npc_obj,
1075
- retrieved_docs=retrieved_docs,
1076
- stream=stream,
1077
- )
1078
- # print(result)
1079
- results_tool_calls.append(result)
1080
- messages = result.get("messages", messages)
1081
- output += result.get("output", "")
1082
- # print(results_tool_calls)
1083
- else:
1084
- for npc_obj in npcs:
1085
- result = npc.handle_agent_pass(
1086
- npc_obj,
1087
- command,
1088
- messages=messages,
1089
- retrieved_docs=retrieved_docs,
1090
- n_docs=n_docs,
1091
- shared_context=npc.shared_context,
1092
- )
1093
-
1094
- messages = result.get("messages", messages)
1095
- results_tool_calls.append(result.get("response"))
1096
- # print(messages[-1])
1097
- # import pdb
1098
-
1099
- # pdb.set_trace()
1100
-
1101
- return {"messages": messages, "output": output}
1102
- else:
1103
- print("Error: Invalid action in LLM response")
1104
- return "Error: Invalid action in LLM response"
1105
-
1106
-
1107
- def handle_tool_call(
1108
- command: str,
1109
- tool_name: str,
1110
- model: str = NPCSH_CHAT_MODEL,
1111
- provider: str = NPCSH_CHAT_PROVIDER,
1112
- api_url: str = NPCSH_API_URL,
1113
- api_key: str = None,
1114
- messages: List[Dict[str, str]] = None,
1115
- npc: Any = None,
1116
- retrieved_docs=None,
1117
- n_docs: int = 5,
1118
- stream=False,
1119
- n_attempts=3,
1120
- attempt=0,
1121
- context=None,
1122
- ) -> Union[str, Dict[str, Any]]:
1123
- """This function handles a tool call.
1124
- Args:
1125
- command (str): The command.
1126
- tool_name (str): The tool name.
1127
- Keyword Args:
1128
- model (str): The model to use for handling the tool call.
1129
- provider (str): The provider to use for handling the tool call.
1130
- messages (List[Dict[str, str]]): The list of messages.
1131
- npc (Any): The NPC object.
1132
- retrieved_docs (Any): The retrieved documents.
1133
- n_docs (int): The number of documents.
1134
- Returns:
1135
- Union[str, Dict[str, Any]]: The result of handling
1136
- the tool call.
1137
-
1138
- """
1139
- # print(npc)
1140
- print("handling tool call")
1141
- if not npc:
1142
- print(
1143
- f"No tools available for NPC '{npc.name}' or tools_dict is empty. Available tools: {available_tools}"
1144
- )
1145
- return f"No tools are available for NPC '{npc.name or 'default'}'."
1146
-
1147
- if tool_name not in npc.all_tools_dict and tool_name not in npc.tools_dict:
1148
- print("not available")
1149
- print(f"Tool '{tool_name}' not found in NPC's tools_dict.")
1150
- print("available tools", npc.all_tools_dict)
1151
- return f"Tool '{tool_name}' not found."
1152
-
1153
- if tool_name in npc.all_tools_dict:
1154
- tool = npc.all_tools_dict[tool_name]
1155
- elif tool_name in npc.tools_dict:
1156
- tool = npc.tools_dict[tool_name]
1157
- print(f"Tool found: {tool.tool_name}")
1158
- jinja_env = Environment(loader=FileSystemLoader("."), undefined=Undefined)
1159
-
1160
- prompt = f"""
1161
- The user wants to use the tool '{tool_name}' with the following request:
1162
- '{command}'
1163
- Here is the tool file:
1164
- ```
1165
- {tool.to_dict()}
1166
- ```
1167
-
1168
- Please extract the required inputs for the tool as a JSON object.
1169
- They must be exactly as they are named in the tool.
1170
- Return only the JSON object without any markdown formatting.
1171
-
1172
- """
1173
-
1174
- if npc and hasattr(npc, "shared_context"):
1175
- if npc.shared_context.get("dataframes"):
1176
- context_info = "\nAvailable dataframes:\n"
1177
- for df_name in npc.shared_context["dataframes"].keys():
1178
- context_info += f"- {df_name}\n"
1179
- prompt += f"""Here is contextual info that may affect your choice: {context_info}
1180
- """
1181
- if context is not None:
1182
- prompt += f"Here is some additional context: {context}"
1183
-
1184
- # print(prompt)
1185
-
1186
- # print(
1187
- # print(prompt)
1188
- response = get_llm_response(
1189
- prompt,
1190
- format="json",
1191
- model=model,
1192
- provider=provider,
1193
- api_url=api_url,
1194
- api_key=api_key,
1195
- npc=npc,
1196
- )
1197
- try:
1198
- # Clean the response of markdown formatting
1199
- response_text = response.get("response", "{}")
1200
- if isinstance(response_text, str):
1201
- response_text = (
1202
- response_text.replace("```json", "").replace("```", "").strip()
1203
- )
1204
-
1205
- # Parse the cleaned response
1206
- if isinstance(response_text, dict):
1207
- input_values = response_text
1208
- else:
1209
- input_values = json.loads(response_text)
1210
- # print(f"Extracted inputs: {input_values}")
1211
- except json.JSONDecodeError as e:
1212
- print(f"Error decoding input values: {e}. Raw response: {response}")
1213
- return f"Error extracting inputs for tool '{tool_name}'"
1214
- # Input validation (example):
1215
- required_inputs = tool.inputs
1216
- missing_inputs = []
1217
- for inp in required_inputs:
1218
- if not isinstance(inp, dict):
1219
- # dicts contain the keywords so its fine if theyre missing from the inputs.
1220
- if inp not in input_values or input_values[inp] == "":
1221
- missing_inputs.append(inp)
1222
- if len(missing_inputs) > 0:
1223
- # print(f"Missing required inputs for tool '{tool_name}': {missing_inputs}")
1224
- if attempt < n_attempts:
1225
- print(f"attempt {attempt+1} to generate inputs failed, trying again")
1226
- print("missing inputs", missing_inputs)
1227
- # print("llm response", response)
1228
- print("input values", input_values)
1229
- return handle_tool_call(
1230
- command,
1231
- tool_name,
1232
- model=model,
1233
- provider=provider,
1234
- messages=messages,
1235
- npc=npc,
1236
- api_url=api_url,
1237
- api_key=api_key,
1238
- retrieved_docs=retrieved_docs,
1239
- n_docs=n_docs,
1240
- stream=stream,
1241
- attempt=attempt + 1,
1242
- n_attempts=n_attempts,
1243
- )
1244
- return {
1245
- "output": f"Missing inputs for tool '{tool_name}': {missing_inputs}",
1246
- "messages": messages,
1247
- }
1248
-
1249
- # try:
1250
- print("Executing tool with input values:", input_values)
1251
-
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:
1268
- # diagnose_problem = get_llm_response(
1269
- ## f"""a problem has occurred.
1270
- # Please provide a diagnosis of the problem and a suggested #fix.
1271
-
1272
- # The tool call failed with this error:
1273
- # {e}
1274
- # Please return a json object containing two fields
1275
- ## -problem
1276
- # -suggested solution.
1277
- # do not include any additional markdown formatting or #leading json tags
1278
-
1279
- # """,
1280
- # model=model,
1281
- # provider=provider,
1282
- # npc=npc,
1283
- ## api_url=api_url,
1284
- # api_ley=api_key,
1285
- # format="json",
1286
- # )
1287
- # print(e)
1288
- # problem = diagnose_problem.get("response", {}).get("problem")
1289
- # suggested_solution = diagnose_problem.get("response", {}).get(
1290
- # "suggested_solution"
1291
- # )
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
- '''
1318
- if stream:
1319
- return tool_output
1320
- # print(f"Tool output: {tool_output}")
1321
- # render_markdown(str(tool_output))
1322
- if messages is not None: # Check if messages is not None
1323
- messages.append({"role": "assistant", "content": tool_output})
1324
- return {"messages": messages, "output": tool_output}
1325
- # except Exception as e:
1326
- # print(f"Error executing tool {tool_name}: {e}")
1327
- # return f"Error executing tool {tool_name}: {e}"
1328
-
1329
-
1330
- def execute_data_operations(
1331
- query: str,
1332
- dataframes: Dict[str, pd.DataFrame],
1333
- npc: Any = None,
1334
- db_path: str = "~/npcsh_history.db",
1335
- ):
1336
- """This function executes data operations.
1337
- Args:
1338
- query (str): The query to execute.
1339
-
1340
- dataframes (Dict[str, pd.DataFrame]): The dictionary of dataframes.
1341
- Keyword Args:
1342
- npc (Any): The NPC object.
1343
- db_path (str): The database path.
1344
- Returns:
1345
- Any: The result of the data operations.
1346
- """
1347
-
1348
- location = os.getcwd()
1349
- db_path = os.path.expanduser(db_path)
1350
-
1351
- try:
1352
- try:
1353
- # Create a safe namespace for pandas execution
1354
- namespace = {
1355
- "pd": pd,
1356
- "np": np,
1357
- "plt": plt,
1358
- **dataframes, # This includes all our loaded dataframes
1359
- }
1360
- # Execute the query
1361
- result = eval(query, namespace)
1362
-
1363
- # Handle the result
1364
- if isinstance(result, (pd.DataFrame, pd.Series)):
1365
- # render_markdown(result)
1366
- return result, "pd"
1367
- elif isinstance(result, plt.Figure):
1368
- plt.show()
1369
- return result, "pd"
1370
- elif result is not None:
1371
- # render_markdown(result)
1372
-
1373
- return result, "pd"
1374
-
1375
- except Exception as exec_error:
1376
- print(f"Pandas Error: {exec_error}")
1377
-
1378
- # 2. Try SQL
1379
- # print(db_path)
1380
- try:
1381
- with sqlite3.connect(db_path) as conn:
1382
- cursor = conn.cursor()
1383
- print(query)
1384
- print(get_available_tables(db_path))
1385
-
1386
- cursor.execute(query)
1387
- # get available tables
1388
-
1389
- result = cursor.fetchall()
1390
- if result:
1391
- for row in result:
1392
- print(row)
1393
- return result, "sql"
1394
- except Exception as e:
1395
- print(f"SQL Error: {e}")
1396
-
1397
- # 3. Try R
1398
- try:
1399
- result = subprocess.run(
1400
- ["Rscript", "-e", query], capture_output=True, text=True
1401
- )
1402
- if result.returncode == 0:
1403
- print(result.stdout)
1404
- return result.stdout, "r"
1405
- else:
1406
- print(f"R Error: {result.stderr}")
1407
- except Exception as e:
1408
- pass
1409
-
1410
- # If all engines fail, ask the LLM
1411
- print("Direct execution failed. Asking LLM for SQL query...")
1412
- llm_prompt = f"""
1413
- The user entered the following query which could not be executed directly using pandas, SQL, R, Scala, or PySpark:
1414
- ```
1415
- {query}
1416
- ```
1417
-
1418
- The available tables in the SQLite database at {db_path} are:
1419
- ```sql
1420
- {get_available_tables(db_path)}
1421
- ```
1422
-
1423
- Please provide a valid SQL query that accomplishes the user's intent. If the query requires data from a file, provide instructions on how to load the data into a table first.
1424
- Return only the SQL query, or instructions for loading data followed by the SQL query.
1425
- """
1426
-
1427
- llm_response = get_llm_response(llm_prompt, npc=npc)
1428
-
1429
- print(f"LLM suggested SQL: {llm_response}")
1430
- command = llm_response.get("response", "")
1431
- if command == "":
1432
- return "LLM did not provide a valid SQL query.", None
1433
- # Execute the LLM-generated SQL
1434
- try:
1435
- with sqlite3.connect(db_path) as conn:
1436
- cursor = conn.cursor()
1437
- cursor.execute(command)
1438
- result = cursor.fetchall()
1439
- if result:
1440
- for row in result:
1441
- print(row)
1442
- return result, "llm"
1443
- except Exception as e:
1444
- print(f"Error executing LLM-generated SQL: {e}")
1445
- return f"Error executing LLM-generated SQL: {e}", None
1446
-
1447
- except Exception as e:
1448
- print(f"Error executing query: {e}")
1449
- return f"Error executing query: {e}", None
1450
-
1451
-
1452
- def check_output_sufficient(
1453
- request: str,
1454
- data: pd.DataFrame,
1455
- query: str,
1456
- model: str = None,
1457
- provider: str = None,
1458
- npc: Any = None,
1459
- ) -> Dict[str, Any]:
1460
- """
1461
- Check if the query results are sufficient to answer the user's request.
1462
- """
1463
- prompt = f"""
1464
- Given:
1465
- - User request: {request}
1466
- - Query executed: {query}
1467
- - Results:
1468
- Summary: {data.describe()}
1469
- data schema: {data.dtypes}
1470
- Sample: {data.head()}
1471
-
1472
- Is this result sufficient to answer the user's request?
1473
- Return JSON with:
1474
- {{
1475
- "IS_SUFFICIENT": <boolean>,
1476
- "EXPLANATION": <string : If the answer is not sufficient specify what else is necessary.
1477
- IFF the answer is sufficient, provide a response that can be returned to the user as an explanation that answers their question.
1478
- The explanation should use the results to answer their question as long as they wouold be useful to the user.
1479
- For example, it is not useful to report on the "average/min/max/std ID" or the "min/max/std/average of a string column".
1480
-
1481
- Be smart about what you report.
1482
- It should not be a conceptual or abstract summary of the data.
1483
- It should not unnecessarily bring up a need for more data.
1484
- You should write it in a tone that answers the user request. Do not spout unnecessary self-referential fluff like "This information gives a clear overview of the x landscape".
1485
- >
1486
- }}
1487
- DO NOT include markdown formatting or ```json tags.
1488
-
1489
- """
1490
-
1491
- response = get_llm_response(
1492
- prompt, format="json", model=model, provider=provider, npc=npc
1493
- )
1494
-
1495
- # Clean response if it's a string
1496
- result = response.get("response", {})
1497
- if isinstance(result, str):
1498
- result = result.replace("```json", "").replace("```", "").strip()
1499
- try:
1500
- result = json.loads(result)
1501
- except json.JSONDecodeError:
1502
- return {"IS_SUFFICIENT": False, "EXPLANATION": "Failed to parse response"}
1503
-
1504
- return result
1505
-
1506
-
1507
- def process_data_output(
1508
- llm_response: Dict[str, Any],
1509
- db_conn,
1510
- request: str,
1511
- tables: str = None,
1512
- history: str = None,
1513
- npc: Any = None,
1514
- model: str = None,
1515
- provider: str = None,
1516
- ) -> Dict[str, Any]:
1517
- """
1518
- Process the LLM's response to a data request and execute the appropriate query.
1519
- """
1520
- try:
1521
- choice = llm_response.get("choice")
1522
- query = llm_response.get("query")
1523
-
1524
- if not query:
1525
- return {"response": "No query provided", "code": 400}
1526
-
1527
- # Create SQLAlchemy engine based on connection type
1528
- if "psycopg2" in db_conn.__class__.__module__:
1529
- engine = create_engine("postgresql://caug:gobears@localhost/npc_test")
1530
- else:
1531
- engine = create_engine("sqlite:///test_sqlite.db")
1532
-
1533
- if choice == 1: # Direct answer query
1534
- try:
1535
- df = pd.read_sql_query(query, engine)
1536
- result = check_output_sufficient(
1537
- request, df, query, model=model, provider=provider, npc=npc
1538
- )
1539
-
1540
- if result.get("IS_SUFFICIENT"):
1541
- return {"response": result["EXPLANATION"], "data": df, "code": 200}
1542
- return {
1543
- "response": f"Results insufficient: {result.get('EXPLANATION')}",
1544
- "code": 400,
1545
- }
1546
-
1547
- except Exception as e:
1548
- return {"response": f"Query execution failed: {str(e)}", "code": 400}
1549
-
1550
- elif choice == 2: # Exploratory query
1551
- try:
1552
- df = pd.read_sql_query(query, engine)
1553
- extra_context = f"""
1554
- Exploratory query results:
1555
- Query: {query}
1556
- Results summary: {df.describe()}
1557
- Sample data: {df.head()}
1558
- """
1559
-
1560
- return get_data_response(
1561
- request,
1562
- db_conn,
1563
- tables=tables,
1564
- extra_context=extra_context,
1565
- history=history,
1566
- model=model,
1567
- provider=provider,
1568
- npc=npc,
1569
- )
1570
-
1571
- except Exception as e:
1572
- return {"response": f"Exploratory query failed: {str(e)}", "code": 400}
1573
-
1574
- return {"response": "Invalid choice specified", "code": 400}
1575
-
1576
- except Exception as e:
1577
- return {"response": f"Processing error: {str(e)}", "code": 400}
1578
-
1579
-
1580
- def get_data_response(
1581
- request: str,
1582
- db_conn,
1583
- tables: str = None,
1584
- n_try_freq: int = 5,
1585
- extra_context: str = None,
1586
- history: str = None,
1587
- model: str = None,
1588
- provider: str = None,
1589
- npc: Any = None,
1590
- max_retries: int = 3,
1591
- ) -> Dict[str, Any]:
1592
- """
1593
- Generate a response to a data request, with retries for failed attempts.
1594
- """
1595
-
1596
- # Extract schema information based on connection type
1597
- schema_info = ""
1598
- if "psycopg2" in db_conn.__class__.__module__:
1599
- cursor = db_conn.cursor()
1600
- # Get all tables and their columns
1601
- cursor.execute(
1602
- """
1603
- SELECT
1604
- t.table_name,
1605
- array_agg(c.column_name || ' ' || c.data_type) as columns,
1606
- array_agg(
1607
- CASE
1608
- WHEN tc.constraint_type = 'FOREIGN KEY'
1609
- THEN kcu.column_name || ' REFERENCES ' || ccu.table_name || '.' || ccu.column_name
1610
- ELSE NULL
1611
- END
1612
- ) as foreign_keys
1613
- FROM information_schema.tables t
1614
- JOIN information_schema.columns c ON t.table_name = c.table_name
1615
- LEFT JOIN information_schema.table_constraints tc
1616
- ON t.table_name = tc.table_name
1617
- AND tc.constraint_type = 'FOREIGN KEY'
1618
- LEFT JOIN information_schema.key_column_usage kcu
1619
- ON tc.constraint_name = kcu.constraint_name
1620
- LEFT JOIN information_schema.constraint_column_usage ccu
1621
- ON tc.constraint_name = ccu.constraint_name
1622
- WHERE t.table_schema = 'public'
1623
- GROUP BY t.table_name;
1624
- """
1625
- )
1626
- for table, columns, fks in cursor.fetchall():
1627
- schema_info += f"\nTable {table}:\n"
1628
- schema_info += "Columns:\n"
1629
- for col in columns:
1630
- schema_info += f" - {col}\n"
1631
- if any(fk for fk in fks if fk is not None):
1632
- schema_info += "Foreign Keys:\n"
1633
- for fk in fks:
1634
- if fk:
1635
- schema_info += f" - {fk}\n"
1636
-
1637
- elif "sqlite3" in db_conn.__class__.__module__:
1638
- cursor = db_conn.cursor()
1639
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
1640
- tables = cursor.fetchall()
1641
- for (table_name,) in tables:
1642
- schema_info += f"\nTable {table_name}:\n"
1643
- cursor.execute(f"PRAGMA table_info({table_name});")
1644
- columns = cursor.fetchall()
1645
- schema_info += "Columns:\n"
1646
- for col in columns:
1647
- schema_info += f" - {col[1]} {col[2]}\n"
1648
-
1649
- cursor.execute(f"PRAGMA foreign_key_list({table_name});")
1650
- foreign_keys = cursor.fetchall()
1651
- if foreign_keys:
1652
- schema_info += "Foreign Keys:\n"
1653
- for fk in foreign_keys:
1654
- schema_info += f" - {fk[3]} REFERENCES {fk[2]}({fk[4]})\n"
1655
-
1656
- prompt = f"""
1657
- User request: {request}
1658
-
1659
- Database Schema:
1660
- {schema_info}
1661
-
1662
- {extra_context or ''}
1663
- {f'Query history: {history}' if history else ''}
1664
-
1665
- Provide either:
1666
- 1) An SQL query to directly answer the request
1667
- 2) An exploratory query to gather more information
1668
-
1669
- Return JSON with:
1670
- {{
1671
- "query": <sql query string>,
1672
- "choice": <1 or 2>,
1673
- "explanation": <reason for choice>
1674
- }}
1675
- DO NOT include markdown formatting or ```json tags.
1676
- """
1677
-
1678
- failures = []
1679
- for attempt in range(max_retries):
1680
- # try:
1681
- llm_response = get_llm_response(
1682
- prompt, npc=npc, format="json", model=model, provider=provider
1683
- )
1684
-
1685
- # Clean response if it's a string
1686
- response_data = llm_response.get("response", {})
1687
- if isinstance(response_data, str):
1688
- response_data = (
1689
- response_data.replace("```json", "").replace("```", "").strip()
1690
- )
1691
- try:
1692
- response_data = json.loads(response_data)
1693
- except json.JSONDecodeError:
1694
- failures.append("Invalid JSON response")
1695
- continue
1696
-
1697
- result = process_data_output(
1698
- response_data,
1699
- db_conn,
1700
- request,
1701
- tables=tables,
1702
- history=failures,
1703
- npc=npc,
1704
- model=model,
1705
- provider=provider,
1706
- )
1707
-
1708
- if result["code"] == 200:
1709
- return result
1710
-
1711
- failures.append(result["response"])
1712
-
1713
- if attempt == max_retries - 1:
1714
- return {
1715
- "response": f"Failed after {max_retries} attempts. Errors: {'; '.join(failures)}",
1716
- "code": 400,
1717
- }
1718
-
1719
- # except Exception as e:
1720
- # failures.append(str(e))
1721
-
1722
-
1723
- def enter_chat_human_in_the_loop(
1724
- messages: List[Dict[str, str]],
1725
- reasoning_model: str = NPCSH_REASONING_MODEL,
1726
- reasoning_provider: str = NPCSH_REASONING_PROVIDER,
1727
- chat_model: str = NPCSH_CHAT_MODEL,
1728
- chat_provider: str = NPCSH_CHAT_PROVIDER,
1729
- npc: Any = None,
1730
- answer_only: bool = False,
1731
- context=None,
1732
- ) -> Generator[str, None, None]:
1733
- """
1734
- Stream responses while checking for think tokens and handling human input when needed.
1735
-
1736
- Args:
1737
- messages: List of conversation messages
1738
- model: LLM model to use
1739
- provider: Model provider
1740
- npc: NPC instance if applicable
1741
-
1742
- Yields:
1743
- Streamed response chunks
1744
- """
1745
- # Get the initial stream
1746
- if answer_only:
1747
- messages[-1]["content"] = (
1748
- messages[-1]["content"].replace(
1749
- "Think first though and use <think> tags", ""
1750
- )
1751
- + " Do not think just answer. "
1752
- )
1753
- else:
1754
- messages[-1]["content"] = (
1755
- messages[-1]["content"]
1756
- + " Think first though and use <think> tags. "
1757
- )
1758
-
1759
- response_stream = get_stream(
1760
- messages,
1761
- model=reasoning_model,
1762
- provider=reasoning_provider,
1763
- npc=npc,
1764
- context=context,
1765
- )
1766
-
1767
- thoughts = []
1768
- response_chunks = []
1769
- in_think_block = False
1770
-
1771
- for chunk in response_stream:
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
1801
- if reasoning_provider == "ollama":
1802
- chunk_content = chunk.get("message", {}).get("content", "")
1803
- elif reasoning_provider == "openai" or reasoning_provider == "deepseek":
1804
- chunk_content = "".join(
1805
- choice.delta.content
1806
- for choice in chunk.choices
1807
- if choice.delta.content is not None
1808
- )
1809
- elif reasoning_provider == "anthropic":
1810
- if chunk.type == "content_block_delta":
1811
- chunk_content = chunk.delta.text
1812
- else:
1813
- chunk_content = ""
1814
- else:
1815
- chunk_content = str(chunk)
1816
-
1817
- response_chunks.append(chunk_content)
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)
1829
- yield chunk
1830
-
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
1854
-
1855
- if not in_think_block:
1856
- yield chunk
1857
-
1858
-
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.
1871
-
1872
- Args:
1873
- messages: List of conversation messages
1874
- model: LLM model to use
1875
- provider: Model provider
1876
- npc: NPC instance if applicable
1877
-
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
1993
-
1994
-
1995
- def handle_request_input(
1996
- context: str,
1997
- model: str = NPCSH_CHAT_MODEL,
1998
- provider: str = NPCSH_CHAT_PROVIDER,
1999
- whisper: bool = False,
2000
- ):
2001
- """
2002
- Analyze text and decide what to request from the user
2003
- """
2004
- prompt = f"""
2005
- Analyze the text:
2006
- {context}
2007
- and determine what additional input is needed.
2008
- Return a JSON object with:
2009
- {{
2010
- "input_needed": boolean,
2011
- "request_reason": string explaining why input is needed,
2012
- "request_prompt": string to show user if input needed
2013
- }}
2014
-
2015
- Do not include any additional markdown formatting or leading ```json tags. Your response
2016
- must be a valid JSON object.
2017
- """
2018
-
2019
- response = get_llm_response(
2020
- prompt,
2021
- model=model,
2022
- provider=provider,
2023
- messages=[],
2024
- format="json",
2025
- )
2026
-
2027
- result = response.get("response", {})
2028
- if isinstance(result, str):
2029
- result = json.loads(result)
2030
-
2031
- user_input = request_user_input(
2032
- {"reason": result["request_reason"], "prompt": result["request_prompt"]},
2033
- )
2034
- return user_input
2035
-
2036
-
2037
- def analyze_thoughts_for_input(
2038
- thought_text: str,
2039
- model: str = NPCSH_CHAT_MODEL,
2040
- provider: str = NPCSH_CHAT_PROVIDER,
2041
- api_url: str = NPCSH_API_URL,
2042
- api_key: str = None,
2043
- ) -> Optional[Dict[str, str]]:
2044
- """
2045
- Analyze accumulated thoughts to determine if user input is needed.
2046
-
2047
- Args:
2048
- thought_text: Accumulated text from think block
2049
- messages: Conversation history
2050
-
2051
- Returns:
2052
- Dict with input request details if needed, None otherwise
2053
- """
2054
-
2055
- prompt = (
2056
- f"""
2057
- Analyze these thoughts:
2058
- {thought_text}
2059
- and determine if additional user input would be helpful.
2060
- Return a JSON object with:"""
2061
- + """
2062
- {
2063
- "input_needed": boolean,
2064
- "request_reason": string explaining why input is needed,
2065
- "request_prompt": string to show user if input needed
2066
- }
2067
- Consider things like:
2068
- - Ambiguity in the user's request
2069
- - Missing context that would help provide a better response
2070
- - Clarification needed about user preferences/requirements
2071
- Only request input if it would meaningfully improve the response.
2072
- Do not include any additional markdown formatting or leading ```json tags. Your response
2073
- must be a valid JSON object.
2074
- """
2075
- )
2076
-
2077
- response = get_llm_response(
2078
- prompt,
2079
- model=model,
2080
- provider=provider,
2081
- api_url=api_url,
2082
- api_key=api_key,
2083
- messages=[],
2084
- format="json",
2085
- )
2086
-
2087
- result = response.get("response", {})
2088
- if isinstance(result, str):
2089
- result = json.loads(result)
2090
-
2091
- if result.get("input_needed"):
2092
- return {
2093
- "reason": result["request_reason"],
2094
- "prompt": result["request_prompt"],
2095
- }
2096
-
2097
-
2098
- def request_user_input(input_request: Dict[str, str]) -> str:
2099
- """
2100
- Request and get input from user.
2101
-
2102
- Args:
2103
- input_request: Dict with reason and prompt for input
2104
-
2105
- Returns:
2106
- User's input text
2107
- """
2108
- print(f"\nAdditional input needed: {input_request['reason']}")
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
- )