npcsh 0.3.32__py3-none-any.whl → 1.0.1__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.
- npcsh/_state.py +942 -0
- npcsh/alicanto.py +1074 -0
- npcsh/guac.py +785 -0
- npcsh/mcp_helpers.py +357 -0
- npcsh/mcp_npcsh.py +822 -0
- npcsh/mcp_server.py +184 -0
- npcsh/npc.py +218 -0
- npcsh/npcsh.py +1161 -0
- npcsh/plonk.py +387 -269
- npcsh/pti.py +234 -0
- npcsh/routes.py +958 -0
- npcsh/spool.py +315 -0
- npcsh/wander.py +550 -0
- npcsh/yap.py +573 -0
- npcsh-1.0.1.dist-info/METADATA +596 -0
- npcsh-1.0.1.dist-info/RECORD +21 -0
- {npcsh-0.3.32.dist-info → npcsh-1.0.1.dist-info}/WHEEL +1 -1
- npcsh-1.0.1.dist-info/entry_points.txt +9 -0
- {npcsh-0.3.32.dist-info → npcsh-1.0.1.dist-info}/licenses/LICENSE +1 -1
- npcsh/audio.py +0 -569
- npcsh/audio_gen.py +0 -1
- npcsh/cli.py +0 -543
- npcsh/command_history.py +0 -566
- npcsh/conversation.py +0 -54
- npcsh/data_models.py +0 -46
- npcsh/dataframes.py +0 -171
- npcsh/embeddings.py +0 -168
- npcsh/helpers.py +0 -646
- npcsh/image.py +0 -298
- npcsh/image_gen.py +0 -79
- npcsh/knowledge_graph.py +0 -1006
- npcsh/llm_funcs.py +0 -2195
- npcsh/load_data.py +0 -83
- npcsh/main.py +0 -5
- npcsh/model_runner.py +0 -189
- npcsh/npc_compiler.py +0 -2879
- npcsh/npc_sysenv.py +0 -388
- npcsh/npc_team/assembly_lines/test_pipeline.py +0 -181
- npcsh/npc_team/corca.npc +0 -13
- npcsh/npc_team/foreman.npc +0 -7
- npcsh/npc_team/npcsh.ctx +0 -11
- npcsh/npc_team/sibiji.npc +0 -4
- npcsh/npc_team/templates/analytics/celona.npc +0 -0
- npcsh/npc_team/templates/hr_support/raone.npc +0 -0
- npcsh/npc_team/templates/humanities/eriane.npc +0 -4
- npcsh/npc_team/templates/it_support/lineru.npc +0 -0
- npcsh/npc_team/templates/marketing/slean.npc +0 -4
- npcsh/npc_team/templates/philosophy/maurawa.npc +0 -0
- npcsh/npc_team/templates/sales/turnic.npc +0 -4
- npcsh/npc_team/templates/software/welxor.npc +0 -0
- npcsh/npc_team/tools/bash_executer.tool +0 -32
- npcsh/npc_team/tools/calculator.tool +0 -8
- npcsh/npc_team/tools/code_executor.tool +0 -16
- npcsh/npc_team/tools/generic_search.tool +0 -27
- npcsh/npc_team/tools/image_generation.tool +0 -25
- npcsh/npc_team/tools/local_search.tool +0 -149
- npcsh/npc_team/tools/npcsh_executor.tool +0 -9
- npcsh/npc_team/tools/screen_cap.tool +0 -27
- npcsh/npc_team/tools/sql_executor.tool +0 -26
- npcsh/response.py +0 -272
- npcsh/search.py +0 -252
- npcsh/serve.py +0 -1467
- npcsh/shell.py +0 -524
- npcsh/shell_helpers.py +0 -3919
- npcsh/stream.py +0 -233
- npcsh/video.py +0 -52
- npcsh/video_gen.py +0 -69
- npcsh-0.3.32.data/data/npcsh/npc_team/bash_executer.tool +0 -32
- npcsh-0.3.32.data/data/npcsh/npc_team/calculator.tool +0 -8
- npcsh-0.3.32.data/data/npcsh/npc_team/celona.npc +0 -0
- npcsh-0.3.32.data/data/npcsh/npc_team/code_executor.tool +0 -16
- npcsh-0.3.32.data/data/npcsh/npc_team/corca.npc +0 -13
- npcsh-0.3.32.data/data/npcsh/npc_team/eriane.npc +0 -4
- npcsh-0.3.32.data/data/npcsh/npc_team/foreman.npc +0 -7
- npcsh-0.3.32.data/data/npcsh/npc_team/generic_search.tool +0 -27
- npcsh-0.3.32.data/data/npcsh/npc_team/image_generation.tool +0 -25
- npcsh-0.3.32.data/data/npcsh/npc_team/lineru.npc +0 -0
- npcsh-0.3.32.data/data/npcsh/npc_team/local_search.tool +0 -149
- npcsh-0.3.32.data/data/npcsh/npc_team/maurawa.npc +0 -0
- npcsh-0.3.32.data/data/npcsh/npc_team/npcsh.ctx +0 -11
- npcsh-0.3.32.data/data/npcsh/npc_team/npcsh_executor.tool +0 -9
- npcsh-0.3.32.data/data/npcsh/npc_team/raone.npc +0 -0
- npcsh-0.3.32.data/data/npcsh/npc_team/screen_cap.tool +0 -27
- npcsh-0.3.32.data/data/npcsh/npc_team/sibiji.npc +0 -4
- npcsh-0.3.32.data/data/npcsh/npc_team/slean.npc +0 -4
- npcsh-0.3.32.data/data/npcsh/npc_team/sql_executor.tool +0 -26
- npcsh-0.3.32.data/data/npcsh/npc_team/test_pipeline.py +0 -181
- npcsh-0.3.32.data/data/npcsh/npc_team/turnic.npc +0 -4
- npcsh-0.3.32.data/data/npcsh/npc_team/welxor.npc +0 -0
- npcsh-0.3.32.dist-info/METADATA +0 -779
- npcsh-0.3.32.dist-info/RECORD +0 -78
- npcsh-0.3.32.dist-info/entry_points.txt +0 -3
- {npcsh-0.3.32.dist-info → npcsh-1.0.1.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
|
-
)
|