npcpy 1.2.27__tar.gz → 1.2.29__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {npcpy-1.2.27/npcpy.egg-info → npcpy-1.2.29}/PKG-INFO +3 -3
- {npcpy-1.2.27 → npcpy-1.2.29}/README.md +2 -2
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/sft.py +25 -7
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/image_gen.py +37 -15
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/npc_compiler.py +60 -64
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/serve.py +96 -82
- {npcpy-1.2.27 → npcpy-1.2.29/npcpy.egg-info}/PKG-INFO +3 -3
- {npcpy-1.2.27 → npcpy-1.2.29}/setup.py +1 -1
- {npcpy-1.2.27 → npcpy-1.2.29}/LICENSE +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/MANIFEST.in +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/audio.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/data_models.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/image.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/load.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/text.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/video.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/data/web.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/diff.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/ge.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/memory_trainer.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/model_ensembler.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/rl.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/ft/usft.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/audio_gen.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/embeddings.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/response.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/gen/video_gen.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/llm_funcs.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/main.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/command_history.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/kg_vis.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/knowledge_graph.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/memory_processor.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/memory/search.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/mix/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/mix/debate.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/npc_sysenv.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/npcs.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/ai_function_tools.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/database_ai_adapters.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/database_ai_functions.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/model_runner.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/npcsql.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/sql/sql_model_compiler.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/tools.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/work/__init__.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/work/desktop.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/work/plan.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy/work/trigger.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy.egg-info/SOURCES.txt +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy.egg-info/dependency_links.txt +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy.egg-info/requires.txt +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/npcpy.egg-info/top_level.txt +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/setup.cfg +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_audio.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_command_history.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_image.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_llm_funcs.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_load.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_npc_compiler.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_npcsql.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_response.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_serve.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_text.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_tools.py +0 -0
- {npcpy-1.2.27 → npcpy-1.2.29}/tests/test_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: npcpy
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.29
|
|
4
4
|
Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
|
|
5
5
|
Home-page: https://github.com/NPC-Worldwide/npcpy
|
|
6
6
|
Author: Christopher Agostino
|
|
@@ -330,7 +330,7 @@ Users are not required to pass agents to get_llm_response, so you can work with
|
|
|
330
330
|
```python
|
|
331
331
|
from npcpy.npc_sysenv import print_and_process_stream
|
|
332
332
|
from npcpy.llm_funcs import get_llm_response
|
|
333
|
-
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='
|
|
333
|
+
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='qwen3:latest', provider='ollama', stream = True)
|
|
334
334
|
|
|
335
335
|
full_response = print_and_process_stream(response['response'], 'llama3.2', 'ollama')
|
|
336
336
|
```
|
|
@@ -338,7 +338,7 @@ Return structured outputs by specifying `format='json'` or passing a Pydantic sc
|
|
|
338
338
|
|
|
339
339
|
```python
|
|
340
340
|
from npcpy.llm_funcs import get_llm_response
|
|
341
|
-
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='
|
|
341
|
+
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='claude-4-5-haiku-latest', provider='deepseek', format='json')
|
|
342
342
|
|
|
343
343
|
print(response['response'])
|
|
344
344
|
```
|
|
@@ -234,7 +234,7 @@ Users are not required to pass agents to get_llm_response, so you can work with
|
|
|
234
234
|
```python
|
|
235
235
|
from npcpy.npc_sysenv import print_and_process_stream
|
|
236
236
|
from npcpy.llm_funcs import get_llm_response
|
|
237
|
-
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='
|
|
237
|
+
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='qwen3:latest', provider='ollama', stream = True)
|
|
238
238
|
|
|
239
239
|
full_response = print_and_process_stream(response['response'], 'llama3.2', 'ollama')
|
|
240
240
|
```
|
|
@@ -242,7 +242,7 @@ Return structured outputs by specifying `format='json'` or passing a Pydantic sc
|
|
|
242
242
|
|
|
243
243
|
```python
|
|
244
244
|
from npcpy.llm_funcs import get_llm_response
|
|
245
|
-
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='
|
|
245
|
+
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='claude-4-5-haiku-latest', provider='deepseek', format='json')
|
|
246
246
|
|
|
247
247
|
print(response['response'])
|
|
248
248
|
```
|
|
@@ -154,13 +154,17 @@ def run_sft(
|
|
|
154
154
|
save_steps=config.save_steps,
|
|
155
155
|
weight_decay=config.weight_decay,
|
|
156
156
|
)
|
|
157
|
-
|
|
157
|
+
|
|
158
|
+
def formatting_func(example):
|
|
159
|
+
return example["text"]
|
|
160
|
+
|
|
158
161
|
trainer = SFTTrainer(
|
|
159
162
|
model=model,
|
|
160
163
|
train_dataset=dataset,
|
|
161
164
|
peft_config=peft_config,
|
|
162
165
|
args=training_args,
|
|
163
|
-
|
|
166
|
+
processing_class=tokenizer,
|
|
167
|
+
formatting_func=formatting_func
|
|
164
168
|
)
|
|
165
169
|
|
|
166
170
|
print(f"Training on {len(dataset)} examples")
|
|
@@ -190,8 +194,6 @@ def load_sft_model(model_path: str):
|
|
|
190
194
|
tokenizer.pad_token = tokenizer.eos_token
|
|
191
195
|
|
|
192
196
|
return model, tokenizer
|
|
193
|
-
|
|
194
|
-
|
|
195
197
|
def predict_sft(
|
|
196
198
|
model,
|
|
197
199
|
tokenizer,
|
|
@@ -202,8 +204,13 @@ def predict_sft(
|
|
|
202
204
|
|
|
203
205
|
device = next(model.parameters()).device
|
|
204
206
|
|
|
207
|
+
formatted_prompt = (
|
|
208
|
+
f"<start_of_turn>user\n{prompt}<end_of_turn>\n"
|
|
209
|
+
f"<start_of_turn>model\n"
|
|
210
|
+
)
|
|
211
|
+
|
|
205
212
|
inputs = tokenizer(
|
|
206
|
-
|
|
213
|
+
formatted_prompt,
|
|
207
214
|
return_tensors="pt",
|
|
208
215
|
truncation=True,
|
|
209
216
|
max_length=512
|
|
@@ -222,9 +229,20 @@ def predict_sft(
|
|
|
222
229
|
pad_token_id=tokenizer.eos_token_id
|
|
223
230
|
)
|
|
224
231
|
|
|
225
|
-
|
|
232
|
+
full_response = tokenizer.decode(
|
|
226
233
|
outputs[0],
|
|
227
|
-
skip_special_tokens=
|
|
234
|
+
skip_special_tokens=False
|
|
228
235
|
)
|
|
229
236
|
|
|
237
|
+
if "<start_of_turn>model\n" in full_response:
|
|
238
|
+
response = full_response.split(
|
|
239
|
+
"<start_of_turn>model\n"
|
|
240
|
+
)[-1]
|
|
241
|
+
response = response.split("<end_of_turn>")[0].strip()
|
|
242
|
+
else:
|
|
243
|
+
response = tokenizer.decode(
|
|
244
|
+
outputs[0][len(input_ids[0]):],
|
|
245
|
+
skip_special_tokens=True
|
|
246
|
+
)
|
|
247
|
+
|
|
230
248
|
return response
|
|
@@ -86,6 +86,16 @@ def generate_image_diffusers(
|
|
|
86
86
|
else:
|
|
87
87
|
raise e
|
|
88
88
|
|
|
89
|
+
import os
|
|
90
|
+
import base64
|
|
91
|
+
import io
|
|
92
|
+
from typing import Union, List, Optional
|
|
93
|
+
|
|
94
|
+
import PIL
|
|
95
|
+
from PIL import Image
|
|
96
|
+
|
|
97
|
+
import requests
|
|
98
|
+
from urllib.request import urlopen
|
|
89
99
|
|
|
90
100
|
def openai_image_gen(
|
|
91
101
|
prompt: str,
|
|
@@ -97,36 +107,47 @@ def openai_image_gen(
|
|
|
97
107
|
):
|
|
98
108
|
"""Generate or edit an image using the OpenAI API."""
|
|
99
109
|
from openai import OpenAI
|
|
100
|
-
|
|
110
|
+
|
|
101
111
|
client = OpenAI()
|
|
102
|
-
|
|
112
|
+
|
|
103
113
|
if height is None:
|
|
104
114
|
height = 1024
|
|
105
115
|
if width is None:
|
|
106
|
-
width = 1024
|
|
107
|
-
|
|
108
|
-
size_str = f"{width}x{height}"
|
|
116
|
+
width = 1024
|
|
117
|
+
|
|
118
|
+
size_str = f"{width}x{height}"
|
|
109
119
|
|
|
110
120
|
if attachments is not None:
|
|
111
121
|
processed_images = []
|
|
122
|
+
files_to_close = []
|
|
112
123
|
for attachment in attachments:
|
|
113
124
|
if isinstance(attachment, str):
|
|
114
|
-
|
|
125
|
+
file_handle = open(attachment, "rb")
|
|
126
|
+
processed_images.append(file_handle)
|
|
127
|
+
files_to_close.append(file_handle)
|
|
115
128
|
elif isinstance(attachment, bytes):
|
|
116
|
-
|
|
129
|
+
img_byte_arr = io.BytesIO(attachment)
|
|
130
|
+
img_byte_arr.name = 'image.png' # FIX: Add filename hint
|
|
131
|
+
processed_images.append(img_byte_arr)
|
|
117
132
|
elif isinstance(attachment, Image.Image):
|
|
118
133
|
img_byte_arr = io.BytesIO()
|
|
119
134
|
attachment.save(img_byte_arr, format='PNG')
|
|
120
135
|
img_byte_arr.seek(0)
|
|
136
|
+
img_byte_arr.name = 'image.png' # FIX: Add filename hint
|
|
121
137
|
processed_images.append(img_byte_arr)
|
|
122
138
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
139
|
+
try:
|
|
140
|
+
result = client.images.edit(
|
|
141
|
+
model=model,
|
|
142
|
+
image=processed_images[0],
|
|
143
|
+
prompt=prompt,
|
|
144
|
+
n=n_images,
|
|
145
|
+
size=size_str,
|
|
146
|
+
)
|
|
147
|
+
finally:
|
|
148
|
+
# This ensures any files we opened are properly closed
|
|
149
|
+
for f in files_to_close:
|
|
150
|
+
f.close()
|
|
130
151
|
else:
|
|
131
152
|
result = client.images.generate(
|
|
132
153
|
model=model,
|
|
@@ -134,7 +155,7 @@ def openai_image_gen(
|
|
|
134
155
|
n=n_images,
|
|
135
156
|
size=size_str,
|
|
136
157
|
)
|
|
137
|
-
|
|
158
|
+
|
|
138
159
|
collected_images = []
|
|
139
160
|
for item_data in result.data:
|
|
140
161
|
if model == 'gpt-image-1':
|
|
@@ -153,6 +174,7 @@ def openai_image_gen(
|
|
|
153
174
|
return collected_images
|
|
154
175
|
|
|
155
176
|
|
|
177
|
+
|
|
156
178
|
def gemini_image_gen(
|
|
157
179
|
prompt: str,
|
|
158
180
|
model: str = "gemini-2.5-flash",
|
|
@@ -264,7 +264,6 @@ class Jinx:
|
|
|
264
264
|
self.inputs = jinx_data.get("inputs", [])
|
|
265
265
|
self.description = jinx_data.get("description", "")
|
|
266
266
|
self.steps = self._parse_steps(jinx_data.get("steps", []))
|
|
267
|
-
|
|
268
267
|
def _parse_steps(self, steps):
|
|
269
268
|
"""Parse steps from jinx definition"""
|
|
270
269
|
parsed_steps = []
|
|
@@ -275,11 +274,12 @@ class Jinx:
|
|
|
275
274
|
"engine": step.get("engine", "natural"),
|
|
276
275
|
"code": step.get("code", "")
|
|
277
276
|
}
|
|
277
|
+
if "mode" in step:
|
|
278
|
+
parsed_step["mode"] = step["mode"]
|
|
278
279
|
parsed_steps.append(parsed_step)
|
|
279
280
|
else:
|
|
280
281
|
raise ValueError(f"Invalid step format: {step}")
|
|
281
282
|
return parsed_steps
|
|
282
|
-
|
|
283
283
|
def execute(self,
|
|
284
284
|
input_values,
|
|
285
285
|
jinxs_dict,
|
|
@@ -317,24 +317,19 @@ class Jinx:
|
|
|
317
317
|
)
|
|
318
318
|
|
|
319
319
|
return context
|
|
320
|
-
|
|
321
320
|
def _execute_step(self,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
):
|
|
328
|
-
"""Execute a single step of the jinx"""
|
|
321
|
+
step,
|
|
322
|
+
context,
|
|
323
|
+
jinja_env,
|
|
324
|
+
npc=None,
|
|
325
|
+
messages=None,
|
|
326
|
+
):
|
|
329
327
|
engine = step.get("engine", "natural")
|
|
330
328
|
code = step.get("code", "")
|
|
331
329
|
step_name = step.get("name", "unnamed_step")
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
330
|
+
mode = step.get("mode", "chat")
|
|
335
331
|
|
|
336
332
|
try:
|
|
337
|
-
|
|
338
333
|
template = jinja_env.from_string(code)
|
|
339
334
|
rendered_code = template.render(**context)
|
|
340
335
|
|
|
@@ -346,16 +341,23 @@ class Jinx:
|
|
|
346
341
|
rendered_code = code
|
|
347
342
|
rendered_engine = engine
|
|
348
343
|
|
|
349
|
-
|
|
350
344
|
if rendered_engine == "natural":
|
|
351
345
|
if rendered_code.strip():
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
346
|
+
if mode == "agent":
|
|
347
|
+
response = npc.get_llm_response(
|
|
348
|
+
rendered_code,
|
|
349
|
+
context=context,
|
|
350
|
+
messages=messages,
|
|
351
|
+
auto_process_tool_calls=True,
|
|
352
|
+
use_core_tools=True
|
|
353
|
+
)
|
|
354
|
+
else:
|
|
355
|
+
response = npc.get_llm_response(
|
|
356
|
+
rendered_code,
|
|
357
|
+
context=context,
|
|
358
|
+
messages=messages,
|
|
359
|
+
)
|
|
360
|
+
|
|
359
361
|
response_text = response.get("response", "")
|
|
360
362
|
context['output'] = response_text
|
|
361
363
|
context["llm_response"] = response_text
|
|
@@ -363,7 +365,6 @@ class Jinx:
|
|
|
363
365
|
context[step_name] = response_text
|
|
364
366
|
context['messages'] = response.get('messages')
|
|
365
367
|
elif rendered_engine == "python":
|
|
366
|
-
|
|
367
368
|
exec_globals = {
|
|
368
369
|
"__builtins__": __builtins__,
|
|
369
370
|
"npc": npc,
|
|
@@ -379,48 +380,44 @@ class Jinx:
|
|
|
379
380
|
"pathlib": pathlib,
|
|
380
381
|
"subprocess": subprocess,
|
|
381
382
|
"get_llm_response": npy.llm_funcs.get_llm_response,
|
|
382
|
-
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
|
|
386
|
-
|
|
387
385
|
exec_locals = {}
|
|
388
386
|
exec(rendered_code, exec_globals, exec_locals)
|
|
389
387
|
|
|
390
|
-
|
|
391
388
|
context.update(exec_locals)
|
|
392
389
|
|
|
393
|
-
|
|
394
390
|
if "output" in exec_locals:
|
|
395
391
|
outp = exec_locals["output"]
|
|
396
392
|
context["output"] = outp
|
|
397
393
|
context[step_name] = outp
|
|
398
394
|
messages.append({'role':'assistant',
|
|
399
|
-
|
|
395
|
+
'content': f'Jinx executed with following output: {outp}'})
|
|
400
396
|
context['messages'] = messages
|
|
401
397
|
|
|
402
398
|
else:
|
|
403
|
-
|
|
404
399
|
context[step_name] = {"error": f"Unsupported engine: {rendered_engine}"}
|
|
405
400
|
|
|
406
401
|
return context
|
|
407
|
-
|
|
408
402
|
def to_dict(self):
|
|
409
403
|
"""Convert to dictionary representation"""
|
|
404
|
+
steps_list = []
|
|
405
|
+
for i, step in enumerate(self.steps):
|
|
406
|
+
step_dict = {
|
|
407
|
+
"name": step.get("name", f"step_{i}"),
|
|
408
|
+
"engine": step.get("engine"),
|
|
409
|
+
"code": step.get("code")
|
|
410
|
+
}
|
|
411
|
+
if "mode" in step:
|
|
412
|
+
step_dict["mode"] = step["mode"]
|
|
413
|
+
steps_list.append(step_dict)
|
|
414
|
+
|
|
410
415
|
return {
|
|
411
416
|
"jinx_name": self.jinx_name,
|
|
412
417
|
"description": self.description,
|
|
413
418
|
"inputs": self.inputs,
|
|
414
|
-
"steps":
|
|
415
|
-
{
|
|
416
|
-
"name": step.get("name", f"step_{i}"),
|
|
417
|
-
"engine": step.get("engine"),
|
|
418
|
-
"code": step.get("code")
|
|
419
|
-
}
|
|
420
|
-
for i, step in enumerate(self.steps)
|
|
421
|
-
]
|
|
419
|
+
"steps": steps_list
|
|
422
420
|
}
|
|
423
|
-
|
|
424
421
|
def save(self, directory):
|
|
425
422
|
"""Save jinx to file"""
|
|
426
423
|
jinx_path = os.path.join(directory, f"{self.jinx_name}.jinx")
|
|
@@ -477,23 +474,23 @@ output = {mcp_tool.__module__}.{name}(
|
|
|
477
474
|
except:
|
|
478
475
|
pass
|
|
479
476
|
|
|
480
|
-
|
|
481
477
|
def load_jinxs_from_directory(directory):
|
|
482
|
-
"""Load all jinxs from a directory"""
|
|
478
|
+
"""Load all jinxs from a directory recursively"""
|
|
483
479
|
jinxs = []
|
|
484
480
|
directory = os.path.expanduser(directory)
|
|
485
481
|
|
|
486
482
|
if not os.path.exists(directory):
|
|
487
483
|
return jinxs
|
|
488
|
-
|
|
489
|
-
for
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
484
|
+
|
|
485
|
+
for root, dirs, files in os.walk(directory):
|
|
486
|
+
for filename in files:
|
|
487
|
+
if filename.endswith(".jinx"):
|
|
488
|
+
try:
|
|
489
|
+
jinx_path = os.path.join(root, filename)
|
|
490
|
+
jinx = Jinx(jinx_path=jinx_path)
|
|
491
|
+
jinxs.append(jinx)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
print(f"Error loading jinx {filename}: {e}")
|
|
497
494
|
|
|
498
495
|
return jinxs
|
|
499
496
|
|
|
@@ -693,7 +690,8 @@ class NPC:
|
|
|
693
690
|
self.jinxs_directory = os.path.expanduser('~/.npcsh/npc_team/jinxs/')
|
|
694
691
|
else:
|
|
695
692
|
self.jinxs_directory = None
|
|
696
|
-
self.npc_directory = None
|
|
693
|
+
self.npc_directory = None
|
|
694
|
+
|
|
697
695
|
self.team = team
|
|
698
696
|
if tools is not None:
|
|
699
697
|
tools_schema, tool_map = auto_tools(tools)
|
|
@@ -1057,18 +1055,16 @@ class NPC:
|
|
|
1057
1055
|
"""Load and process NPC-specific jinxs"""
|
|
1058
1056
|
npc_jinxs = []
|
|
1059
1057
|
|
|
1060
|
-
if self.jinxs_directory is None:
|
|
1061
|
-
self.jinxs_dict = {}
|
|
1062
|
-
return None
|
|
1063
|
-
|
|
1064
1058
|
if jinxs == "*":
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1059
|
+
if self.team and hasattr(self.team, 'jinxs_dict'):
|
|
1060
|
+
for jinx in self.team.jinxs_dict.values():
|
|
1061
|
+
npc_jinxs.append(jinx)
|
|
1062
|
+
elif self.use_global_jinxs or (hasattr(self, 'jinxs_directory') and self.jinxs_directory):
|
|
1063
|
+
jinxs_dir = self.jinxs_directory or os.path.expanduser('~/.npcsh/npc_team/jinxs/')
|
|
1064
|
+
if os.path.exists(jinxs_dir):
|
|
1065
|
+
npc_jinxs.extend(load_jinxs_from_directory(jinxs_dir))
|
|
1069
1066
|
|
|
1070
1067
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
1071
|
-
|
|
1072
1068
|
return npc_jinxs
|
|
1073
1069
|
|
|
1074
1070
|
for jinx in jinxs:
|
|
@@ -1076,13 +1072,13 @@ class NPC:
|
|
|
1076
1072
|
npc_jinxs.append(jinx)
|
|
1077
1073
|
elif isinstance(jinx, dict):
|
|
1078
1074
|
npc_jinxs.append(Jinx(jinx_data=jinx))
|
|
1079
|
-
|
|
1075
|
+
elif isinstance(jinx, str):
|
|
1080
1076
|
jinx_path = None
|
|
1081
1077
|
jinx_name = jinx
|
|
1082
1078
|
if not jinx_name.endswith(".jinx"):
|
|
1083
1079
|
jinx_name += ".jinx"
|
|
1084
1080
|
|
|
1085
|
-
if hasattr(self, 'jinxs_directory') and os.path.exists(self.jinxs_directory):
|
|
1081
|
+
if hasattr(self, 'jinxs_directory') and self.jinxs_directory and os.path.exists(self.jinxs_directory):
|
|
1086
1082
|
candidate_path = os.path.join(self.jinxs_directory, jinx_name)
|
|
1087
1083
|
if os.path.exists(candidate_path):
|
|
1088
1084
|
jinx_path = candidate_path
|
|
@@ -1092,8 +1088,8 @@ class NPC:
|
|
|
1092
1088
|
npc_jinxs.append(jinx_obj)
|
|
1093
1089
|
|
|
1094
1090
|
self.jinxs_dict = {jinx.jinx_name: jinx for jinx in npc_jinxs}
|
|
1091
|
+
print(npc_jinxs)
|
|
1095
1092
|
return npc_jinxs
|
|
1096
|
-
|
|
1097
1093
|
def get_llm_response(self,
|
|
1098
1094
|
request,
|
|
1099
1095
|
jinxs=None,
|
|
@@ -502,56 +502,73 @@ def get_global_settings():
|
|
|
502
502
|
except Exception as e:
|
|
503
503
|
print(f"Error in get_global_settings: {str(e)}")
|
|
504
504
|
return jsonify({"error": str(e)}), 500
|
|
505
|
-
|
|
505
|
+
def _get_jinx_files_recursively(directory):
|
|
506
|
+
"""Helper to recursively find all .jinx file paths."""
|
|
507
|
+
jinx_paths = []
|
|
508
|
+
if os.path.exists(directory):
|
|
509
|
+
for root, _, files in os.walk(directory):
|
|
510
|
+
for filename in files:
|
|
511
|
+
if filename.endswith(".jinx"):
|
|
512
|
+
jinx_paths.append(os.path.join(root, filename))
|
|
513
|
+
return jinx_paths
|
|
506
514
|
|
|
507
515
|
@app.route("/api/jinxs/available", methods=["GET"])
|
|
508
516
|
def get_available_jinxs():
|
|
509
|
-
"""
|
|
510
|
-
Get all available jinxs for a given NPC and/or team.
|
|
511
|
-
Returns a list of jinx names that can be executed.
|
|
512
|
-
"""
|
|
513
517
|
try:
|
|
514
518
|
current_path = request.args.get('currentPath')
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
jinx_names = set() # Use set to avoid duplicates
|
|
518
|
-
|
|
519
|
-
# Get team jinxs from project directory
|
|
519
|
+
jinx_names = set()
|
|
520
|
+
|
|
520
521
|
if current_path:
|
|
521
522
|
team_jinxs_dir = os.path.join(current_path, 'npc_team', 'jinxs')
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
# Get global jinxs
|
|
523
|
+
jinx_paths = _get_jinx_files_recursively(team_jinxs_dir)
|
|
524
|
+
for path in jinx_paths:
|
|
525
|
+
jinx_names.add(os.path.basename(path)[:-5])
|
|
526
|
+
|
|
528
527
|
global_jinxs_dir = os.path.expanduser('~/.npcsh/npc_team/jinxs')
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
# Get NPC-specific jinxs if NPC is specified
|
|
535
|
-
if npc_name:
|
|
536
|
-
# Try to load the NPC and get its jinxs
|
|
537
|
-
db_conn = get_db_connection()
|
|
538
|
-
npc_object = load_npc_by_name_and_source(npc_name, 'project', db_conn, current_path)
|
|
539
|
-
if not npc_object:
|
|
540
|
-
npc_object = load_npc_by_name_and_source(npc_name, 'global', db_conn)
|
|
541
|
-
|
|
542
|
-
if npc_object and hasattr(npc_object, 'jinxs_dict') and npc_object.jinxs_dict:
|
|
543
|
-
jinx_names.update(npc_object.jinxs_dict.keys())
|
|
544
|
-
|
|
545
|
-
return jsonify({
|
|
546
|
-
'jinxs': sorted(list(jinx_names)),
|
|
547
|
-
'error': None
|
|
548
|
-
})
|
|
549
|
-
|
|
528
|
+
jinx_paths = _get_jinx_files_recursively(global_jinxs_dir)
|
|
529
|
+
for path in jinx_paths:
|
|
530
|
+
jinx_names.add(os.path.basename(path)[:-5])
|
|
531
|
+
|
|
532
|
+
return jsonify({'jinxs': sorted(list(jinx_names)), 'error': None})
|
|
550
533
|
except Exception as e:
|
|
551
534
|
print(f"Error getting available jinxs: {str(e)}")
|
|
552
535
|
traceback.print_exc()
|
|
553
536
|
return jsonify({'jinxs': [], 'error': str(e)}), 500
|
|
554
537
|
|
|
538
|
+
@app.route("/api/jinxs/global", methods=["GET"])
|
|
539
|
+
def get_global_jinxs():
|
|
540
|
+
jinxs_dir = os.path.join(os.path.expanduser("~"), ".npcsh", "npc_team", "jinxs")
|
|
541
|
+
jinx_paths = _get_jinx_files_recursively(jinxs_dir)
|
|
542
|
+
jinxs = []
|
|
543
|
+
for path in jinx_paths:
|
|
544
|
+
try:
|
|
545
|
+
with open(path, "r") as f:
|
|
546
|
+
jinx_data = yaml.safe_load(f)
|
|
547
|
+
jinxs.append(jinx_data)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
print(f"Error loading global jinx {path}: {e}")
|
|
550
|
+
return jsonify({"jinxs": jinxs})
|
|
551
|
+
|
|
552
|
+
@app.route("/api/jinxs/project", methods=["GET"])
|
|
553
|
+
def get_project_jinxs():
|
|
554
|
+
current_path = request.args.get("currentPath")
|
|
555
|
+
if not current_path:
|
|
556
|
+
return jsonify({"jinxs": []})
|
|
557
|
+
|
|
558
|
+
if not current_path.endswith("npc_team"):
|
|
559
|
+
current_path = os.path.join(current_path, "npc_team")
|
|
560
|
+
|
|
561
|
+
jinxs_dir = os.path.join(current_path, "jinxs")
|
|
562
|
+
jinx_paths = _get_jinx_files_recursively(jinxs_dir)
|
|
563
|
+
jinxs = []
|
|
564
|
+
for path in jinx_paths:
|
|
565
|
+
try:
|
|
566
|
+
with open(path, "r") as f:
|
|
567
|
+
jinx_data = yaml.safe_load(f)
|
|
568
|
+
jinxs.append(jinx_data)
|
|
569
|
+
except Exception as e:
|
|
570
|
+
print(f"Error loading project jinx {path}: {e}")
|
|
571
|
+
return jsonify({"jinxs": jinxs})
|
|
555
572
|
|
|
556
573
|
@app.route("/api/jinx/execute", methods=["POST"])
|
|
557
574
|
def execute_jinx():
|
|
@@ -568,8 +585,11 @@ def execute_jinx():
|
|
|
568
585
|
with cancellation_lock:
|
|
569
586
|
cancellation_flags[stream_id] = False
|
|
570
587
|
|
|
588
|
+
print(data)
|
|
589
|
+
|
|
571
590
|
jinx_name = data.get("jinxName")
|
|
572
591
|
jinx_args = data.get("jinxArgs", [])
|
|
592
|
+
print(jinx_args)
|
|
573
593
|
conversation_id = data.get("conversationId")
|
|
574
594
|
model = data.get("model")
|
|
575
595
|
provider = data.get("provider")
|
|
@@ -616,8 +636,45 @@ def execute_jinx():
|
|
|
616
636
|
|
|
617
637
|
# Extract inputs from args
|
|
618
638
|
from npcpy.npc_compiler import extract_jinx_inputs
|
|
619
|
-
|
|
620
|
-
|
|
639
|
+
|
|
640
|
+
# --- Start of Fix ---
|
|
641
|
+
# Re-assemble arguments that were incorrectly split by spaces.
|
|
642
|
+
fixed_args = []
|
|
643
|
+
i = 0
|
|
644
|
+
while i < len(jinx_args):
|
|
645
|
+
arg = jinx_args[i]
|
|
646
|
+
if arg.startswith('-'):
|
|
647
|
+
fixed_args.append(arg)
|
|
648
|
+
value_parts = []
|
|
649
|
+
i += 1
|
|
650
|
+
# Collect all subsequent parts until the next flag or the end of the list.
|
|
651
|
+
while i < len(jinx_args) and not jinx_args[i].startswith('-'):
|
|
652
|
+
value_parts.append(jinx_args[i])
|
|
653
|
+
i += 1
|
|
654
|
+
|
|
655
|
+
if value_parts:
|
|
656
|
+
# Join the parts back into a single string.
|
|
657
|
+
full_value = " ".join(value_parts)
|
|
658
|
+
# Clean up the extraneous quotes that the initial bad split left behind.
|
|
659
|
+
if full_value.startswith("'") and full_value.endswith("'"):
|
|
660
|
+
full_value = full_value[1:-1]
|
|
661
|
+
elif full_value.startswith('"') and full_value.endswith('"'):
|
|
662
|
+
full_value = full_value[1:-1]
|
|
663
|
+
fixed_args.append(full_value)
|
|
664
|
+
# The 'i' counter is already advanced, so the loop continues from the next flag.
|
|
665
|
+
else:
|
|
666
|
+
# This handles positional arguments, just in case.
|
|
667
|
+
fixed_args.append(arg)
|
|
668
|
+
i += 1
|
|
669
|
+
# --- End of Fix ---
|
|
670
|
+
|
|
671
|
+
# Now, use the corrected arguments to extract inputs.
|
|
672
|
+
input_values = extract_jinx_inputs(fixed_args, jinx)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
print('executing jinx with input_values ,', input_values)
|
|
621
678
|
# Get conversation history
|
|
622
679
|
command_history = CommandHistory(app.config.get('DB_PATH'))
|
|
623
680
|
messages = fetch_messages_for_conversation(conversation_id)
|
|
@@ -966,49 +1023,6 @@ def get_npc_team_global():
|
|
|
966
1023
|
return jsonify({"npcs": [], "error": str(e)})
|
|
967
1024
|
|
|
968
1025
|
|
|
969
|
-
@app.route("/api/jinxs/global", methods=["GET"])
|
|
970
|
-
def get_global_jinxs():
|
|
971
|
-
|
|
972
|
-
user_home = os.path.expanduser("~")
|
|
973
|
-
jinxs_dir = os.path.join(user_home, ".npcsh", "npc_team", "jinxs")
|
|
974
|
-
jinxs = []
|
|
975
|
-
if os.path.exists(jinxs_dir):
|
|
976
|
-
for file in os.listdir(jinxs_dir):
|
|
977
|
-
if file.endswith(".jinx"):
|
|
978
|
-
with open(os.path.join(jinxs_dir, file), "r") as f:
|
|
979
|
-
jinx_data = yaml.safe_load(f)
|
|
980
|
-
jinxs.append(jinx_data)
|
|
981
|
-
print("file", file)
|
|
982
|
-
|
|
983
|
-
return jsonify({"jinxs": jinxs})
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
@app.route("/api/jinxs/project", methods=["GET"])
|
|
991
|
-
def get_project_jinxs():
|
|
992
|
-
current_path = request.args.get(
|
|
993
|
-
"currentPath"
|
|
994
|
-
)
|
|
995
|
-
if not current_path:
|
|
996
|
-
return jsonify({"jinxs": []})
|
|
997
|
-
|
|
998
|
-
if not current_path.endswith("npc_team"):
|
|
999
|
-
current_path = os.path.join(current_path, "npc_team")
|
|
1000
|
-
|
|
1001
|
-
jinxs_dir = os.path.join(current_path, "jinxs")
|
|
1002
|
-
jinxs = []
|
|
1003
|
-
if os.path.exists(jinxs_dir):
|
|
1004
|
-
for file in os.listdir(jinxs_dir):
|
|
1005
|
-
if file.endswith(".jinx"):
|
|
1006
|
-
with open(os.path.join(jinxs_dir, file), "r") as f:
|
|
1007
|
-
jinx_data = yaml.safe_load(f)
|
|
1008
|
-
jinxs.append(jinx_data)
|
|
1009
|
-
return jsonify({"jinxs": jinxs})
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
1026
|
@app.route("/api/jinxs/save", methods=["POST"])
|
|
1013
1027
|
def save_jinx():
|
|
1014
1028
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: npcpy
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.29
|
|
4
4
|
Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
|
|
5
5
|
Home-page: https://github.com/NPC-Worldwide/npcpy
|
|
6
6
|
Author: Christopher Agostino
|
|
@@ -330,7 +330,7 @@ Users are not required to pass agents to get_llm_response, so you can work with
|
|
|
330
330
|
```python
|
|
331
331
|
from npcpy.npc_sysenv import print_and_process_stream
|
|
332
332
|
from npcpy.llm_funcs import get_llm_response
|
|
333
|
-
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='
|
|
333
|
+
response = get_llm_response("When did the united states government begin sending advisors to vietnam?", model='qwen3:latest', provider='ollama', stream = True)
|
|
334
334
|
|
|
335
335
|
full_response = print_and_process_stream(response['response'], 'llama3.2', 'ollama')
|
|
336
336
|
```
|
|
@@ -338,7 +338,7 @@ Return structured outputs by specifying `format='json'` or passing a Pydantic sc
|
|
|
338
338
|
|
|
339
339
|
```python
|
|
340
340
|
from npcpy.llm_funcs import get_llm_response
|
|
341
|
-
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='
|
|
341
|
+
response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='claude-4-5-haiku-latest', provider='deepseek', format='json')
|
|
342
342
|
|
|
343
343
|
print(response['response'])
|
|
344
344
|
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|