npcpy 1.2.32__tar.gz → 1.2.33__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.
Files changed (72) hide show
  1. {npcpy-1.2.32/npcpy.egg-info → npcpy-1.2.33}/PKG-INFO +97 -34
  2. {npcpy-1.2.32 → npcpy-1.2.33}/README.md +96 -33
  3. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/llm_funcs.py +57 -37
  4. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/npc_compiler.py +549 -350
  5. {npcpy-1.2.32 → npcpy-1.2.33/npcpy.egg-info}/PKG-INFO +97 -34
  6. {npcpy-1.2.32 → npcpy-1.2.33}/setup.py +1 -1
  7. {npcpy-1.2.32 → npcpy-1.2.33}/LICENSE +0 -0
  8. {npcpy-1.2.32 → npcpy-1.2.33}/MANIFEST.in +0 -0
  9. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/__init__.py +0 -0
  10. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/__init__.py +0 -0
  11. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/audio.py +0 -0
  12. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/data_models.py +0 -0
  13. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/image.py +0 -0
  14. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/load.py +0 -0
  15. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/text.py +0 -0
  16. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/video.py +0 -0
  17. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/data/web.py +0 -0
  18. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/__init__.py +0 -0
  19. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/diff.py +0 -0
  20. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/ge.py +0 -0
  21. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/memory_trainer.py +0 -0
  22. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/model_ensembler.py +0 -0
  23. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/rl.py +0 -0
  24. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/sft.py +0 -0
  25. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/ft/usft.py +0 -0
  26. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/__init__.py +0 -0
  27. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/audio_gen.py +0 -0
  28. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/embeddings.py +0 -0
  29. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/image_gen.py +0 -0
  30. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/response.py +0 -0
  31. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/gen/video_gen.py +0 -0
  32. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/main.py +0 -0
  33. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/__init__.py +0 -0
  34. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/command_history.py +0 -0
  35. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/kg_vis.py +0 -0
  36. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/knowledge_graph.py +0 -0
  37. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/memory_processor.py +0 -0
  38. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/memory/search.py +0 -0
  39. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/mix/__init__.py +0 -0
  40. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/mix/debate.py +0 -0
  41. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/npc_sysenv.py +0 -0
  42. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/npcs.py +0 -0
  43. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/serve.py +0 -0
  44. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/__init__.py +0 -0
  45. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/ai_function_tools.py +0 -0
  46. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/database_ai_adapters.py +0 -0
  47. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/database_ai_functions.py +0 -0
  48. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/model_runner.py +0 -0
  49. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/npcsql.py +0 -0
  50. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/sql/sql_model_compiler.py +0 -0
  51. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/tools.py +0 -0
  52. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/work/__init__.py +0 -0
  53. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/work/desktop.py +0 -0
  54. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/work/plan.py +0 -0
  55. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy/work/trigger.py +0 -0
  56. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy.egg-info/SOURCES.txt +0 -0
  57. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy.egg-info/dependency_links.txt +0 -0
  58. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy.egg-info/requires.txt +0 -0
  59. {npcpy-1.2.32 → npcpy-1.2.33}/npcpy.egg-info/top_level.txt +0 -0
  60. {npcpy-1.2.32 → npcpy-1.2.33}/setup.cfg +0 -0
  61. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_audio.py +0 -0
  62. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_command_history.py +0 -0
  63. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_image.py +0 -0
  64. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_llm_funcs.py +0 -0
  65. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_load.py +0 -0
  66. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_npc_compiler.py +0 -0
  67. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_npcsql.py +0 -0
  68. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_response.py +0 -0
  69. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_serve.py +0 -0
  70. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_text.py +0 -0
  71. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_tools.py +0 -0
  72. {npcpy-1.2.32 → npcpy-1.2.33}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.2.32
3
+ Version: 1.2.33
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
@@ -185,62 +185,117 @@ for tool_call in response['tool_results']:
185
185
  Here is an example for setting up an agent team to use Jinja Execution (Jinxs) templates that are processed entirely with prompts, allowing you to use them with models that do or do not possess tool calling support.
186
186
 
187
187
  ```python
188
+
188
189
  from npcpy.npc_compiler import NPC, Team, Jinx
189
190
  from npcpy.tools import auto_tools
190
191
  import os
192
+ from jinja2 import Environment, Undefined, DictLoader # Import necessary Jinja2 components for Jinx code
191
193
 
192
-
193
-
194
+ # --- REVISED file_reader_jinx ---
194
195
  file_reader_jinx = Jinx(jinx_data={
195
196
  "jinx_name": "file_reader",
196
- "description": "Read a file and summarize its contents",
197
+ "description": "Read a file and optionally summarize its contents using an LLM.",
197
198
  "inputs": ["filename"],
198
199
  "steps": [
199
200
  {
200
- "name": "read_file",
201
+ "name": "read_file_content",
201
202
  "engine": "python",
202
- "code": """
203
+ "code": '''
203
204
  import os
204
- with open(os.path.abspath('{{ filename }}'), 'r') as f:
205
- content = f.read()
206
- output= content
207
- """
205
+ from jinja2 import Environment, Undefined, DictLoader # Local import for Jinx step
206
+
207
+ # The 'filename' input to the file_reader jinx might be a Jinja template string like "{{ source_filename }}"
208
+ # or a direct filename. We need to render it using the current execution context.
209
+
210
+ # Get the Jinja environment from the NPC if available, otherwise create a default one.
211
+ # The 'npc' variable is available in the Jinx execution context.
212
+ # We need to ensure 'npc' exists before trying to access its 'jinja_env'.
213
+ execution_jinja_env = npc.jinja_env if npc else Environment(loader=DictLoader({}), undefined=Undefined)
214
+
215
+ # Render the filename. The current 'context' should contain the variables needed for rendering.
216
+ # For declarative calls, the parent Jinx's inputs (like 'source_filename') will be in this context.
217
+ # We also need to ensure the value from context['filename'] is treated as a template string.
218
+ filename_template = execution_jinja_env.from_string(context['filename'])
219
+ rendered_filename = filename_template.render(**context)
220
+
221
+ file_path_abs = os.path.abspath(rendered_filename)
222
+ try:
223
+ with open(file_path_abs, 'r') as f:
224
+ content = f.read()
225
+ context['file_raw_content'] = content # Store raw content in context for later use
226
+ output = content # Output of this step is the raw content
227
+ except FileNotFoundError:
228
+ output = f"Error: File not found at {file_path_abs}"
229
+ context['file_raw_content'] = output # Store error message for consistency
230
+ except Exception as e:
231
+ output = f"Error reading file {file_path_abs}: {e}"
232
+ context['file_raw_content'] = output # Store error message for consistency
233
+ '''
208
234
  },
209
235
  {
210
- "name": "summarize_content",
211
- "engine": "natural",
212
- "code": """
213
- Summarize the content of the file: {{ read_file }}.
214
- """
236
+ "name": "summarize_file_content",
237
+ "engine": "python",
238
+ "code": '''
239
+ # Check if the previous step encountered an error
240
+ if "Error" not in context['file_raw_content']:
241
+ prompt = f"Summarize the following content concisely, highlighting key themes and points: {context['file_raw_content']}"
242
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
243
+ output = llm_result.get('response', 'Failed to generate summary due to LLM error.')
244
+ else:
245
+ output = "Skipping summary due to previous file reading error."
246
+ '''
215
247
  }
216
248
  ]
217
249
  })
218
250
 
219
-
220
- # Define a jinx for literary research
251
+ # --- REVISED literary_research_jinx ---
221
252
  literary_research_jinx = Jinx(jinx_data={
222
253
  "jinx_name": "literary_research",
223
- "description": "Research a literary topic, analyze files, and summarize findings",
224
- "inputs": ["topic"],
254
+ "description": "Research a literary topic, read a specific file, analyze, and synthesize findings.",
255
+ "inputs": ["topic", "source_filename"],
225
256
  "steps": [
226
257
  {
227
- "name": "gather_info",
228
- "engine": "natural",
229
- "code": """
230
- Research the topic: {{ topic }}.
231
- Summarize the main themes and historical context.
232
- """
258
+ "name": "initial_llm_research",
259
+ "engine": "python",
260
+ "code": '''
261
+ prompt = f"Research the topic: {context['topic']}. Summarize the main themes, key authors, and historical context. Be thorough."
262
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
263
+ context['research_summary'] = llm_result.get('response', 'No initial LLM research found.')
264
+ output = context['research_summary']
265
+ '''
266
+ },
267
+ {
268
+ "name": "read_and_process_source_file",
269
+ "engine": "file_reader",
270
+ "filename": "{{ source_filename }}" # This is passed as a string template to file_reader
233
271
  },
234
272
  {
235
- "name": "final_summary",
236
- "engine": "natural",
237
- "code": """
238
- Based on the research in. {{gather_info}}, write a concise, creative summary.
239
- """
273
+ "name": "final_synthesis_and_creative_writing",
274
+ "engine": "python",
275
+ "code": '''
276
+ # Access outputs from previous steps.
277
+ research_summary = context['initial_llm_research']
278
+ # The output of a declarative jinx call (like 'file_reader') is stored under its step name.
279
+ # The actual content we want is the 'output' of the *last step* within that sub-jinx.
280
+ file_summary = context['read_and_process_source_file'].get('output', 'No file summary available.')
281
+
282
+ prompt = f"""Based on the following information:
283
+ 1. Comprehensive Research Summary:
284
+ {research_summary}
285
+
286
+ 2. Key Insights from Source File:
287
+ {file_summary}
288
+
289
+ Integrate these findings and write a concise, creative, and poetically styled summary of the literary topic '{context['topic']}'. Emphasize unique perspectives or connections between the research and the file content, as if written by a master of magical realism.
290
+ """
291
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
292
+ output = llm_result.get('response', 'Failed to generate final creative summary.')
293
+ '''
240
294
  }
241
295
  ]
242
296
  })
243
297
 
298
+ # --- NPC Definitions (unchanged) ---
244
299
  ggm = NPC(
245
300
  name='Gabriel Garcia Marquez',
246
301
  primary_directive='You are Gabriel Garcia Marquez, master of magical realism. Research, analyze, and write with poetic flair.',
@@ -263,16 +318,24 @@ borges = NPC(
263
318
  provider='ollama',
264
319
  )
265
320
 
266
- # Set up a team with a forenpc that orchestrates the other npcs
267
- lit_team = Team(npcs=[ggm, isabel], forenpc=borges, jinxs={'literary_research': literary_research_jinx, 'file_reader': file_reader_jinx},
321
+ # --- Team Setup ---
322
+ lit_team = Team(
323
+ npcs=[ggm, isabel],
324
+ forenpc=borges,
325
+ jinxs=[literary_research_jinx, file_reader_jinx],
268
326
  )
269
327
 
270
- # Example: Orchestrate a jinx workflow
328
+ # --- Orchestration Example ---
271
329
  result = lit_team.orchestrate(
272
- "Research the topic of magical realism, read ./test_data/magical_realism.txt and summarize the findings"
330
+ "Research the topic of magical realism, using the file './test_data/magical_realism.txt' as a primary source, and provide a comprehensive, creative summary."
273
331
  )
332
+
333
+ print("\n--- Orchestration Result Summary ---")
274
334
  print(result['debrief']['summary'])
275
335
 
336
+ print("\n--- Full Orchestration Output ---")
337
+ print(result['output'])
338
+
276
339
  ```
277
340
  ```
278
341
  • Action chosen: pass_to_npc
@@ -89,62 +89,117 @@ for tool_call in response['tool_results']:
89
89
  Here is an example for setting up an agent team to use Jinja Execution (Jinxs) templates that are processed entirely with prompts, allowing you to use them with models that do or do not possess tool calling support.
90
90
 
91
91
  ```python
92
+
92
93
  from npcpy.npc_compiler import NPC, Team, Jinx
93
94
  from npcpy.tools import auto_tools
94
95
  import os
96
+ from jinja2 import Environment, Undefined, DictLoader # Import necessary Jinja2 components for Jinx code
95
97
 
96
-
97
-
98
+ # --- REVISED file_reader_jinx ---
98
99
  file_reader_jinx = Jinx(jinx_data={
99
100
  "jinx_name": "file_reader",
100
- "description": "Read a file and summarize its contents",
101
+ "description": "Read a file and optionally summarize its contents using an LLM.",
101
102
  "inputs": ["filename"],
102
103
  "steps": [
103
104
  {
104
- "name": "read_file",
105
+ "name": "read_file_content",
105
106
  "engine": "python",
106
- "code": """
107
+ "code": '''
107
108
  import os
108
- with open(os.path.abspath('{{ filename }}'), 'r') as f:
109
- content = f.read()
110
- output= content
111
- """
109
+ from jinja2 import Environment, Undefined, DictLoader # Local import for Jinx step
110
+
111
+ # The 'filename' input to the file_reader jinx might be a Jinja template string like "{{ source_filename }}"
112
+ # or a direct filename. We need to render it using the current execution context.
113
+
114
+ # Get the Jinja environment from the NPC if available, otherwise create a default one.
115
+ # The 'npc' variable is available in the Jinx execution context.
116
+ # We need to ensure 'npc' exists before trying to access its 'jinja_env'.
117
+ execution_jinja_env = npc.jinja_env if npc else Environment(loader=DictLoader({}), undefined=Undefined)
118
+
119
+ # Render the filename. The current 'context' should contain the variables needed for rendering.
120
+ # For declarative calls, the parent Jinx's inputs (like 'source_filename') will be in this context.
121
+ # We also need to ensure the value from context['filename'] is treated as a template string.
122
+ filename_template = execution_jinja_env.from_string(context['filename'])
123
+ rendered_filename = filename_template.render(**context)
124
+
125
+ file_path_abs = os.path.abspath(rendered_filename)
126
+ try:
127
+ with open(file_path_abs, 'r') as f:
128
+ content = f.read()
129
+ context['file_raw_content'] = content # Store raw content in context for later use
130
+ output = content # Output of this step is the raw content
131
+ except FileNotFoundError:
132
+ output = f"Error: File not found at {file_path_abs}"
133
+ context['file_raw_content'] = output # Store error message for consistency
134
+ except Exception as e:
135
+ output = f"Error reading file {file_path_abs}: {e}"
136
+ context['file_raw_content'] = output # Store error message for consistency
137
+ '''
112
138
  },
113
139
  {
114
- "name": "summarize_content",
115
- "engine": "natural",
116
- "code": """
117
- Summarize the content of the file: {{ read_file }}.
118
- """
140
+ "name": "summarize_file_content",
141
+ "engine": "python",
142
+ "code": '''
143
+ # Check if the previous step encountered an error
144
+ if "Error" not in context['file_raw_content']:
145
+ prompt = f"Summarize the following content concisely, highlighting key themes and points: {context['file_raw_content']}"
146
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
147
+ output = llm_result.get('response', 'Failed to generate summary due to LLM error.')
148
+ else:
149
+ output = "Skipping summary due to previous file reading error."
150
+ '''
119
151
  }
120
152
  ]
121
153
  })
122
154
 
123
-
124
- # Define a jinx for literary research
155
+ # --- REVISED literary_research_jinx ---
125
156
  literary_research_jinx = Jinx(jinx_data={
126
157
  "jinx_name": "literary_research",
127
- "description": "Research a literary topic, analyze files, and summarize findings",
128
- "inputs": ["topic"],
158
+ "description": "Research a literary topic, read a specific file, analyze, and synthesize findings.",
159
+ "inputs": ["topic", "source_filename"],
129
160
  "steps": [
130
161
  {
131
- "name": "gather_info",
132
- "engine": "natural",
133
- "code": """
134
- Research the topic: {{ topic }}.
135
- Summarize the main themes and historical context.
136
- """
162
+ "name": "initial_llm_research",
163
+ "engine": "python",
164
+ "code": '''
165
+ prompt = f"Research the topic: {context['topic']}. Summarize the main themes, key authors, and historical context. Be thorough."
166
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
167
+ context['research_summary'] = llm_result.get('response', 'No initial LLM research found.')
168
+ output = context['research_summary']
169
+ '''
170
+ },
171
+ {
172
+ "name": "read_and_process_source_file",
173
+ "engine": "file_reader",
174
+ "filename": "{{ source_filename }}" # This is passed as a string template to file_reader
137
175
  },
138
176
  {
139
- "name": "final_summary",
140
- "engine": "natural",
141
- "code": """
142
- Based on the research in. {{gather_info}}, write a concise, creative summary.
143
- """
177
+ "name": "final_synthesis_and_creative_writing",
178
+ "engine": "python",
179
+ "code": '''
180
+ # Access outputs from previous steps.
181
+ research_summary = context['initial_llm_research']
182
+ # The output of a declarative jinx call (like 'file_reader') is stored under its step name.
183
+ # The actual content we want is the 'output' of the *last step* within that sub-jinx.
184
+ file_summary = context['read_and_process_source_file'].get('output', 'No file summary available.')
185
+
186
+ prompt = f"""Based on the following information:
187
+ 1. Comprehensive Research Summary:
188
+ {research_summary}
189
+
190
+ 2. Key Insights from Source File:
191
+ {file_summary}
192
+
193
+ Integrate these findings and write a concise, creative, and poetically styled summary of the literary topic '{context['topic']}'. Emphasize unique perspectives or connections between the research and the file content, as if written by a master of magical realism.
194
+ """
195
+ llm_result = npc.get_llm_response(prompt, tool_choice=False) # FIX: Passed prompt positionally
196
+ output = llm_result.get('response', 'Failed to generate final creative summary.')
197
+ '''
144
198
  }
145
199
  ]
146
200
  })
147
201
 
202
+ # --- NPC Definitions (unchanged) ---
148
203
  ggm = NPC(
149
204
  name='Gabriel Garcia Marquez',
150
205
  primary_directive='You are Gabriel Garcia Marquez, master of magical realism. Research, analyze, and write with poetic flair.',
@@ -167,16 +222,24 @@ borges = NPC(
167
222
  provider='ollama',
168
223
  )
169
224
 
170
- # Set up a team with a forenpc that orchestrates the other npcs
171
- lit_team = Team(npcs=[ggm, isabel], forenpc=borges, jinxs={'literary_research': literary_research_jinx, 'file_reader': file_reader_jinx},
225
+ # --- Team Setup ---
226
+ lit_team = Team(
227
+ npcs=[ggm, isabel],
228
+ forenpc=borges,
229
+ jinxs=[literary_research_jinx, file_reader_jinx],
172
230
  )
173
231
 
174
- # Example: Orchestrate a jinx workflow
232
+ # --- Orchestration Example ---
175
233
  result = lit_team.orchestrate(
176
- "Research the topic of magical realism, read ./test_data/magical_realism.txt and summarize the findings"
234
+ "Research the topic of magical realism, using the file './test_data/magical_realism.txt' as a primary source, and provide a comprehensive, creative summary."
177
235
  )
236
+
237
+ print("\n--- Orchestration Result Summary ---")
178
238
  print(result['debrief']['summary'])
179
239
 
240
+ print("\n--- Full Orchestration Output ---")
241
+ print(result['output'])
242
+
180
243
  ```
181
244
  ```
182
245
  • Action chosen: pass_to_npc
@@ -379,6 +379,8 @@ def execute_llm_command(
379
379
  "messages": messages,
380
380
  "output": "Max attempts reached. Unable to execute the command successfully.",
381
381
  }
382
+
383
+ # --- START OF CORRECTED handle_jinx_call ---
382
384
  def handle_jinx_call(
383
385
  command: str,
384
386
  jinx_name: str,
@@ -391,7 +393,7 @@ def handle_jinx_call(
391
393
  n_attempts=3,
392
394
  attempt=0,
393
395
  context=None,
394
- extra_globals=None, # ADD THIS
396
+ extra_globals=None,
395
397
  **kwargs
396
398
  ) -> Union[str, Dict[str, Any]]:
397
399
  """This function handles a jinx call.
@@ -411,10 +413,13 @@ def handle_jinx_call(
411
413
  if npc is None and team is None:
412
414
  return f"No jinxs are available. "
413
415
  else:
416
+ jinx = None
417
+ if npc and hasattr(npc, 'jinxs_dict') and jinx_name in npc.jinxs_dict:
418
+ jinx = npc.jinxs_dict[jinx_name]
419
+ elif team and hasattr(team, 'jinxs_dict') and jinx_name in team.jinxs_dict:
420
+ jinx = team.jinxs_dict[jinx_name]
414
421
 
415
-
416
-
417
- if jinx_name not in npc.jinxs_dict and jinx_name not in team.jinxs_dict:
422
+ if not jinx:
418
423
  print(f"Jinx {jinx_name} not available")
419
424
  if attempt < n_attempts:
420
425
  print(f"attempt {attempt+1} to generate jinx name failed, trying again")
@@ -442,16 +447,11 @@ def handle_jinx_call(
442
447
  "messages": messages,
443
448
  }
444
449
 
445
-
446
-
447
-
448
- elif jinx_name in npc.jinxs_dict:
449
- jinx = npc.jinxs_dict[jinx_name]
450
- elif jinx_name in team.jinxs_dict:
451
- jinx = team.jinxs_dict[jinx_name]
452
-
453
450
  render_markdown(f"jinx found: {jinx.jinx_name}")
454
- jinja_env = Environment(loader=FileSystemLoader("."), undefined=Undefined)
451
+
452
+ # This jinja_env is for parsing the Jinx's *inputs* from the LLM response, not for Jinx.execute's second pass.
453
+ local_jinja_env_for_input_parsing = Environment(loader=FileSystemLoader("."), undefined=Undefined)
454
+
455
455
  example_format = {}
456
456
  for inp in jinx.inputs:
457
457
  if isinstance(inp, str):
@@ -563,20 +563,32 @@ def handle_jinx_call(
563
563
 
564
564
  render_markdown( "\n".join(['\n - ' + str(key) + ': ' +str(val) for key, val in input_values.items()]))
565
565
 
566
+ # Initialize jinx_output before the try block to prevent UnboundLocalError
567
+ jinx_output = {"output": "Jinx execution did not complete."}
568
+
566
569
  try:
570
+ # --- CRITICAL FIX HERE ---
571
+ # Pass arguments as keyword arguments to avoid positional confusion
572
+ # Use npc.jinja_env for the second-pass rendering
567
573
  jinx_output = jinx.execute(
568
- input_values,
569
- jinja_env,
570
- npc=npc,
574
+ input_values=input_values,
575
+ npc=npc, # This is the orchestrating NPC
571
576
  messages=messages,
572
- extra_globals=extra_globals # ADD THIS
573
-
577
+ extra_globals=extra_globals,
578
+ jinja_env=npc.jinja_env if npc else (team.forenpc.jinja_env if team and team.forenpc else None) # Use NPC's or Team's forenpc's jinja_env
574
579
  )
580
+ # Ensure jinx_output is a dict with an 'output' key
581
+ if jinx_output is None:
582
+ jinx_output = {"output": "Jinx executed, but returned no explicit output."}
583
+ elif not isinstance(jinx_output, dict):
584
+ jinx_output = {"output": str(jinx_output)}
585
+
575
586
  except Exception as e:
576
587
  print(f"An error occurred while executing the jinx: {e}")
577
588
  print(f"trying again, attempt {attempt+1}")
578
589
  print('command', command)
579
590
  if attempt < n_attempts:
591
+ # Recursively call handle_jinx_call for retry
580
592
  jinx_output = handle_jinx_call(
581
593
  command,
582
594
  jinx_name,
@@ -589,9 +601,15 @@ def handle_jinx_call(
589
601
  attempt=attempt + 1,
590
602
  n_attempts=n_attempts,
591
603
  context=f""" \n \n \n "jinx failed: {e} \n \n \n here was the previous attempt: {input_values}""",
604
+ extra_globals=extra_globals
592
605
  )
606
+ else:
607
+ # If max attempts reached, set a clear error output
608
+ jinx_output = {"output": f"Jinx '{jinx_name}' failed after {n_attempts} attempts: {e}", "error": True}
609
+
610
+
593
611
  if not stream and len(messages) > 0 :
594
- render_markdown(f""" ## jinx OUTPUT FROM CALLING {jinx_name} \n \n output:{jinx_output['output']}""" )
612
+ render_markdown(f""" ## jinx OUTPUT FROM CALLING {jinx_name} \n \n output:{jinx_output.get('output', 'No output.')}""" )
595
613
  response = get_llm_response(f"""
596
614
  The user had the following request: {command}.
597
615
  Here were the jinx outputs from calling {jinx_name}: {jinx_output.get('output', '')}
@@ -610,7 +628,9 @@ def handle_jinx_call(
610
628
  response = response.get("response", {})
611
629
  return {'messages':messages, 'output':response}
612
630
 
613
- return {'messages': messages, 'output': jinx_output['output']}
631
+ return {'messages': messages, 'output': jinx_output.get('output', 'No output.')} # Ensure 'output' key exists
632
+
633
+ # --- END OF CORRECTED handle_jinx_call ---
614
634
 
615
635
 
616
636
  def handle_request_input(
@@ -670,7 +690,7 @@ def jinx_handler(command, extracted_data, **kwargs):
670
690
  team=kwargs.get('team'),
671
691
  stream=kwargs.get('stream'),
672
692
  context=kwargs.get('context'),
673
- extra_globals=kwargs.get('extra_globals') # ADD THIS
693
+ extra_globals=kwargs.get('extra_globals')
674
694
  )
675
695
 
676
696
  def answer_handler(command, extracted_data, **kwargs):
@@ -976,8 +996,8 @@ def execute_multi_step_plan(
976
996
  images: list = None,
977
997
  stream=False,
978
998
  context=None,
979
-
980
999
  actions: Dict[str, Dict] = None,
1000
+ extra_globals=None,
981
1001
  **kwargs,
982
1002
  ):
983
1003
  """
@@ -1045,7 +1065,7 @@ def execute_multi_step_plan(
1045
1065
  render_markdown(
1046
1066
  f"- Executing Action: {action_name} \n- Explanation: {action_data.get('explanation')}\n "
1047
1067
  )
1048
-
1068
+
1049
1069
  result = handler(
1050
1070
  command=command,
1051
1071
  extracted_data=action_data,
@@ -1059,7 +1079,7 @@ def execute_multi_step_plan(
1059
1079
  stream=stream,
1060
1080
  context=context+step_context,
1061
1081
  images=images,
1062
- extra_globals=kwargs.get('extra_globals') # ADD THIS
1082
+ extra_globals=extra_globals
1063
1083
  )
1064
1084
  except KeyError as e:
1065
1085
 
@@ -1862,7 +1882,7 @@ def zoom_in(facts,
1862
1882
  npc=npc,
1863
1883
  context=context,
1864
1884
  attempt_number=attempt_number+1,
1865
- n_tries=n_tries,
1885
+ n_tries=n_attempts, # Corrected from n_tries to n_attempts
1866
1886
  **kwargs)
1867
1887
  return facts
1868
1888
  def generate_groups(facts,
@@ -1945,7 +1965,6 @@ def remove_redundant_groups(groups,
1945
1965
  response = get_llm_response(prompt,
1946
1966
  model=model,
1947
1967
  provider=provider,
1948
- format="json",
1949
1968
  npc=npc,
1950
1969
  context=context,
1951
1970
  **kwargs)
@@ -2056,17 +2075,18 @@ def get_related_facts_llm(new_fact_statement,
2056
2075
  npc=npc,
2057
2076
  context=context,
2058
2077
  **kwargs)
2059
- if attempt_number > n_attempts:
2060
- print(f" Attempt {attempt_number} to find related facts yielded no results. Giving up.")
2061
- return get_related_facts_llm(new_fact_statement,
2062
- existing_fact_statements,
2063
- model=model,
2064
- provider=provider,
2065
- npc=npc,
2066
- attempt_number=attempt_number+1,
2067
- n_attempts=n_attempts,
2068
- context=context,
2069
- **kwargs)
2078
+ if attempt_number <= n_attempts: # Corrected logic: retry if attempt_number is within limits
2079
+ if not response["response"].get("related_facts", []): # Only retry if no related facts found
2080
+ print(f" Attempt {attempt_number} to find related facts yielded no results. Retrying...")
2081
+ return get_related_facts_llm(new_fact_statement,
2082
+ existing_fact_statements,
2083
+ model=model,
2084
+ provider=provider,
2085
+ npc=npc,
2086
+ attempt_number=attempt_number+1,
2087
+ n_attempts=n_attempts,
2088
+ context=context,
2089
+ **kwargs)
2070
2090
 
2071
2091
  return response["response"].get("related_facts", [])
2072
2092