npcsh 1.1.5__py3-none-any.whl → 1.1.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.
Files changed (76) hide show
  1. npcsh/_state.py +438 -319
  2. npcsh/npc_team/jinxs/code/sh.jinx +0 -1
  3. npcsh/npc_team/jinxs/code/sql.jinx +1 -3
  4. npcsh/npc_team/jinxs/utils/npc-studio.jinx +33 -38
  5. npcsh/npc_team/jinxs/utils/ots.jinx +34 -65
  6. npcsh/npc_team/jinxs/utils/search.jinx +130 -0
  7. npcsh/npc_team/jinxs/utils/vixynt.jinx +33 -45
  8. npcsh/routes.py +32 -14
  9. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/npc-studio.jinx +33 -38
  10. npcsh-1.1.6.data/data/npcsh/npc_team/ots.jinx +61 -0
  11. npcsh-1.1.6.data/data/npcsh/npc_team/search.jinx +130 -0
  12. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sh.jinx +0 -1
  13. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sql.jinx +1 -3
  14. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/vixynt.jinx +33 -45
  15. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/METADATA +1 -10
  16. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/RECORD +65 -73
  17. npcsh/npc_team/jinxs/utils/search/brainblast.jinx +0 -51
  18. npcsh/npc_team/jinxs/utils/search/kg_search.jinx +0 -43
  19. npcsh/npc_team/jinxs/utils/search/memory_search.jinx +0 -36
  20. npcsh/npc_team/jinxs/utils/search/rag.jinx +0 -70
  21. npcsh/npc_team/jinxs/utils/search/search.jinx +0 -192
  22. npcsh-1.1.5.data/data/npcsh/npc_team/brainblast.jinx +0 -51
  23. npcsh-1.1.5.data/data/npcsh/npc_team/kg_search.jinx +0 -43
  24. npcsh-1.1.5.data/data/npcsh/npc_team/memory_search.jinx +0 -36
  25. npcsh-1.1.5.data/data/npcsh/npc_team/ots.jinx +0 -92
  26. npcsh-1.1.5.data/data/npcsh/npc_team/rag.jinx +0 -70
  27. npcsh-1.1.5.data/data/npcsh/npc_team/search.jinx +0 -192
  28. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/alicanto.jinx +0 -0
  29. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  30. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/alicanto.png +0 -0
  31. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/breathe.jinx +0 -0
  32. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/build.jinx +0 -0
  33. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/compile.jinx +0 -0
  34. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/corca.jinx +0 -0
  35. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/corca.npc +0 -0
  36. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/corca.png +0 -0
  37. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/corca_example.png +0 -0
  38. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  39. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/flush.jinx +0 -0
  40. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/foreman.npc +0 -0
  41. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/frederic.npc +0 -0
  42. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/frederic4.png +0 -0
  43. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/guac.jinx +0 -0
  44. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/guac.png +0 -0
  45. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/help.jinx +0 -0
  46. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/init.jinx +0 -0
  47. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/jinxs.jinx +0 -0
  48. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  49. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  50. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  51. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  52. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plan.jinx +0 -0
  53. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plonk.jinx +0 -0
  54. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plonk.npc +0 -0
  55. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plonk.png +0 -0
  56. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plonkjr.npc +0 -0
  57. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  58. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/pti.jinx +0 -0
  59. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/python.jinx +0 -0
  60. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/roll.jinx +0 -0
  61. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sample.jinx +0 -0
  62. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/serve.jinx +0 -0
  63. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/set.jinx +0 -0
  64. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  65. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sibiji.png +0 -0
  66. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  67. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/spool.jinx +0 -0
  68. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/spool.png +0 -0
  69. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  70. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/wander.jinx +0 -0
  71. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/yap.jinx +0 -0
  72. {npcsh-1.1.5.data → npcsh-1.1.6.data}/data/npcsh/npc_team/yap.png +0 -0
  73. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/WHEEL +0 -0
  74. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/entry_points.txt +0 -0
  75. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/licenses/LICENSE +0 -0
  76. {npcsh-1.1.5.dist-info → npcsh-1.1.6.dist-info}/top_level.txt +0 -0
npcsh/_state.py CHANGED
@@ -88,12 +88,18 @@ from npcpy.llm_funcs import (
88
88
  breathe,
89
89
 
90
90
  )
91
+
91
92
  from npcpy.memory.knowledge_graph import (
92
93
  kg_evolve_incremental,
93
94
 
94
95
  )
95
96
  from npcpy.gen.embeddings import get_embeddings
96
97
 
98
+ import inspect
99
+ import sys
100
+ from npcpy.memory.search import execute_rag_command, execute_brainblast_command
101
+ from npcpy.data.load import load_file_contents
102
+ from npcpy.data.web import search_web
97
103
  try:
98
104
  import readline
99
105
  except:
@@ -443,6 +449,138 @@ def get_team_ctx_path(team_path: str) -> Optional[str]:
443
449
  return str(ctx_files[0]) if ctx_files else None
444
450
 
445
451
 
452
+ from npcpy.memory.memory_processor import memory_approval_ui
453
+ from npcpy.ft.memory_trainer import MemoryTrainer
454
+ from npcpy.llm_funcs import get_facts
455
+
456
+ def get_relevant_memories(
457
+ command_history: CommandHistory,
458
+ npc_name: str,
459
+ team_name: str,
460
+ path: str,
461
+ query: Optional[str] = None,
462
+ max_memories: int = 10,
463
+ state: Optional[ShellState] = None
464
+ ) -> List[Dict]:
465
+
466
+ engine = command_history.engine
467
+
468
+ all_memories = command_history.get_memories_for_scope(
469
+ npc=npc_name,
470
+ team=team_name,
471
+ directory_path=path,
472
+ )
473
+
474
+ if not all_memories:
475
+ return []
476
+
477
+ if len(all_memories) <= max_memories and not query:
478
+ return all_memories
479
+
480
+ if query:
481
+ query_lower = query.lower()
482
+ keyword_matches = [
483
+ m for m in all_memories
484
+ if query_lower in (m.get('final_memory') or m.get('initial_memory') or '').lower()
485
+ ]
486
+
487
+ if keyword_matches:
488
+ return keyword_matches[:max_memories]
489
+
490
+ if state and state.embedding_model and state.embedding_provider:
491
+ try:
492
+ from npcpy.gen.embeddings import get_embeddings
493
+
494
+ search_text = query if query else "recent context"
495
+ query_embedding = get_embeddings(
496
+ [search_text],
497
+ state.embedding_model,
498
+ state.embedding_provider
499
+ )[0]
500
+
501
+ memory_texts = [
502
+ m.get('final_memory', '') for m in all_memories
503
+ ]
504
+ memory_embeddings = get_embeddings(
505
+ memory_texts,
506
+ state.embedding_model,
507
+ state.embedding_provider
508
+ )
509
+
510
+ import numpy as np
511
+ similarities = []
512
+ for mem_emb in memory_embeddings:
513
+ similarity = np.dot(query_embedding, mem_emb) / (
514
+ np.linalg.norm(query_embedding) *
515
+ np.linalg.norm(mem_emb)
516
+ )
517
+ similarities.append(similarity)
518
+
519
+ sorted_indices = np.argsort(similarities)[::-1]
520
+ return [all_memories[i] for i in sorted_indices[:max_memories]]
521
+
522
+ except Exception as e:
523
+ print(colored(
524
+ f"RAG search failed, using recent: {e}",
525
+ "yellow"
526
+ ))
527
+
528
+ return all_memories[-max_memories:]
529
+
530
+
531
+ def search_kg_facts(
532
+ self,
533
+ npc: str,
534
+ team: str,
535
+ directory_path: str,
536
+ query: str
537
+ ) -> List[Dict]:
538
+
539
+ kg = load_kg_from_db(
540
+ self.engine,
541
+ team,
542
+ npc,
543
+ directory_path
544
+ )
545
+
546
+ if not kg or 'facts' not in kg:
547
+ return []
548
+
549
+ query_lower = query.lower()
550
+ matching_facts = []
551
+
552
+ for fact in kg['facts']:
553
+ statement = fact.get('statement', '').lower()
554
+ if query_lower in statement:
555
+ matching_facts.append(fact)
556
+
557
+ return matching_facts
558
+
559
+ def format_memory_context(memory_examples):
560
+ if not memory_examples:
561
+ return ""
562
+
563
+ context_parts = []
564
+
565
+ approved_examples = memory_examples.get("approved", [])
566
+ rejected_examples = memory_examples.get("rejected", [])
567
+
568
+ if approved_examples:
569
+ context_parts.append("EXAMPLES OF GOOD MEMORIES:")
570
+ for ex in approved_examples[:5]:
571
+ final = ex.get("final_memory") or ex.get("initial_memory")
572
+ context_parts.append(f"- {final}")
573
+
574
+ if rejected_examples:
575
+ context_parts.append("\nEXAMPLES OF POOR MEMORIES TO AVOID:")
576
+ for ex in rejected_examples[:3]:
577
+ context_parts.append(f"- {ex.get('initial_memory')}")
578
+
579
+ if context_parts:
580
+ context_parts.append("\nLearn from these examples to generate similar high-quality memories.")
581
+ return "\n".join(context_parts)
582
+
583
+ return ""
446
584
  def add_npcshrc_to_shell_config() -> None:
447
585
  """
448
586
  Function Description:
@@ -1863,18 +2001,19 @@ def should_skip_kg_processing(user_input: str, assistant_output: str) -> bool:
1863
2001
 
1864
2002
  return False
1865
2003
 
1866
-
1867
-
1868
-
1869
2004
  def execute_slash_command(command: str,
1870
2005
  stdin_input: Optional[str],
1871
2006
  state: ShellState,
1872
2007
  stream: bool,
1873
2008
  router) -> Tuple[ShellState, Any]:
1874
- """Executes slash commands using the router or checking NPC/Team jinxs."""
1875
- all_command_parts = shlex.split(command)
2009
+ """Executes slash commands using the router."""
2010
+ try:
2011
+ all_command_parts = shlex.split(command)
2012
+ except ValueError:
2013
+ all_command_parts = command.split()
1876
2014
  command_name = all_command_parts[0].lstrip('/')
1877
2015
 
2016
+ # --- NPC SWITCHING LOGIC ---
1878
2017
  if command_name in ['n', 'npc']:
1879
2018
  npc_to_switch_to = all_command_parts[1] if len(all_command_parts) > 1 else None
1880
2019
  if npc_to_switch_to and state.team and npc_to_switch_to in state.team.npcs:
@@ -1884,129 +2023,35 @@ def execute_slash_command(command: str,
1884
2023
  available_npcs = list(state.team.npcs.keys()) if state.team else []
1885
2024
  return state, {"output": colored(f"NPC '{npc_to_switch_to}' not found. Available NPCs: {', '.join(available_npcs)}", "red"), "messages": state.messages}
1886
2025
 
2026
+ # --- ROUTER LOGIC ---
1887
2027
  handler = router.get_route(command_name)
1888
2028
  if handler:
1889
- parsed_flags, positional_args = parse_generic_command_flags(all_command_parts[1:])
1890
- normalized_flags = normalize_and_expand_flags(parsed_flags)
1891
-
1892
2029
  handler_kwargs = {
1893
- 'stream': stream,
1894
- 'team': state.team,
1895
- 'messages': state.messages,
1896
- 'api_url': state.api_url,
1897
- 'api_key': state.api_key,
1898
- 'stdin_input': stdin_input,
1899
- 'positional_args': positional_args,
1900
- 'plonk_context': state.team.shared_context.get('PLONK_CONTEXT') if state.team and hasattr(state.team, 'shared_context') else None,
1901
-
2030
+ 'stream': stream, 'team': state.team, 'messages': state.messages, 'api_url': state.api_url,
2031
+ 'api_key': state.api_key, 'stdin_input': stdin_input,
1902
2032
  'model': state.npc.model if isinstance(state.npc, NPC) and state.npc.model else state.chat_model,
1903
2033
  'provider': state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else state.chat_provider,
1904
- 'npc': state.npc,
1905
-
1906
- 'sprovider': state.search_provider,
1907
- 'emodel': state.embedding_model,
1908
- 'eprovider': state.embedding_provider,
1909
- 'igmodel': state.image_gen_model,
1910
- 'igprovider': state.image_gen_provider,
1911
- 'vgmodel': state.video_gen_model,
1912
- 'vgprovider': state.video_gen_provider,
1913
- 'vmodel': state.vision_model,
1914
- 'vprovider': state.vision_provider,
1915
- 'rmodel': state.reasoning_model,
1916
- 'rprovider': state.reasoning_provider,
2034
+ 'npc': state.npc, 'sprovider': state.search_provider, 'emodel': state.embedding_model,
2035
+ 'eprovider': state.embedding_provider, 'igmodel': state.image_gen_model, 'igprovider': state.image_gen_provider,
2036
+ 'vmodel': state.vision_model, 'vprovider': state.vision_provider, 'rmodel': state.reasoning_model,
2037
+ 'rprovider': state.reasoning_provider, 'state': state
1917
2038
  }
1918
-
1919
- if len(normalized_flags) > 0:
1920
- kwarg_part = 'with kwargs: \n -' + '\n -'.join(f'{key}={item}' for key, item in normalized_flags.items())
1921
- else:
1922
- kwarg_part = ''
1923
-
1924
- render_markdown(f'- Calling {command_name} handler {kwarg_part} ')
1925
-
1926
- if 'model' in normalized_flags and 'provider' not in normalized_flags:
1927
- inferred_provider = lookup_provider(normalized_flags['model'])
1928
- if inferred_provider:
1929
- handler_kwargs['provider'] = inferred_provider
1930
- print(colored(f"Info: Inferred provider '{inferred_provider}' for model '{normalized_flags['model']}'.", "cyan"))
1931
-
1932
- if 'provider' in normalized_flags and 'model' not in normalized_flags:
1933
- current_provider = lookup_provider(handler_kwargs['model'])
1934
- if current_provider != normalized_flags['provider']:
1935
- prov = normalized_flags['provider']
1936
- print(f'Please specify a model for the provider: {prov}')
1937
-
1938
- handler_kwargs.update(normalized_flags)
1939
-
1940
2039
  try:
1941
2040
  result = handler(command=command, **handler_kwargs)
1942
-
1943
- if isinstance(result, dict):
2041
+ if isinstance(result, dict):
1944
2042
  state.messages = result.get("messages", state.messages)
1945
- return state, result
1946
- elif isinstance(result, str):
1947
- return state, {"output": result, "messages": state.messages}
1948
- else:
1949
- return state, {"output": str(result), "messages": state.messages}
1950
-
2043
+ return state, result
1951
2044
  except Exception as e:
1952
2045
  import traceback
1953
- print(f"Error executing slash command '{command_name}':", file=sys.stderr)
1954
2046
  traceback.print_exc()
1955
2047
  return state, {"output": colored(f"Error executing slash command '{command_name}': {e}", "red"), "messages": state.messages}
1956
-
1957
- active_npc = state.npc if isinstance(state.npc, NPC) else None
1958
- jinx_to_execute = None
1959
- executor = None
1960
-
1961
- if active_npc and hasattr(active_npc, 'jinxs_dict') and command_name in active_npc.jinxs_dict:
1962
- jinx_to_execute = active_npc.jinxs_dict[command_name]
1963
- executor = active_npc
1964
- elif state.team and hasattr(state.team, 'jinxs_dict') and command_name in state.team.jinxs_dict:
1965
- jinx_to_execute = state.team.jinxs_dict[command_name]
1966
- executor = state.team
1967
- if jinx_to_execute:
1968
- args = all_command_parts[1:]
1969
- try:
1970
- input_values = {}
1971
- if hasattr(jinx_to_execute, 'inputs') and jinx_to_execute.inputs:
1972
- for i, input_name in enumerate(jinx_to_execute.inputs):
1973
- if i < len(args):
1974
- input_values[input_name] = args[i]
1975
-
1976
- if isinstance(executor, NPC):
1977
- jinx_output = jinx_to_execute.execute(
1978
- input_values=input_values,
1979
- jinxs_dict=executor.jinxs_dict if hasattr(executor, 'jinxs_dict') else {},
1980
- npc=executor,
1981
- messages=state.messages
1982
- )
1983
- else:
1984
- jinx_output = jinx_to_execute.execute(
1985
- input_values=input_values,
1986
- jinxs_dict=executor.jinxs_dict if hasattr(executor, 'jinxs_dict') else {},
1987
- npc=active_npc or state.npc,
1988
- messages=state.messages
1989
- )
1990
- if isinstance(jinx_output, dict) and 'messages' in jinx_output:
1991
- state.messages = jinx_output['messages']
1992
- return state, jinx_output
1993
- elif isinstance(jinx_output, dict):
1994
- return state, jinx_output
1995
- else:
1996
- return state, {"output": str(jinx_output), "messages": state.messages}
1997
-
1998
- except Exception as e:
1999
- import traceback
2000
- print(f"Error executing jinx '{command_name}':", file=sys.stderr)
2001
- traceback.print_exc()
2002
- return state, {"output": colored(f"Error executing jinx '{command_name}': {e}", "red"), "messages": state.messages}
2003
2048
 
2049
+ # Fallback for switching NPC by name
2004
2050
  if state.team and command_name in state.team.npcs:
2005
- new_npc = state.team.npcs[command_name]
2006
- state.npc = new_npc
2007
- return state, {"output": f"Switched to NPC: {new_npc.name}", "messages": state.messages}
2051
+ state.npc = state.team.npcs[command_name]
2052
+ return state, {"output": f"Switched to NPC: {state.npc.name}", "messages": state.messages}
2008
2053
 
2009
- return state, {"output": colored(f"Unknown slash command, jinx, or NPC: {command_name}", "red"), "messages": state.messages}
2054
+ return state, {"output": colored(f"Unknown slash command or NPC: {command_name}", "red"), "messages": state.messages}
2010
2055
 
2011
2056
 
2012
2057
  def process_pipeline_command(
@@ -2017,15 +2062,14 @@ def process_pipeline_command(
2017
2062
  review = False,
2018
2063
  router = None,
2019
2064
  ) -> Tuple[ShellState, Any]:
2020
- '''
2021
- Processing command
2022
- '''
2023
2065
 
2024
2066
  if not cmd_segment:
2025
2067
  return state, stdin_input
2026
2068
 
2027
2069
  available_models_all = get_locally_available_models(state.current_path)
2028
- available_models_all_list = [item for key, item in available_models_all.items()]
2070
+ available_models_all_list = [
2071
+ item for key, item in available_models_all.items()
2072
+ ]
2029
2073
 
2030
2074
  model_override, provider_override, cmd_cleaned = get_model_and_provider(
2031
2075
  cmd_segment, available_models_all_list
@@ -2034,18 +2078,33 @@ def process_pipeline_command(
2034
2078
  if not cmd_to_process:
2035
2079
  return state, stdin_input
2036
2080
 
2037
- npc_model = state.npc.model if isinstance(state.npc, NPC) and state.npc.model else None
2038
- npc_provider = state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else None
2081
+ npc_model = (
2082
+ state.npc.model
2083
+ if isinstance(state.npc, NPC) and state.npc.model
2084
+ else None
2085
+ )
2086
+ npc_provider = (
2087
+ state.npc.provider
2088
+ if isinstance(state.npc, NPC) and state.npc.provider
2089
+ else None
2090
+ )
2039
2091
 
2040
2092
  exec_model = model_override or npc_model or state.chat_model
2041
2093
  exec_provider = provider_override or npc_provider or state.chat_provider
2042
2094
 
2043
2095
  if cmd_to_process.startswith("/"):
2044
- return execute_slash_command(cmd_to_process,
2045
- stdin_input,
2046
- state,
2047
- stream_final,
2048
- router)
2096
+ with SpinnerContext(
2097
+ f"Routing to {cmd_to_process.split()[0]}",
2098
+ style="arrow"
2099
+ ):
2100
+ result = execute_slash_command(
2101
+ cmd_to_process,
2102
+ stdin_input,
2103
+ state,
2104
+ stream_final,
2105
+ router
2106
+ )
2107
+ return result
2049
2108
 
2050
2109
  cmd_parts = parse_command_safely(cmd_to_process)
2051
2110
  if not cmd_parts:
@@ -2058,6 +2117,7 @@ def process_pipeline_command(
2058
2117
 
2059
2118
  if command_name in interactive_commands:
2060
2119
  return handle_interactive_command(cmd_parts, state)
2120
+
2061
2121
  if command_name in TERMINAL_EDITORS:
2062
2122
  print(f"Starting interactive editor: {command_name}...")
2063
2123
  full_command_str = " ".join(cmd_parts)
@@ -2065,46 +2125,107 @@ def process_pipeline_command(
2065
2125
  return state, output
2066
2126
 
2067
2127
  if validate_bash_command(cmd_parts):
2068
- success, result = handle_bash_command(cmd_parts, cmd_to_process, stdin_input, state)
2128
+ with SpinnerContext(f"Executing {command_name}", style="line"):
2129
+ success, result = handle_bash_command(
2130
+ cmd_parts,
2131
+ cmd_to_process,
2132
+ stdin_input,
2133
+ state
2134
+ )
2135
+
2069
2136
  if success:
2070
2137
  return state, result
2071
2138
  else:
2072
- print(colored(f"Bash command failed: {result}. Asking LLM for a fix...", "yellow"), file=sys.stderr)
2073
- fixer_prompt = f"The command '{cmd_to_process}' failed with the error: '{result}'. Provide the correct command."
2074
- response = execute_llm_command(
2075
- fixer_prompt,
2076
- model=exec_model,
2077
- provider=exec_provider,
2078
- npc=state.npc,
2079
- stream=stream_final,
2080
- messages=state.messages
2139
+ print(
2140
+ colored(
2141
+ f"Command failed. Consulting {exec_model}...",
2142
+ "yellow"
2143
+ ),
2144
+ file=sys.stderr
2145
+ )
2146
+ fixer_prompt = (
2147
+ f"The command '{cmd_to_process}' failed with error: "
2148
+ f"'{result}'. Provide the correct command."
2081
2149
  )
2150
+
2151
+ with SpinnerContext(
2152
+ f"{exec_model} analyzing error",
2153
+ style="brain"
2154
+ ):
2155
+ response = execute_llm_command(
2156
+ fixer_prompt,
2157
+ model=exec_model,
2158
+ provider=exec_provider,
2159
+ npc=state.npc,
2160
+ stream=stream_final,
2161
+ messages=state.messages
2162
+ )
2163
+
2082
2164
  state.messages = response['messages']
2083
2165
  return state, response['response']
2084
2166
  else:
2085
- full_llm_cmd = f"{cmd_to_process} {stdin_input}" if stdin_input else cmd_to_process
2167
+ full_llm_cmd = (
2168
+ f"{cmd_to_process} {stdin_input}"
2169
+ if stdin_input
2170
+ else cmd_to_process
2171
+ )
2086
2172
  path_cmd = 'The current working directory is: ' + state.current_path
2087
- ls_files = 'Files in the current directory (full paths):\n' + "\n".join([os.path.join(state.current_path, f) for f in os.listdir(state.current_path)]) if os.path.exists(state.current_path) else 'No files found in the current directory.'
2088
- platform_info = f"Platform: {platform.system()} {platform.release()} ({platform.machine()})"
2173
+ ls_files = (
2174
+ 'Files in the current directory (full paths):\n' +
2175
+ "\n".join([
2176
+ os.path.join(state.current_path, f)
2177
+ for f in os.listdir(state.current_path)
2178
+ ])
2179
+ if os.path.exists(state.current_path)
2180
+ else 'No files found in the current directory.'
2181
+ )
2182
+ platform_info = (
2183
+ f"Platform: {platform.system()} {platform.release()} "
2184
+ f"({platform.machine()})"
2185
+ )
2089
2186
  info = path_cmd + '\n' + ls_files + '\n' + platform_info + '\n'
2090
2187
  state.messages.append({'role':'user', 'content':full_llm_cmd})
2091
2188
 
2092
-
2093
- llm_result = check_llm_command(
2094
- full_llm_cmd,
2095
- model=exec_model,
2096
- provider=exec_provider,
2097
- api_url=state.api_url,
2098
- api_key=state.api_key,
2099
- npc=state.npc,
2100
- team=state.team,
2101
- messages=state.messages,
2102
- images=state.attachments,
2103
- stream=stream_final,
2104
- context=info,
2189
+ npc_name = (
2190
+ state.npc.name
2191
+ if isinstance(state.npc, NPC)
2192
+ else "Assistant"
2105
2193
  )
2106
-
2107
2194
 
2195
+ with SpinnerContext(
2196
+ f"{npc_name} processing with {exec_model}",
2197
+ style="dots_pulse"
2198
+ ):
2199
+ # Build extra_globals for jinx execution
2200
+ application_globals_for_jinx = {
2201
+ "CommandHistory": CommandHistory,
2202
+ "load_kg_from_db": load_kg_from_db,
2203
+ "execute_rag_command": execute_rag_command,
2204
+ "execute_brainblast_command": execute_brainblast_command,
2205
+ "load_file_contents": load_file_contents,
2206
+ "search_web": search_web,
2207
+ "get_relevant_memories": get_relevant_memories,
2208
+ "search_kg_facts": search_kg_facts,
2209
+ 'state': state
2210
+ }
2211
+ current_module = sys.modules[__name__]
2212
+ for name, func in inspect.getmembers(current_module, inspect.isfunction):
2213
+ application_globals_for_jinx[name] = func
2214
+
2215
+ llm_result = check_llm_command(
2216
+ full_llm_cmd,
2217
+ model=exec_model,
2218
+ provider=exec_provider,
2219
+ api_url=state.api_url,
2220
+ api_key=state.api_key,
2221
+ npc=state.npc,
2222
+ team=state.team,
2223
+ messages=state.messages,
2224
+ images=state.attachments,
2225
+ stream=stream_final,
2226
+ context=info,
2227
+ extra_globals=application_globals_for_jinx # NOW PASS IT
2228
+ )
2108
2229
  if not review:
2109
2230
  if isinstance(llm_result, dict):
2110
2231
  state.messages = llm_result.get("messages", state.messages)
@@ -2112,7 +2233,6 @@ def process_pipeline_command(
2112
2233
  return state, output
2113
2234
  else:
2114
2235
  return state, llm_result
2115
-
2116
2236
  else:
2117
2237
  return review_and_iterate_command(
2118
2238
  original_command=full_llm_cmd,
@@ -2123,6 +2243,8 @@ def process_pipeline_command(
2123
2243
  stream_final=stream_final,
2124
2244
  info=info
2125
2245
  )
2246
+
2247
+
2126
2248
  def review_and_iterate_command(
2127
2249
  original_command: str,
2128
2250
  initial_result: Any,
@@ -2181,6 +2303,71 @@ def check_mode_switch(command:str , state: ShellState):
2181
2303
  return True, state
2182
2304
  return False, state
2183
2305
 
2306
+ import sys
2307
+ import time
2308
+ import threading
2309
+ from itertools import cycle
2310
+
2311
+ class SpinnerContext:
2312
+ def __init__(self, message="Processing", style="dots"):
2313
+ self.message = message
2314
+ self.spinning = False
2315
+ self.thread = None
2316
+
2317
+ styles = {
2318
+ "dots": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
2319
+ "line": ["-", "\\", "|", "/"],
2320
+ "arrow": ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"],
2321
+ "box": ["◰", "◳", "◲", "◱"],
2322
+ "dots_pulse": ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
2323
+ "brain": ["🧠", "💭", "🤔", "💡"],
2324
+ }
2325
+ self.frames = cycle(styles.get(style, styles["dots"]))
2326
+
2327
+ def _spin(self):
2328
+ while self.spinning:
2329
+ sys.stdout.write(
2330
+ f"\r{colored(next(self.frames), 'cyan')} "
2331
+ f"{colored(self.message, 'yellow')}..."
2332
+ )
2333
+ sys.stdout.flush()
2334
+ time.sleep(0.1)
2335
+
2336
+ def __enter__(self):
2337
+ self.spinning = True
2338
+ self.thread = threading.Thread(target=self._spin)
2339
+ self.thread.start()
2340
+ return self
2341
+
2342
+ def __exit__(self, exc_type, exc_val, exc_tb):
2343
+ self.spinning = False
2344
+ if self.thread:
2345
+ self.thread.join()
2346
+ sys.stdout.write("\r" + " " * 80 + "\r")
2347
+ sys.stdout.flush()
2348
+
2349
+ def show_thinking_animation(message="Thinking", duration=None):
2350
+ frames = ["🤔", "💭", "🧠", "💡", "✨"]
2351
+ colors = ["cyan", "blue", "magenta", "yellow", "green"]
2352
+
2353
+ start = time.time()
2354
+ i = 0
2355
+ while duration is None or (time.time() - start) < duration:
2356
+ frame = frames[i % len(frames)]
2357
+ color = colors[i % len(colors)]
2358
+ sys.stdout.write(
2359
+ f"\r{colored(frame, color)} "
2360
+ f"{colored(message, 'yellow')}..."
2361
+ )
2362
+ sys.stdout.flush()
2363
+ time.sleep(0.3)
2364
+ i += 1
2365
+ if duration and (time.time() - start) >= duration:
2366
+ break
2367
+
2368
+ sys.stdout.write("\r" + " " * 80 + "\r")
2369
+ sys.stdout.flush()
2370
+
2184
2371
  def execute_command(
2185
2372
  command: str,
2186
2373
  state: ShellState,
@@ -2194,29 +2381,51 @@ def execute_command(
2194
2381
 
2195
2382
  mode_change, state = check_mode_switch(command, state)
2196
2383
  if mode_change:
2384
+ print(colored(f"⚡ Switched to {state.current_mode} mode", "green"))
2197
2385
  return state, 'Mode changed.'
2198
2386
 
2199
- npc_name = state.npc.name if isinstance(state.npc, NPC) else "__none__"
2387
+ npc_name = (
2388
+ state.npc.name
2389
+ if isinstance(state.npc, NPC)
2390
+ else "__none__"
2391
+ )
2200
2392
  team_name = state.team.name if state.team else "__none__"
2201
2393
 
2202
-
2203
2394
  original_command_for_embedding = command
2204
2395
  commands = split_by_pipes(command)
2205
2396
 
2206
2397
  stdin_for_next = None
2207
2398
  final_output = None
2208
2399
  current_state = state
2209
- npc_model = state.npc.model if isinstance(state.npc, NPC) and state.npc.model else None
2210
- npc_provider = state.npc.provider if isinstance(state.npc, NPC) and state.npc.provider else None
2400
+ npc_model = (
2401
+ state.npc.model
2402
+ if isinstance(state.npc, NPC) and state.npc.model
2403
+ else None
2404
+ )
2405
+ npc_provider = (
2406
+ state.npc.provider
2407
+ if isinstance(state.npc, NPC) and state.npc.provider
2408
+ else None
2409
+ )
2211
2410
  active_model = npc_model or state.chat_model
2212
2411
  active_provider = npc_provider or state.chat_provider
2412
+
2213
2413
  if state.current_mode == 'agent':
2214
-
2215
-
2414
+ total_stages = len(commands)
2415
+
2216
2416
  for i, cmd_segment in enumerate(commands):
2217
- render_markdown(f'- Executing command {i+1}/{len(commands)}')
2417
+ stage_num = i + 1
2418
+ stage_emoji = ["🎯", "⚙️", "🔧", "✨", "🚀"][i % 5]
2419
+
2420
+ print(colored(
2421
+ f"\n{stage_emoji} Pipeline Stage {stage_num}/{total_stages}",
2422
+ "cyan",
2423
+ attrs=["bold"]
2424
+ ))
2425
+
2218
2426
  is_last_command = (i == len(commands) - 1)
2219
2427
  stream_this_segment = state.stream_output and not is_last_command
2428
+
2220
2429
  try:
2221
2430
  current_state, output = process_pipeline_command(
2222
2431
  cmd_segment.strip(),
@@ -2224,19 +2433,26 @@ def execute_command(
2224
2433
  current_state,
2225
2434
  stream_final=stream_this_segment,
2226
2435
  review=review,
2227
- router= router
2436
+ router=router
2228
2437
  )
2438
+
2229
2439
  if is_last_command:
2440
+ print(colored("✅ Pipeline complete", "green"))
2230
2441
  return current_state, output
2442
+
2231
2443
  if isinstance(output, str):
2232
2444
  stdin_for_next = output
2233
2445
  elif not isinstance(output, str):
2234
2446
  try:
2235
2447
  if stream_this_segment:
2236
- full_stream_output = print_and_process_stream_with_markdown(output,
2237
- state.npc.model,
2238
- state.npc.provider,
2239
- show=True)
2448
+ full_stream_output = (
2449
+ print_and_process_stream_with_markdown(
2450
+ output,
2451
+ state.npc.model,
2452
+ state.npc.provider,
2453
+ show=True
2454
+ )
2455
+ )
2240
2456
  stdin_for_next = full_stream_output
2241
2457
  if is_last_command:
2242
2458
  final_output = full_stream_output
@@ -2245,24 +2461,40 @@ def execute_command(
2245
2461
  try:
2246
2462
  stdin_for_next = str(output)
2247
2463
  except Exception:
2248
- print(f"Warning: Cannot convert output to string for piping: {type(output)}", file=sys.stderr)
2464
+ print(
2465
+ f"Warning: Cannot convert output to "
2466
+ f"string for piping: {type(output)}",
2467
+ file=sys.stderr
2468
+ )
2249
2469
  stdin_for_next = None
2250
2470
  else:
2251
2471
  stdin_for_next = None
2472
+
2473
+ print(colored(
2474
+ f" → Passing to stage {stage_num + 1}",
2475
+ "blue"
2476
+ ))
2477
+
2252
2478
  except Exception as pipeline_error:
2253
2479
  import traceback
2254
2480
  traceback.print_exc()
2255
- error_msg = colored(f"Error in pipeline stage {i+1} ('{cmd_segment[:50]}...'): {pipeline_error}", "red")
2481
+ error_msg = colored(
2482
+ f"❌ Error in stage {stage_num} "
2483
+ f"('{cmd_segment[:50]}...'): {pipeline_error}",
2484
+ "red"
2485
+ )
2256
2486
  return current_state, error_msg
2257
2487
 
2258
2488
  if final_output is not None and isinstance(final_output,str):
2259
- store_command_embeddings(original_command_for_embedding, final_output, current_state)
2489
+ store_command_embeddings(
2490
+ original_command_for_embedding,
2491
+ final_output,
2492
+ current_state
2493
+ )
2260
2494
 
2261
2495
  return current_state, final_output
2262
2496
 
2263
-
2264
2497
  elif state.current_mode == 'chat':
2265
-
2266
2498
  cmd_parts = parse_command_safely(command)
2267
2499
  is_probably_bash = (
2268
2500
  cmd_parts
@@ -2273,6 +2505,7 @@ def execute_command(
2273
2505
  or command.strip().startswith("/")
2274
2506
  )
2275
2507
  )
2508
+
2276
2509
  if is_probably_bash:
2277
2510
  try:
2278
2511
  command_name = cmd_parts[0]
@@ -2282,36 +2515,55 @@ def execute_command(
2282
2515
  return handle_cd_command(cmd_parts, state)
2283
2516
  else:
2284
2517
  try:
2285
- bash_state, bash_output = handle_bash_command(cmd_parts, command, None, state)
2518
+ bash_state, bash_output = handle_bash_command(
2519
+ cmd_parts,
2520
+ command,
2521
+ None,
2522
+ state
2523
+ )
2286
2524
  return state, bash_output
2287
2525
  except Exception as bash_err:
2288
- return state, colored(f"Bash execution failed: {bash_err}", "red")
2526
+ return state, colored(
2527
+ f"Bash execution failed: {bash_err}",
2528
+ "red"
2529
+ )
2289
2530
  except Exception:
2290
2531
  pass
2291
2532
 
2292
-
2293
- response = get_llm_response(
2294
- command,
2295
- model=active_model,
2296
- provider=active_provider,
2297
- npc=state.npc,
2298
- stream=state.stream_output,
2299
- messages=state.messages
2300
- )
2533
+ with SpinnerContext(
2534
+ f"Chatting with {active_model}",
2535
+ style="brain"
2536
+ ):
2537
+ response = get_llm_response(
2538
+ command,
2539
+ model=active_model,
2540
+ provider=active_provider,
2541
+ npc=state.npc,
2542
+ stream=state.stream_output,
2543
+ messages=state.messages
2544
+ )
2545
+
2301
2546
  state.messages = response['messages']
2302
2547
  return state, response['response']
2303
2548
 
2304
2549
  elif state.current_mode == 'cmd':
2305
-
2306
- response = execute_llm_command(command,
2307
- model=active_model,
2308
- provider=active_provider,
2309
- npc = state.npc,
2310
- stream = state.stream_output,
2311
- messages = state.messages)
2550
+ with SpinnerContext(
2551
+ f"Executing with {active_model}",
2552
+ style="dots_pulse"
2553
+ ):
2554
+ response = execute_llm_command(
2555
+ command,
2556
+ model=active_model,
2557
+ provider=active_provider,
2558
+ npc=state.npc,
2559
+ stream=state.stream_output,
2560
+ messages=state.messages
2561
+ )
2562
+
2312
2563
  state.messages = response['messages']
2313
2564
  return state, response['response']
2314
2565
 
2566
+
2315
2567
  def setup_shell() -> Tuple[CommandHistory, Team, Optional[NPC]]:
2316
2568
  setup_npcsh_config()
2317
2569
 
@@ -2445,139 +2697,6 @@ def initialize_router_with_jinxs(team, router):
2445
2697
 
2446
2698
  return router
2447
2699
 
2448
- from npcpy.memory.memory_processor import memory_approval_ui
2449
- from npcpy.ft.memory_trainer import MemoryTrainer
2450
- from npcpy.llm_funcs import get_facts
2451
-
2452
- def get_relevant_memories(
2453
- command_history: CommandHistory,
2454
- npc_name: str,
2455
- team_name: str,
2456
- path: str,
2457
- query: Optional[str] = None,
2458
- max_memories: int = 10,
2459
- state: Optional[ShellState] = None
2460
- ) -> List[Dict]:
2461
-
2462
- engine = command_history.engine
2463
-
2464
- all_memories = command_history.get_memories_for_scope(
2465
- npc=npc_name,
2466
- team=team_name,
2467
- directory_path=path,
2468
- status='human-approved'
2469
- )
2470
-
2471
- if not all_memories:
2472
- return []
2473
-
2474
- if len(all_memories) <= max_memories and not query:
2475
- return all_memories
2476
-
2477
- if query:
2478
- query_lower = query.lower()
2479
- keyword_matches = [
2480
- m for m in all_memories
2481
- if query_lower in (m.get('final_memory') or m.get('initial_memory') or '').lower()
2482
- ]
2483
-
2484
- if keyword_matches:
2485
- return keyword_matches[:max_memories]
2486
-
2487
- if state and state.embedding_model and state.embedding_provider:
2488
- try:
2489
- from npcpy.gen.embeddings import get_embeddings
2490
-
2491
- search_text = query if query else "recent context"
2492
- query_embedding = get_embeddings(
2493
- [search_text],
2494
- state.embedding_model,
2495
- state.embedding_provider
2496
- )[0]
2497
-
2498
- memory_texts = [
2499
- m.get('final_memory', '') for m in all_memories
2500
- ]
2501
- memory_embeddings = get_embeddings(
2502
- memory_texts,
2503
- state.embedding_model,
2504
- state.embedding_provider
2505
- )
2506
-
2507
- import numpy as np
2508
- similarities = []
2509
- for mem_emb in memory_embeddings:
2510
- similarity = np.dot(query_embedding, mem_emb) / (
2511
- np.linalg.norm(query_embedding) *
2512
- np.linalg.norm(mem_emb)
2513
- )
2514
- similarities.append(similarity)
2515
-
2516
- sorted_indices = np.argsort(similarities)[::-1]
2517
- return [all_memories[i] for i in sorted_indices[:max_memories]]
2518
-
2519
- except Exception as e:
2520
- print(colored(
2521
- f"RAG search failed, using recent: {e}",
2522
- "yellow"
2523
- ))
2524
-
2525
- return all_memories[-max_memories:]
2526
-
2527
-
2528
- def search_kg_facts(
2529
- self,
2530
- npc: str,
2531
- team: str,
2532
- directory_path: str,
2533
- query: str
2534
- ) -> List[Dict]:
2535
-
2536
- kg = load_kg_from_db(
2537
- self.engine,
2538
- team,
2539
- npc,
2540
- directory_path
2541
- )
2542
-
2543
- if not kg or 'facts' not in kg:
2544
- return []
2545
-
2546
- query_lower = query.lower()
2547
- matching_facts = []
2548
-
2549
- for fact in kg['facts']:
2550
- statement = fact.get('statement', '').lower()
2551
- if query_lower in statement:
2552
- matching_facts.append(fact)
2553
-
2554
- return matching_facts
2555
-
2556
- def format_memory_context(memory_examples):
2557
- if not memory_examples:
2558
- return ""
2559
-
2560
- context_parts = []
2561
-
2562
- approved_examples = memory_examples.get("approved", [])
2563
- rejected_examples = memory_examples.get("rejected", [])
2564
-
2565
- if approved_examples:
2566
- context_parts.append("EXAMPLES OF GOOD MEMORIES:")
2567
- for ex in approved_examples[:5]:
2568
- final = ex.get("final_memory") or ex.get("initial_memory")
2569
- context_parts.append(f"- {final}")
2570
-
2571
- if rejected_examples:
2572
- context_parts.append("\nEXAMPLES OF POOR MEMORIES TO AVOID:")
2573
- for ex in rejected_examples[:3]:
2574
- context_parts.append(f"- {ex.get('initial_memory')}")
2575
-
2576
- if context_parts:
2577
- context_parts.append("\nLearn from these examples to generate similar high-quality memories.")
2578
- return "\n".join(context_parts)
2579
-
2580
- return ""
2581
2700
 
2582
2701
  def process_memory_approvals(command_history, memory_queue):
2583
2702
  pending_memories = memory_queue.get_approval_batch(max_items=5)