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/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, only truncate for API calls to reduce tokens
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
- # Try with native tool calling first
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:], # Truncate for API call only
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(colored(f"[check_llm_command] EXCEPTION in get_llm_response: {type(e).__name__}: {e}", "red"))
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
- # Add assistant message with tool_calls before executing them
668
- assistant_msg = {"role": "assistant", "content": response.get("response", "")}
669
- if tool_calls:
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": current_messages,
780
- "output": response.get("response", "Max iterations reached"),
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
- for iteration in range(max_iterations):
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
- Return JSON: {{"action": "answer", "response": "..."}} OR {{"action": "jinx", "jinx_name": "...", "inputs": {{"param_name": "value"}}}}
813
- Use EXACT parameter names from the tool definitions above."""
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"\nContext: {context}"
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
- return {"messages": current_messages, "output": decision, "usage": total_usage}
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"![Generated Image]({url})\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
- jinx_name = decision.get("jinx_name")
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
- try:
871
- from termcolor import colored
872
- print(colored(f" ⚡ {jinx_name}", "cyan"), end="", flush=True)
873
- except:
874
- pass
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
- try:
877
- print(colored(" ✓", "green"), flush=True)
878
- except:
879
- pass
880
- context = f"Tool '{jinx_name}' returned: {output}"
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
- return {"messages": current_messages, "output": str(decision), "usage": total_usage}
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
- return {"messages": current_messages, "output": f"Max iterations reached. Last: {context}", "usage": total_usage}
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,