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