npcpy 1.3.4__py3-none-any.whl → 1.3.6__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.
- npcpy/build_funcs.py +288 -0
- npcpy/data/load.py +1 -1
- npcpy/data/web.py +5 -4
- npcpy/gen/image_gen.py +2 -1
- npcpy/gen/response.py +119 -66
- npcpy/gen/world_gen.py +609 -0
- npcpy/llm_funcs.py +177 -271
- npcpy/memory/command_history.py +107 -2
- npcpy/memory/knowledge_graph.py +1 -1
- npcpy/npc_compiler.py +176 -32
- npcpy/npc_sysenv.py +5 -5
- npcpy/serve.py +311 -2
- npcpy/sql/npcsql.py +272 -59
- npcpy/work/browser.py +30 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.6.dist-info}/METADATA +1 -1
- {npcpy-1.3.4.dist-info → npcpy-1.3.6.dist-info}/RECORD +19 -16
- {npcpy-1.3.4.dist-info → npcpy-1.3.6.dist-info}/WHEEL +0 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.3.4.dist-info → npcpy-1.3.6.dist-info}/top_level.txt +0 -0
npcpy/llm_funcs.py
CHANGED
|
@@ -6,7 +6,10 @@ import random
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import copy
|
|
8
8
|
import itertools
|
|
9
|
+
import logging
|
|
9
10
|
from typing import List, Dict, Any, Optional, Union
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("npcpy.llm_funcs")
|
|
10
13
|
from npcpy.npc_sysenv import (
|
|
11
14
|
print_and_process_stream_with_markdown,
|
|
12
15
|
render_markdown,
|
|
@@ -243,7 +246,7 @@ def get_llm_response(
|
|
|
243
246
|
ctx_suffix = _context_suffix(run_context)
|
|
244
247
|
run_messages = _build_messages(messages, system_message, prompt, ctx_suffix)
|
|
245
248
|
return get_litellm_response(
|
|
246
|
-
prompt + ctx_suffix,
|
|
249
|
+
(prompt + ctx_suffix) if prompt else None,
|
|
247
250
|
messages=run_messages,
|
|
248
251
|
model=run_model,
|
|
249
252
|
provider=run_provider,
|
|
@@ -580,16 +583,17 @@ def check_llm_command(
|
|
|
580
583
|
if jinxs is None:
|
|
581
584
|
jinxs = _get_jinxs(npc, team)
|
|
582
585
|
|
|
583
|
-
#
|
|
584
|
-
tools = None
|
|
585
|
-
if tool_capable is not False and jinxs:
|
|
586
|
-
tools = _jinxs_to_tools(jinxs)
|
|
587
|
-
|
|
588
|
-
# Keep full message history, only truncate for API calls to reduce tokens
|
|
586
|
+
# Keep full message history
|
|
589
587
|
full_messages = messages.copy() if messages else []
|
|
590
588
|
|
|
591
|
-
#
|
|
589
|
+
# If we have jinxs, use ReAct fallback (JSON prompting) instead of tool_calls
|
|
590
|
+
if jinxs:
|
|
591
|
+
return _react_fallback(
|
|
592
|
+
command, model, provider, api_url, api_key, npc, team,
|
|
593
|
+
full_messages, images, stream, context, jinxs, extra_globals, max_iterations
|
|
594
|
+
)
|
|
592
595
|
|
|
596
|
+
# No jinxs - just get a direct response
|
|
593
597
|
try:
|
|
594
598
|
response = get_llm_response(
|
|
595
599
|
command,
|
|
@@ -597,13 +601,12 @@ def check_llm_command(
|
|
|
597
601
|
provider=provider,
|
|
598
602
|
api_url=api_url,
|
|
599
603
|
api_key=api_key,
|
|
600
|
-
messages=messages[-10:],
|
|
604
|
+
messages=messages[-10:],
|
|
601
605
|
npc=npc,
|
|
602
606
|
team=team,
|
|
603
607
|
images=images,
|
|
604
608
|
stream=stream,
|
|
605
609
|
context=context,
|
|
606
|
-
tools=tools,
|
|
607
610
|
)
|
|
608
611
|
except Exception as e:
|
|
609
612
|
print(f"[check_llm_command] EXCEPTION in get_llm_response: {type(e).__name__}: {e}", "red")
|
|
@@ -614,156 +617,17 @@ def check_llm_command(
|
|
|
614
617
|
"usage": total_usage,
|
|
615
618
|
}
|
|
616
619
|
|
|
617
|
-
|
|
618
620
|
if response.get("usage"):
|
|
619
621
|
total_usage["input_tokens"] += response["usage"].get("input_tokens", 0)
|
|
620
622
|
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
621
623
|
|
|
622
|
-
# Check if tool calls were made
|
|
623
|
-
tool_calls = response.get("tool_calls", [])
|
|
624
|
-
if not tool_calls:
|
|
625
|
-
# Direct answer - append user message to full messages
|
|
626
|
-
full_messages.append({"role": "user", "content": command})
|
|
627
|
-
assistant_response = response.get("response", "")
|
|
628
|
-
# Only append assistant message if it's a string (not a stream)
|
|
629
|
-
# For streaming, the caller (process_result) handles appending after consumption
|
|
630
|
-
if assistant_response and isinstance(assistant_response, str):
|
|
631
|
-
full_messages.append({"role": "assistant", "content": assistant_response})
|
|
632
|
-
return {
|
|
633
|
-
"messages": full_messages,
|
|
634
|
-
"output": assistant_response,
|
|
635
|
-
"usage": total_usage,
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
# Helper to serialize tool_calls for message history
|
|
639
|
-
def _serialize_tool_calls(tcs):
|
|
640
|
-
"""Convert tool_call objects to dicts for message history."""
|
|
641
|
-
serialized = []
|
|
642
|
-
for tc in tcs:
|
|
643
|
-
if hasattr(tc, 'id'):
|
|
644
|
-
# It's an object, convert to dict
|
|
645
|
-
func = tc.function
|
|
646
|
-
serialized.append({
|
|
647
|
-
"id": tc.id,
|
|
648
|
-
"type": "function",
|
|
649
|
-
"function": {
|
|
650
|
-
"name": func.name if hasattr(func, 'name') else func.get('name'),
|
|
651
|
-
"arguments": func.arguments if hasattr(func, 'arguments') else func.get('arguments', '{}')
|
|
652
|
-
}
|
|
653
|
-
})
|
|
654
|
-
else:
|
|
655
|
-
# Already a dict
|
|
656
|
-
serialized.append(tc)
|
|
657
|
-
return serialized
|
|
658
|
-
|
|
659
|
-
# Execute tool calls in a loop - start with full messages
|
|
660
624
|
full_messages.append({"role": "user", "content": command})
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
assistant_msg["tool_calls"] = _serialize_tool_calls(tool_calls)
|
|
665
|
-
full_messages.append(assistant_msg)
|
|
666
|
-
current_messages = full_messages
|
|
667
|
-
for _ in range(max_iterations):
|
|
668
|
-
for tc in tool_calls:
|
|
669
|
-
# Handle both dict and object formats
|
|
670
|
-
if hasattr(tc, 'function'):
|
|
671
|
-
func = tc.function
|
|
672
|
-
jinx_name = func.name if hasattr(func, 'name') else func.get('name')
|
|
673
|
-
args_str = func.arguments if hasattr(func, 'arguments') else func.get('arguments', '{}')
|
|
674
|
-
tc_id = tc.id if hasattr(tc, 'id') else tc.get('id', '')
|
|
675
|
-
else:
|
|
676
|
-
func = tc.get("function", {})
|
|
677
|
-
jinx_name = func.get("name")
|
|
678
|
-
args_str = func.get("arguments", "{}")
|
|
679
|
-
tc_id = tc.get("id", "")
|
|
680
|
-
|
|
681
|
-
try:
|
|
682
|
-
inputs = json.loads(args_str) if isinstance(args_str, str) else args_str
|
|
683
|
-
except:
|
|
684
|
-
inputs = {}
|
|
685
|
-
|
|
686
|
-
if jinx_name in jinxs:
|
|
687
|
-
try:
|
|
688
|
-
|
|
689
|
-
print((f" ⚡ {jinx_name}", "cyan"), end="", flush=True)
|
|
690
|
-
except:
|
|
691
|
-
pass
|
|
692
|
-
output = _execute_jinx(jinxs[jinx_name], inputs, npc, team, current_messages, extra_globals)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
# Add tool result to messages
|
|
696
|
-
# Include name for Gemini compatibility
|
|
697
|
-
current_messages.append({
|
|
698
|
-
"role": "tool",
|
|
699
|
-
"tool_call_id": tc_id,
|
|
700
|
-
"name": jinx_name,
|
|
701
|
-
"content": str(output)
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
# Get next response - truncate carefully to not orphan tool responses
|
|
705
|
-
# Find a safe truncation point that doesn't split tool_call/tool_response pairs
|
|
706
|
-
truncated = current_messages
|
|
707
|
-
if len(current_messages) > 15:
|
|
708
|
-
# Start from -15 and walk back to find a user message or start
|
|
709
|
-
start_idx = len(current_messages) - 15
|
|
710
|
-
while start_idx > 0:
|
|
711
|
-
msg = current_messages[start_idx]
|
|
712
|
-
# Don't start on a tool response - would orphan it
|
|
713
|
-
if msg.get("role") == "tool":
|
|
714
|
-
start_idx -= 1
|
|
715
|
-
# Don't start on assistant with tool_calls unless next is tool response
|
|
716
|
-
elif msg.get("role") == "assistant" and msg.get("tool_calls"):
|
|
717
|
-
start_idx -= 1
|
|
718
|
-
else:
|
|
719
|
-
break
|
|
720
|
-
truncated = current_messages[start_idx:]
|
|
721
|
-
|
|
722
|
-
try:
|
|
723
|
-
response = get_llm_response(
|
|
724
|
-
"", # continuation
|
|
725
|
-
model=model,
|
|
726
|
-
provider=provider,
|
|
727
|
-
api_url=api_url,
|
|
728
|
-
api_key=api_key,
|
|
729
|
-
messages=truncated,
|
|
730
|
-
npc=npc,
|
|
731
|
-
team=team,
|
|
732
|
-
stream=stream,
|
|
733
|
-
context=context,
|
|
734
|
-
tools=tools,
|
|
735
|
-
)
|
|
736
|
-
except Exception as e:
|
|
737
|
-
# If continuation fails, return what we have so far
|
|
738
|
-
return {
|
|
739
|
-
"messages": current_messages,
|
|
740
|
-
"output": f"Tool executed successfully. (Continuation error: {type(e).__name__})",
|
|
741
|
-
"usage": total_usage,
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if response.get("usage"):
|
|
745
|
-
total_usage["input_tokens"] += response["usage"].get("input_tokens", 0)
|
|
746
|
-
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
747
|
-
|
|
748
|
-
tool_calls = response.get("tool_calls", [])
|
|
749
|
-
# Append assistant response to full messages with tool_calls if present
|
|
750
|
-
assistant_response = response.get("response", "")
|
|
751
|
-
assistant_msg = {"role": "assistant", "content": assistant_response}
|
|
752
|
-
if tool_calls:
|
|
753
|
-
assistant_msg["tool_calls"] = _serialize_tool_calls(tool_calls)
|
|
754
|
-
current_messages.append(assistant_msg)
|
|
755
|
-
|
|
756
|
-
if not tool_calls:
|
|
757
|
-
# Done - return full message history
|
|
758
|
-
return {
|
|
759
|
-
"messages": current_messages,
|
|
760
|
-
"output": assistant_response,
|
|
761
|
-
"usage": total_usage,
|
|
762
|
-
}
|
|
763
|
-
|
|
625
|
+
assistant_response = response.get("response", "")
|
|
626
|
+
if assistant_response and isinstance(assistant_response, str):
|
|
627
|
+
full_messages.append({"role": "assistant", "content": assistant_response})
|
|
764
628
|
return {
|
|
765
|
-
"messages":
|
|
766
|
-
"output":
|
|
629
|
+
"messages": full_messages,
|
|
630
|
+
"output": assistant_response,
|
|
767
631
|
"usage": total_usage,
|
|
768
632
|
}
|
|
769
633
|
|
|
@@ -775,10 +639,12 @@ def _react_fallback(
|
|
|
775
639
|
"""ReAct-style fallback for models without tool calling."""
|
|
776
640
|
import logging
|
|
777
641
|
logger = logging.getLogger("npcpy.llm_funcs")
|
|
778
|
-
logger.debug(f"[_react_fallback] Starting with {len(messages) if messages else 0} messages")
|
|
642
|
+
logger.debug(f"[_react_fallback] Starting with {len(messages) if messages else 0} messages, jinxs: {list(jinxs.keys()) if jinxs else None}")
|
|
779
643
|
|
|
780
644
|
total_usage = {"input_tokens": 0, "output_tokens": 0}
|
|
781
645
|
current_messages = messages.copy() if messages else []
|
|
646
|
+
jinx_executions = [] # Track jinx calls for UI display
|
|
647
|
+
generated_images = [] # Track images generated by jinxs for subsequent LLM calls
|
|
782
648
|
logger.debug(f"[_react_fallback] current_messages initialized with {len(current_messages)} messages")
|
|
783
649
|
|
|
784
650
|
# Build jinx list with input parameters
|
|
@@ -789,17 +655,32 @@ def _react_fallback(
|
|
|
789
655
|
|
|
790
656
|
jinx_list = "\n".join(_jinx_info(n, j) for n, j in jinxs.items()) if jinxs else "None"
|
|
791
657
|
|
|
792
|
-
|
|
658
|
+
# Cap iterations - after this, return to orchestrator for review/compression
|
|
659
|
+
effective_max = min(max_iterations, 7)
|
|
660
|
+
|
|
661
|
+
for iteration in range(effective_max):
|
|
662
|
+
# Build history of what's been tried
|
|
663
|
+
history_text = ""
|
|
664
|
+
if jinx_executions:
|
|
665
|
+
history_text = "\n\nPrevious tool calls this session:\n" + "\n".join(
|
|
666
|
+
f"- {h['name']}({h['inputs']}) -> {h['output']}"
|
|
667
|
+
for h in jinx_executions[-5:]
|
|
668
|
+
)
|
|
669
|
+
|
|
793
670
|
prompt = f"""Request: {command}
|
|
794
671
|
|
|
795
|
-
Tools:
|
|
672
|
+
Available Tools:
|
|
796
673
|
{jinx_list}
|
|
797
674
|
|
|
798
|
-
|
|
799
|
-
|
|
675
|
+
Instructions:
|
|
676
|
+
1. Analyze the request and determine the best tool to use
|
|
677
|
+
2. If you have enough information to answer, use {{"action": "answer", "response": "your answer"}}
|
|
678
|
+
3. If you need to use a tool, use {{"action": "jinx", "jinx_name": "tool_name", "inputs": {{"param": "value"}}}}
|
|
679
|
+
4. Use EXACT parameter names from tool definitions
|
|
680
|
+
5. Do NOT repeat the same tool call with the same inputs{history_text}"""
|
|
800
681
|
|
|
801
682
|
if context:
|
|
802
|
-
prompt += f"\
|
|
683
|
+
prompt += f"\n\nCurrent context: {context}"
|
|
803
684
|
|
|
804
685
|
response = get_llm_response(
|
|
805
686
|
prompt,
|
|
@@ -810,7 +691,7 @@ Use EXACT parameter names from the tool definitions above."""
|
|
|
810
691
|
messages=current_messages[-10:],
|
|
811
692
|
npc=npc,
|
|
812
693
|
team=team,
|
|
813
|
-
images=images if iteration == 0 else None,
|
|
694
|
+
images=((images or []) if iteration == 0 else []) + generated_images or None,
|
|
814
695
|
format="json",
|
|
815
696
|
context=context,
|
|
816
697
|
)
|
|
@@ -820,14 +701,32 @@ Use EXACT parameter names from the tool definitions above."""
|
|
|
820
701
|
total_usage["output_tokens"] += response["usage"].get("output_tokens", 0)
|
|
821
702
|
|
|
822
703
|
decision = response.get("response", {})
|
|
704
|
+
logger.debug(f"[_react_fallback] Raw decision: {str(decision)[:200]}")
|
|
705
|
+
print(f"[REACT-DEBUG] Full response keys: {response.keys()}")
|
|
706
|
+
print(f"[REACT-DEBUG] Raw response['response']: {str(response.get('response', 'NONE'))[:500]}")
|
|
707
|
+
print(f"[REACT-DEBUG] Raw decision type: {type(decision)}, value: {str(decision)[:500]}")
|
|
823
708
|
if isinstance(decision, str):
|
|
824
709
|
try:
|
|
825
710
|
decision = json.loads(decision)
|
|
826
711
|
except:
|
|
827
|
-
|
|
712
|
+
logger.debug(f"[_react_fallback] Could not parse JSON, returning as text")
|
|
713
|
+
return {"messages": current_messages, "output": decision, "usage": total_usage, "jinx_executions": jinx_executions}
|
|
828
714
|
|
|
715
|
+
logger.debug(f"[_react_fallback] Parsed decision action: {decision.get('action')}")
|
|
829
716
|
if decision.get("action") == "answer":
|
|
830
717
|
output = decision.get("response", "")
|
|
718
|
+
|
|
719
|
+
# Prepend any image URLs from jinx executions to the output
|
|
720
|
+
import re
|
|
721
|
+
for jexec in jinx_executions:
|
|
722
|
+
joutput = jexec.get("output", "")
|
|
723
|
+
if joutput:
|
|
724
|
+
# Find image URLs in jinx output
|
|
725
|
+
img_urls = re.findall(r'/uploads/[^\s,\'"]+\.(?:png|jpg|jpeg|webp|gif)', str(joutput), re.IGNORECASE)
|
|
726
|
+
for url in img_urls:
|
|
727
|
+
if url not in output:
|
|
728
|
+
output = f"\n\n{output}"
|
|
729
|
+
|
|
831
730
|
# Add user message to full history
|
|
832
731
|
current_messages.append({"role": "user", "content": command})
|
|
833
732
|
if stream:
|
|
@@ -838,35 +737,140 @@ Use EXACT parameter names from the tool definitions above."""
|
|
|
838
737
|
total_usage["output_tokens"] += final["usage"].get("output_tokens", 0)
|
|
839
738
|
# Return full messages, not truncated from final
|
|
840
739
|
logger.debug(f"[_react_fallback] Answer (stream) - returning {len(current_messages)} messages")
|
|
841
|
-
return {"messages": current_messages, "output": final.get("response", output), "usage": total_usage}
|
|
740
|
+
return {"messages": current_messages, "output": final.get("response", output), "usage": total_usage, "jinx_executions": jinx_executions}
|
|
842
741
|
# Non-streaming: add assistant response to full history
|
|
843
742
|
if output and isinstance(output, str):
|
|
844
743
|
current_messages.append({"role": "assistant", "content": output})
|
|
845
744
|
logger.debug(f"[_react_fallback] Answer - returning {len(current_messages)} messages")
|
|
846
|
-
return {"messages": current_messages, "output": output, "usage": total_usage}
|
|
847
|
-
|
|
848
|
-
elif decision.get("action") == "jinx":
|
|
849
|
-
|
|
745
|
+
return {"messages": current_messages, "output": output, "usage": total_usage, "jinx_executions": jinx_executions}
|
|
746
|
+
|
|
747
|
+
elif decision.get("action") == "jinx" or decision.get("action") in jinxs:
|
|
748
|
+
# Handle multiple formats:
|
|
749
|
+
# 1. {"action": "jinx", "jinx_name": "foo", "inputs": {...}}
|
|
750
|
+
# 2. {"action": "foo", "inputs": {...}}
|
|
751
|
+
# 3. {"action": "foo", "param1": "...", "param2": "..."} - params at top level
|
|
752
|
+
jinx_name = decision.get("jinx_name") or decision.get("action")
|
|
850
753
|
inputs = decision.get("inputs", {})
|
|
851
754
|
|
|
755
|
+
# If inputs is empty, check if params are at top level
|
|
756
|
+
if not inputs:
|
|
757
|
+
# Extract all keys except 'action', 'jinx_name', 'inputs' as potential inputs
|
|
758
|
+
inputs = {k: v for k, v in decision.items() if k not in ('action', 'jinx_name', 'inputs', 'response')}
|
|
759
|
+
logger.debug(f"[_react_fallback] Jinx action: {jinx_name} with inputs: {inputs}")
|
|
760
|
+
print(f"[REACT-DEBUG] Chose jinx: {jinx_name}, inputs: {str(inputs)[:200]}")
|
|
761
|
+
|
|
852
762
|
if jinx_name not in jinxs:
|
|
853
763
|
context = f"Error: '{jinx_name}' not found. Available: {list(jinxs.keys())}"
|
|
764
|
+
logger.debug(f"[_react_fallback] Jinx not found: {jinx_name}")
|
|
854
765
|
continue
|
|
855
766
|
|
|
856
|
-
|
|
857
|
-
|
|
767
|
+
# Validate required parameters before executing
|
|
768
|
+
jinx_obj = jinxs[jinx_name]
|
|
769
|
+
# Handle both dict and Jinx object
|
|
770
|
+
if hasattr(jinx_obj, 'inputs'):
|
|
771
|
+
required_inputs = jinx_obj.inputs or []
|
|
772
|
+
elif isinstance(jinx_obj, dict):
|
|
773
|
+
required_inputs = jinx_obj.get('inputs', [])
|
|
774
|
+
else:
|
|
775
|
+
required_inputs = []
|
|
776
|
+
|
|
777
|
+
if required_inputs:
|
|
778
|
+
# Get parameter names, distinguishing required (string) from optional with defaults (dict)
|
|
779
|
+
required_names = [] # Params without defaults - truly required
|
|
780
|
+
optional_names = [] # Params with defaults - not required
|
|
781
|
+
for inp in required_inputs:
|
|
782
|
+
if isinstance(inp, str):
|
|
783
|
+
# String inputs have no default, so they're required
|
|
784
|
+
required_names.append(inp)
|
|
785
|
+
elif isinstance(inp, dict):
|
|
786
|
+
# Dict inputs have defaults (e.g., "backup: true"), so they're optional
|
|
787
|
+
optional_names.extend(inp.keys())
|
|
788
|
+
|
|
789
|
+
# Only check truly required params (those without defaults)
|
|
790
|
+
missing = [p for p in required_names if p not in inputs or not inputs.get(p)]
|
|
791
|
+
provided = list(inputs.keys())
|
|
792
|
+
if missing:
|
|
793
|
+
all_params = required_names + optional_names
|
|
794
|
+
context = f"Error: jinx '{jinx_name}' requires parameters {required_names} but got {provided}. Missing: {missing}. Optional params with defaults: {optional_names}. Please retry with correct parameter names."
|
|
795
|
+
logger.debug(f"[_react_fallback] Missing required params: {missing}")
|
|
796
|
+
print(f"[REACT-DEBUG] Missing params for {jinx_name}: {missing}, got: {provided}")
|
|
797
|
+
continue
|
|
798
|
+
|
|
799
|
+
logger.debug(f"[_react_fallback] Executing jinx: {jinx_name}")
|
|
858
800
|
output = _execute_jinx(jinxs[jinx_name], inputs, npc, team, current_messages, extra_globals)
|
|
801
|
+
logger.debug(f"[_react_fallback] Jinx output: {str(output)[:200]}")
|
|
802
|
+
jinx_executions.append({
|
|
803
|
+
"name": jinx_name,
|
|
804
|
+
"inputs": inputs,
|
|
805
|
+
"output": str(output) if output else None
|
|
806
|
+
})
|
|
859
807
|
|
|
860
|
-
|
|
861
|
-
|
|
808
|
+
# Extract generated image paths from output for subsequent LLM calls
|
|
809
|
+
import re
|
|
810
|
+
image_paths = re.findall(r'/uploads/[^\s,\'"]+\.(?:png|jpg|jpeg|webp|gif)', str(output), re.IGNORECASE)
|
|
811
|
+
if image_paths:
|
|
812
|
+
# Convert relative URLs to absolute file paths
|
|
813
|
+
for img_path in image_paths:
|
|
814
|
+
# Remove leading slash
|
|
815
|
+
local_path = img_path.lstrip('/')
|
|
816
|
+
# Check various possible locations
|
|
817
|
+
if os.path.exists(local_path):
|
|
818
|
+
generated_images.append(local_path)
|
|
819
|
+
print(f"[REACT-DEBUG] Added generated image: {local_path}")
|
|
820
|
+
elif os.path.exists(os.path.join(os.getcwd(), local_path)):
|
|
821
|
+
full_path = os.path.join(os.getcwd(), local_path)
|
|
822
|
+
generated_images.append(full_path)
|
|
823
|
+
print(f"[REACT-DEBUG] Added generated image (cwd): {full_path}")
|
|
824
|
+
else:
|
|
825
|
+
# Just add the URL path anyway - let get_llm_response handle it
|
|
826
|
+
generated_images.append(local_path)
|
|
827
|
+
print(f"[REACT-DEBUG] Added generated image (not found, using anyway): {local_path}")
|
|
828
|
+
|
|
829
|
+
# Truncate output for context to avoid sending huge base64 data back to LLM
|
|
830
|
+
output_for_context = str(output)[:8000] + "..." if len(str(output)) > 8000 else str(output)
|
|
831
|
+
context = f"Tool '{jinx_name}' returned: {output_for_context}"
|
|
862
832
|
command = f"{command}\n\nPrevious: {context}"
|
|
863
833
|
|
|
864
834
|
else:
|
|
865
835
|
logger.debug(f"[_react_fallback] Unknown action - returning {len(current_messages)} messages")
|
|
866
|
-
|
|
836
|
+
# If we have jinx executions, return the last output instead of empty decision
|
|
837
|
+
if jinx_executions and jinx_executions[-1].get("output"):
|
|
838
|
+
return {"messages": current_messages, "output": jinx_executions[-1]["output"], "usage": total_usage, "jinx_executions": jinx_executions}
|
|
839
|
+
# If decision is empty {}, retry with clearer prompt if jinxs are available
|
|
840
|
+
if not decision or decision == {}:
|
|
841
|
+
if jinxs and iteration < max_iterations - 1:
|
|
842
|
+
# Retry with explicit instruction to use a jinx
|
|
843
|
+
print(f"[REACT-DEBUG] Empty decision on iteration {iteration}, retrying with clearer prompt")
|
|
844
|
+
context = f"You MUST use one of these tools to complete the task: {list(jinxs.keys())}. Return JSON with action and inputs."
|
|
845
|
+
continue
|
|
846
|
+
else:
|
|
847
|
+
# Last resort: get a text response
|
|
848
|
+
print(f"[REACT-DEBUG] Empty decision, getting text response instead")
|
|
849
|
+
current_messages.append({"role": "user", "content": command})
|
|
850
|
+
fallback_response = get_llm_response(
|
|
851
|
+
command,
|
|
852
|
+
model=model,
|
|
853
|
+
provider=provider,
|
|
854
|
+
messages=current_messages[-10:],
|
|
855
|
+
npc=npc,
|
|
856
|
+
team=team,
|
|
857
|
+
stream=stream,
|
|
858
|
+
context=context,
|
|
859
|
+
)
|
|
860
|
+
if fallback_response.get("usage"):
|
|
861
|
+
total_usage["input_tokens"] += fallback_response["usage"].get("input_tokens", 0)
|
|
862
|
+
total_usage["output_tokens"] += fallback_response["usage"].get("output_tokens", 0)
|
|
863
|
+
output = fallback_response.get("response", "")
|
|
864
|
+
if output and isinstance(output, str):
|
|
865
|
+
current_messages.append({"role": "assistant", "content": output})
|
|
866
|
+
return {"messages": current_messages, "output": output, "usage": total_usage, "jinx_executions": jinx_executions}
|
|
867
|
+
return {"messages": current_messages, "output": str(decision), "usage": total_usage, "jinx_executions": jinx_executions}
|
|
867
868
|
|
|
868
869
|
logger.debug(f"[_react_fallback] Max iterations - returning {len(current_messages)} messages")
|
|
869
|
-
|
|
870
|
+
# If we have jinx executions, return the last output
|
|
871
|
+
if jinx_executions and jinx_executions[-1].get("output"):
|
|
872
|
+
return {"messages": current_messages, "output": jinx_executions[-1]["output"], "usage": total_usage, "jinx_executions": jinx_executions}
|
|
873
|
+
return {"messages": current_messages, "output": f"Max iterations reached. Last: {context}", "usage": total_usage, "jinx_executions": jinx_executions}
|
|
870
874
|
|
|
871
875
|
|
|
872
876
|
|
|
@@ -1231,104 +1235,6 @@ def abstract(groups,
|
|
|
1231
1235
|
return response["response"].get("groups", [])
|
|
1232
1236
|
|
|
1233
1237
|
|
|
1234
|
-
def extract_facts(
|
|
1235
|
-
text: str,
|
|
1236
|
-
model: str,
|
|
1237
|
-
provider: str,
|
|
1238
|
-
npc = None,
|
|
1239
|
-
context: str = None
|
|
1240
|
-
) -> List[str]:
|
|
1241
|
-
"""Extract concise facts from text using LLM (as defined earlier)"""
|
|
1242
|
-
|
|
1243
|
-
prompt = """Extract concise facts from this text.
|
|
1244
|
-
A fact is a piece of information that makes a statement about the world.
|
|
1245
|
-
A fact is typically a sentence that is true or false.
|
|
1246
|
-
Facts may be simple or complex. They can also be conflicting with each other, usually
|
|
1247
|
-
because there is some hidden context that is not mentioned in the text.
|
|
1248
|
-
In any case, it is simply your job to extract a list of facts that could pertain to
|
|
1249
|
-
an individual's personality.
|
|
1250
|
-
|
|
1251
|
-
For example, if a message says:
|
|
1252
|
-
"since I am a doctor I am often trying to think up new ways to help people.
|
|
1253
|
-
Can you help me set up a new kind of software to help with that?"
|
|
1254
|
-
You might extract the following facts:
|
|
1255
|
-
- The individual is a doctor
|
|
1256
|
-
- They are helpful
|
|
1257
|
-
|
|
1258
|
-
Another example:
|
|
1259
|
-
"I am a software engineer who loves to play video games. I am also a huge fan of the
|
|
1260
|
-
Star Wars franchise and I am a member of the 501st Legion."
|
|
1261
|
-
You might extract the following facts:
|
|
1262
|
-
- The individual is a software engineer
|
|
1263
|
-
- The individual loves to play video games
|
|
1264
|
-
- The individual is a huge fan of the Star Wars franchise
|
|
1265
|
-
- The individual is a member of the 501st Legion
|
|
1266
|
-
|
|
1267
|
-
Another example:
|
|
1268
|
-
"The quantum tunneling effect allows particles to pass through barriers
|
|
1269
|
-
that classical physics says they shouldn't be able to cross. This has
|
|
1270
|
-
huge implications for semiconductor design."
|
|
1271
|
-
You might extract these facts:
|
|
1272
|
-
- Quantum tunneling enables particles to pass through barriers that are
|
|
1273
|
-
impassable according to classical physics
|
|
1274
|
-
- The behavior of quantum tunneling has significant implications for
|
|
1275
|
-
how semiconductors must be designed
|
|
1276
|
-
|
|
1277
|
-
Another example:
|
|
1278
|
-
"People used to think the Earth was flat. Now we know it's spherical,
|
|
1279
|
-
though technically it's an oblate spheroid due to its rotation."
|
|
1280
|
-
You might extract these facts:
|
|
1281
|
-
- People historically believed the Earth was flat
|
|
1282
|
-
- It is now known that the Earth is an oblate spheroid
|
|
1283
|
-
- The Earth's oblate spheroid shape is caused by its rotation
|
|
1284
|
-
|
|
1285
|
-
Another example:
|
|
1286
|
-
"My research on black holes suggests they emit radiation, but my professor
|
|
1287
|
-
says this conflicts with Einstein's work. After reading more papers, I
|
|
1288
|
-
learned this is actually Hawking radiation and doesn't conflict at all."
|
|
1289
|
-
You might extract the following facts:
|
|
1290
|
-
- Black holes emit radiation
|
|
1291
|
-
- The professor believes this radiation conflicts with Einstein's work
|
|
1292
|
-
- The radiation from black holes is called Hawking radiation
|
|
1293
|
-
- Hawking radiation does not conflict with Einstein's work
|
|
1294
|
-
|
|
1295
|
-
Another example:
|
|
1296
|
-
"During the pandemic, many developers switched to remote work. I found
|
|
1297
|
-
that I'm actually more productive at home, though my company initially
|
|
1298
|
-
thought productivity would drop. Now they're keeping remote work permanent."
|
|
1299
|
-
You might extract the following facts:
|
|
1300
|
-
- The pandemic caused many developers to switch to remote work
|
|
1301
|
-
- The individual discovered higher productivity when working from home
|
|
1302
|
-
- The company predicted productivity would decrease with remote work
|
|
1303
|
-
- The company decided to make remote work a permanent option
|
|
1304
|
-
|
|
1305
|
-
Thus, it is your mission to reliably extract lists of facts.
|
|
1306
|
-
|
|
1307
|
-
Return a JSON object with the following structure:
|
|
1308
|
-
{
|
|
1309
|
-
"fact_list": "a list containing the facts where each fact is a string",
|
|
1310
|
-
}
|
|
1311
|
-
"""
|
|
1312
|
-
if context and len(context) > 0:
|
|
1313
|
-
prompt+=f""" Here is some relevant user context: {context}"""
|
|
1314
|
-
|
|
1315
|
-
prompt+="""
|
|
1316
|
-
Return only the JSON object.
|
|
1317
|
-
Do not include any additional markdown formatting.
|
|
1318
|
-
"""
|
|
1319
|
-
|
|
1320
|
-
response = get_llm_response(
|
|
1321
|
-
prompt + f"HERE BEGINS THE TEXT TO INVESTIGATE:\n\nText: {text}",
|
|
1322
|
-
model=model,
|
|
1323
|
-
provider=provider,
|
|
1324
|
-
format="json",
|
|
1325
|
-
npc=npc,
|
|
1326
|
-
context=context,
|
|
1327
|
-
)
|
|
1328
|
-
response = response["response"]
|
|
1329
|
-
return response.get("fact_list", [])
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
1238
|
def get_facts(content_text,
|
|
1333
1239
|
model= None,
|
|
1334
1240
|
provider = None,
|