npcpy 1.3.19__py3-none-any.whl → 1.3.20__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/data/web.py +113 -8
- npcpy/llm_funcs.py +1 -3
- npcpy/memory/command_history.py +85 -9
- npcpy/memory/knowledge_graph.py +554 -33
- npcpy/memory/memory_processor.py +269 -53
- npcpy/npc_compiler.py +6 -0
- npcpy/serve.py +25 -1
- {npcpy-1.3.19.dist-info → npcpy-1.3.20.dist-info}/METADATA +3 -1
- {npcpy-1.3.19.dist-info → npcpy-1.3.20.dist-info}/RECORD +12 -12
- {npcpy-1.3.19.dist-info → npcpy-1.3.20.dist-info}/WHEEL +1 -1
- {npcpy-1.3.19.dist-info → npcpy-1.3.20.dist-info}/licenses/LICENSE +0 -0
- {npcpy-1.3.19.dist-info → npcpy-1.3.20.dist-info}/top_level.txt +0 -0
npcpy/data/web.py
CHANGED
|
@@ -24,22 +24,118 @@ except:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def search_exa(query:str,
|
|
28
|
-
api_key:str = None,
|
|
27
|
+
def search_exa(query:str,
|
|
28
|
+
api_key:str = None,
|
|
29
29
|
top_k = 5,
|
|
30
30
|
**kwargs):
|
|
31
31
|
from exa_py import Exa
|
|
32
32
|
if api_key is None:
|
|
33
|
-
api_key = os.environ.get('EXA_API_KEY')
|
|
33
|
+
api_key = os.environ.get('EXA_API_KEY')
|
|
34
34
|
exa = Exa(api_key)
|
|
35
35
|
|
|
36
36
|
results = exa.search_and_contents(
|
|
37
|
-
query,
|
|
38
|
-
text=True
|
|
37
|
+
query,
|
|
38
|
+
text=True
|
|
39
39
|
)
|
|
40
40
|
return results.results[0:top_k]
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def search_searxng(query: str, num_results: int = 5, instance_url: str = None):
|
|
44
|
+
"""Search using SearXNG public instances."""
|
|
45
|
+
instances = [instance_url] if instance_url else [
|
|
46
|
+
os.environ.get('SEARXNG_URL'),
|
|
47
|
+
'https://search.sapti.me',
|
|
48
|
+
'https://searx.work',
|
|
49
|
+
'https://search.ononoki.org',
|
|
50
|
+
]
|
|
51
|
+
instances = [i for i in instances if i]
|
|
52
|
+
|
|
53
|
+
for instance in instances:
|
|
54
|
+
try:
|
|
55
|
+
response = requests.get(
|
|
56
|
+
f"{instance}/search",
|
|
57
|
+
params={'q': query, 'format': 'json', 'categories': 'general'},
|
|
58
|
+
headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0'},
|
|
59
|
+
timeout=10
|
|
60
|
+
)
|
|
61
|
+
if response.status_code == 200:
|
|
62
|
+
data = response.json()
|
|
63
|
+
results = []
|
|
64
|
+
for r in data.get('results', [])[:num_results]:
|
|
65
|
+
results.append({
|
|
66
|
+
'title': r.get('title', ''),
|
|
67
|
+
'link': r.get('url', ''),
|
|
68
|
+
'content': r.get('content', '')
|
|
69
|
+
})
|
|
70
|
+
if results:
|
|
71
|
+
return results
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print(f"SearXNG {instance} failed: {e}")
|
|
74
|
+
continue
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def search_startpage(query: str, num_results: int = 5):
|
|
79
|
+
"""Search using Startpage (scraping)."""
|
|
80
|
+
try:
|
|
81
|
+
response = requests.post(
|
|
82
|
+
'https://www.startpage.com/sp/search',
|
|
83
|
+
data={'query': query, 'cat': 'web'},
|
|
84
|
+
headers={
|
|
85
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
86
|
+
'Accept': 'text/html',
|
|
87
|
+
},
|
|
88
|
+
timeout=10
|
|
89
|
+
)
|
|
90
|
+
if response.status_code == 200:
|
|
91
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
92
|
+
results = []
|
|
93
|
+
for item in soup.select('.result')[:num_results]:
|
|
94
|
+
title_el = item.select_one('h2') or item.select_one('a')
|
|
95
|
+
link_el = item.select_one('a[href^="http"]')
|
|
96
|
+
desc_el = item.select_one('p') or item.select_one('.description')
|
|
97
|
+
if link_el:
|
|
98
|
+
results.append({
|
|
99
|
+
'title': title_el.get_text(strip=True) if title_el else '',
|
|
100
|
+
'link': link_el.get('href', ''),
|
|
101
|
+
'content': desc_el.get_text(strip=True) if desc_el else ''
|
|
102
|
+
})
|
|
103
|
+
return results
|
|
104
|
+
except Exception as e:
|
|
105
|
+
print(f"Startpage search failed: {e}")
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def search_brave(query: str, num_results: int = 5, api_key: str = None):
|
|
110
|
+
"""Search using Brave Search API."""
|
|
111
|
+
if api_key is None:
|
|
112
|
+
api_key = os.environ.get('BRAVE_API_KEY')
|
|
113
|
+
if not api_key:
|
|
114
|
+
print("Brave API key not set")
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = requests.get(
|
|
119
|
+
'https://api.search.brave.com/res/v1/web/search',
|
|
120
|
+
params={'q': query, 'count': num_results},
|
|
121
|
+
headers={'X-Subscription-Token': api_key, 'Accept': 'application/json'},
|
|
122
|
+
timeout=10
|
|
123
|
+
)
|
|
124
|
+
if response.status_code == 200:
|
|
125
|
+
data = response.json()
|
|
126
|
+
results = []
|
|
127
|
+
for r in data.get('web', {}).get('results', [])[:num_results]:
|
|
128
|
+
results.append({
|
|
129
|
+
'title': r.get('title', ''),
|
|
130
|
+
'link': r.get('url', ''),
|
|
131
|
+
'content': r.get('description', '')
|
|
132
|
+
})
|
|
133
|
+
return results
|
|
134
|
+
except Exception as e:
|
|
135
|
+
print(f"Brave search failed: {e}")
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
|
|
43
139
|
def search_perplexity(
|
|
44
140
|
query: str,
|
|
45
141
|
api_key: str = None,
|
|
@@ -134,10 +230,19 @@ def search_web(
|
|
|
134
230
|
print("DuckDuckGo search failed: ", e)
|
|
135
231
|
urls = []
|
|
136
232
|
results = []
|
|
137
|
-
elif provider =='exa':
|
|
138
|
-
return search_exa(query, api_key=api_key, )
|
|
233
|
+
elif provider == 'exa':
|
|
234
|
+
return search_exa(query, api_key=api_key, top_k=num_results)
|
|
235
|
+
|
|
236
|
+
elif provider == 'searxng':
|
|
237
|
+
results = search_searxng(query, num_results=num_results)
|
|
238
|
+
|
|
239
|
+
elif provider == 'startpage':
|
|
240
|
+
results = search_startpage(query, num_results=num_results)
|
|
241
|
+
|
|
242
|
+
elif provider == 'brave':
|
|
243
|
+
results = search_brave(query, num_results=num_results, api_key=api_key)
|
|
139
244
|
|
|
140
|
-
elif provider =='google':
|
|
245
|
+
elif provider == 'google':
|
|
141
246
|
urls = list(search(query, num_results=num_results))
|
|
142
247
|
|
|
143
248
|
|
npcpy/llm_funcs.py
CHANGED
|
@@ -499,12 +499,10 @@ def handle_request_input(
|
|
|
499
499
|
|
|
500
500
|
|
|
501
501
|
def _get_jinxs(npc, team):
|
|
502
|
-
"""Get available jinxs from npc
|
|
502
|
+
"""Get available jinxs from npc (already filtered by jinxs_spec)."""
|
|
503
503
|
jinxs = {}
|
|
504
504
|
if npc and hasattr(npc, 'jinxs_dict'):
|
|
505
505
|
jinxs.update(npc.jinxs_dict)
|
|
506
|
-
if team and hasattr(team, 'jinxs_dict'):
|
|
507
|
-
jinxs.update(team.jinxs_dict)
|
|
508
506
|
return jinxs
|
|
509
507
|
|
|
510
508
|
|
npcpy/memory/command_history.py
CHANGED
|
@@ -613,7 +613,11 @@ class CommandHistory:
|
|
|
613
613
|
Column('tool_results', Text), # JSON array of tool call results
|
|
614
614
|
Column('parent_message_id', String(50)), # Links assistant response to parent user message for broadcast grouping
|
|
615
615
|
Column('device_id', String(255)), # UUID of the device that created this message
|
|
616
|
-
Column('device_name', String(255)) # Human-readable device name
|
|
616
|
+
Column('device_name', String(255)), # Human-readable device name
|
|
617
|
+
Column('params', Text), # JSON object for LLM params: temperature, top_p, top_k, max_tokens, etc.
|
|
618
|
+
Column('input_tokens', Integer), # Input/prompt token count
|
|
619
|
+
Column('output_tokens', Integer), # Output/completion token count
|
|
620
|
+
Column('cost', String(50)) # Cost in USD (stored as string for precision)
|
|
617
621
|
)
|
|
618
622
|
|
|
619
623
|
Table('message_attachments', metadata,
|
|
@@ -679,13 +683,29 @@ class CommandHistory:
|
|
|
679
683
|
metadata.create_all(self.engine, checkfirst=True)
|
|
680
684
|
init_kg_schema(self.engine)
|
|
681
685
|
|
|
682
|
-
# Add
|
|
686
|
+
# Add columns if they don't exist (migrations for existing databases)
|
|
683
687
|
if 'sqlite' in str(self.engine.url):
|
|
684
688
|
with self.engine.begin() as conn:
|
|
685
689
|
try:
|
|
686
690
|
conn.execute(text("ALTER TABLE conversation_history ADD COLUMN parent_message_id VARCHAR(50)"))
|
|
687
691
|
except Exception:
|
|
688
692
|
pass # Column already exists
|
|
693
|
+
try:
|
|
694
|
+
conn.execute(text("ALTER TABLE conversation_history ADD COLUMN params TEXT"))
|
|
695
|
+
except Exception:
|
|
696
|
+
pass # Column already exists
|
|
697
|
+
try:
|
|
698
|
+
conn.execute(text("ALTER TABLE conversation_history ADD COLUMN input_tokens INTEGER"))
|
|
699
|
+
except Exception:
|
|
700
|
+
pass # Column already exists
|
|
701
|
+
try:
|
|
702
|
+
conn.execute(text("ALTER TABLE conversation_history ADD COLUMN output_tokens INTEGER"))
|
|
703
|
+
except Exception:
|
|
704
|
+
pass # Column already exists
|
|
705
|
+
try:
|
|
706
|
+
conn.execute(text("ALTER TABLE conversation_history ADD COLUMN cost VARCHAR(50)"))
|
|
707
|
+
except Exception:
|
|
708
|
+
pass # Column already exists
|
|
689
709
|
|
|
690
710
|
def _setup_execution_triggers(self):
|
|
691
711
|
if 'sqlite' in str(self.engine.url):
|
|
@@ -871,6 +891,10 @@ class CommandHistory:
|
|
|
871
891
|
parent_message_id=None,
|
|
872
892
|
device_id=None,
|
|
873
893
|
device_name=None,
|
|
894
|
+
gen_params=None,
|
|
895
|
+
input_tokens=None,
|
|
896
|
+
output_tokens=None,
|
|
897
|
+
cost=None,
|
|
874
898
|
):
|
|
875
899
|
if isinstance(content, (dict, list)):
|
|
876
900
|
content = json.dumps(content, cls=CustomJSONEncoder)
|
|
@@ -880,21 +904,31 @@ class CommandHistory:
|
|
|
880
904
|
tool_calls = json.dumps(tool_calls, cls=CustomJSONEncoder)
|
|
881
905
|
if tool_results is not None and not isinstance(tool_results, str):
|
|
882
906
|
tool_results = json.dumps(tool_results, cls=CustomJSONEncoder)
|
|
907
|
+
# Serialize gen_params as JSON
|
|
908
|
+
gen_params_json = None
|
|
909
|
+
if gen_params is not None and not isinstance(gen_params, str):
|
|
910
|
+
gen_params_json = json.dumps(gen_params, cls=CustomJSONEncoder)
|
|
911
|
+
elif isinstance(gen_params, str):
|
|
912
|
+
gen_params_json = gen_params
|
|
883
913
|
|
|
884
914
|
# Normalize directory path for cross-platform compatibility
|
|
885
915
|
normalized_directory_path = normalize_path_for_db(directory_path)
|
|
886
916
|
|
|
917
|
+
# Convert cost to string for precision
|
|
918
|
+
cost_str = str(cost) if cost is not None else None
|
|
919
|
+
|
|
887
920
|
stmt = """
|
|
888
921
|
INSERT INTO conversation_history
|
|
889
|
-
(message_id, timestamp, role, content, conversation_id, directory_path, model, provider, npc, team, reasoning_content, tool_calls, tool_results, parent_message_id, device_id, device_name)
|
|
890
|
-
VALUES (:message_id, :timestamp, :role, :content, :conversation_id, :directory_path, :model, :provider, :npc, :team, :reasoning_content, :tool_calls, :tool_results, :parent_message_id, :device_id, :device_name)
|
|
922
|
+
(message_id, timestamp, role, content, conversation_id, directory_path, model, provider, npc, team, reasoning_content, tool_calls, tool_results, parent_message_id, device_id, device_name, params, input_tokens, output_tokens, cost)
|
|
923
|
+
VALUES (:message_id, :timestamp, :role, :content, :conversation_id, :directory_path, :model, :provider, :npc, :team, :reasoning_content, :tool_calls, :tool_results, :parent_message_id, :device_id, :device_name, :params, :input_tokens, :output_tokens, :cost)
|
|
891
924
|
"""
|
|
892
925
|
params = {
|
|
893
926
|
"message_id": message_id, "timestamp": timestamp, "role": role, "content": content,
|
|
894
927
|
"conversation_id": conversation_id, "directory_path": normalized_directory_path, "model": model,
|
|
895
928
|
"provider": provider, "npc": npc, "team": team, "reasoning_content": reasoning_content,
|
|
896
929
|
"tool_calls": tool_calls, "tool_results": tool_results, "parent_message_id": parent_message_id,
|
|
897
|
-
"device_id": device_id, "device_name": device_name
|
|
930
|
+
"device_id": device_id, "device_name": device_name, "params": gen_params_json,
|
|
931
|
+
"input_tokens": input_tokens, "output_tokens": output_tokens, "cost": cost_str
|
|
898
932
|
}
|
|
899
933
|
with self.engine.begin() as conn:
|
|
900
934
|
conn.execute(text(stmt), params)
|
|
@@ -1050,7 +1084,35 @@ class CommandHistory:
|
|
|
1050
1084
|
|
|
1051
1085
|
with self.engine.begin() as conn:
|
|
1052
1086
|
conn.execute(text(stmt), params)
|
|
1053
|
-
|
|
1087
|
+
|
|
1088
|
+
def get_approved_memories_by_scope(self):
|
|
1089
|
+
"""Get all approved/edited memories grouped by (npc, team, path) scope."""
|
|
1090
|
+
stmt = """
|
|
1091
|
+
SELECT id, npc, team, directory_path, initial_memory, final_memory, status
|
|
1092
|
+
FROM memory_lifecycle
|
|
1093
|
+
WHERE status IN ('human-approved', 'human-edited')
|
|
1094
|
+
ORDER BY npc, team, directory_path, created_at
|
|
1095
|
+
"""
|
|
1096
|
+
rows = self._fetch_all(stmt, {})
|
|
1097
|
+
|
|
1098
|
+
from collections import defaultdict
|
|
1099
|
+
memories_by_scope = defaultdict(list)
|
|
1100
|
+
for row in rows:
|
|
1101
|
+
statement = row.get('final_memory') or row.get('initial_memory')
|
|
1102
|
+
scope_key = (
|
|
1103
|
+
row.get('npc') or 'default',
|
|
1104
|
+
row.get('team') or 'global_team',
|
|
1105
|
+
row.get('directory_path') or os.getcwd()
|
|
1106
|
+
)
|
|
1107
|
+
memories_by_scope[scope_key].append({
|
|
1108
|
+
'id': row.get('id'),
|
|
1109
|
+
'statement': statement,
|
|
1110
|
+
'source_text': '',
|
|
1111
|
+
'type': 'explicit',
|
|
1112
|
+
'generation': 0
|
|
1113
|
+
})
|
|
1114
|
+
return dict(memories_by_scope)
|
|
1115
|
+
|
|
1054
1116
|
def add_attachment(self, message_id, name, attachment_type, data, size, file_path=None):
|
|
1055
1117
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
1056
1118
|
stmt = """
|
|
@@ -1239,7 +1301,7 @@ class CommandHistory:
|
|
|
1239
1301
|
stmt = """
|
|
1240
1302
|
SELECT id, message_id, timestamp, role, content, conversation_id,
|
|
1241
1303
|
directory_path, model, provider, npc, team,
|
|
1242
|
-
reasoning_content, tool_calls, tool_results
|
|
1304
|
+
reasoning_content, tool_calls, tool_results, parent_message_id, params
|
|
1243
1305
|
FROM conversation_history WHERE conversation_id = :conversation_id
|
|
1244
1306
|
ORDER BY timestamp ASC
|
|
1245
1307
|
"""
|
|
@@ -1260,6 +1322,11 @@ class CommandHistory:
|
|
|
1260
1322
|
message_dict["tool_results"] = json.loads(message_dict["tool_results"])
|
|
1261
1323
|
except (json.JSONDecodeError, TypeError):
|
|
1262
1324
|
pass
|
|
1325
|
+
if message_dict.get("params"):
|
|
1326
|
+
try:
|
|
1327
|
+
message_dict["params"] = json.loads(message_dict["params"])
|
|
1328
|
+
except (json.JSONDecodeError, TypeError):
|
|
1329
|
+
pass
|
|
1263
1330
|
return results
|
|
1264
1331
|
|
|
1265
1332
|
def get_npc_conversation_stats(self, start_date=None, end_date=None) -> pd.DataFrame:
|
|
@@ -1468,10 +1535,15 @@ def save_conversation_message(
|
|
|
1468
1535
|
skip_if_exists: bool = True,
|
|
1469
1536
|
device_id: str = None,
|
|
1470
1537
|
device_name: str = None,
|
|
1538
|
+
gen_params: Dict = None,
|
|
1539
|
+
input_tokens: int = None,
|
|
1540
|
+
output_tokens: int = None,
|
|
1541
|
+
cost: float = None,
|
|
1471
1542
|
):
|
|
1472
1543
|
"""
|
|
1473
1544
|
Saves a conversation message linked to a conversation ID with optional attachments.
|
|
1474
|
-
Now also supports reasoning_content, tool_calls, tool_results,
|
|
1545
|
+
Now also supports reasoning_content, tool_calls, tool_results, parent_message_id for broadcast grouping,
|
|
1546
|
+
and gen_params for temperature, top_p, top_k, max_tokens, etc.
|
|
1475
1547
|
If skip_if_exists is True and message_id already exists, skip saving to prevent duplicates.
|
|
1476
1548
|
"""
|
|
1477
1549
|
if wd is None:
|
|
@@ -1504,7 +1576,11 @@ def save_conversation_message(
|
|
|
1504
1576
|
tool_results=tool_results,
|
|
1505
1577
|
parent_message_id=parent_message_id,
|
|
1506
1578
|
device_id=device_id,
|
|
1507
|
-
device_name=device_name
|
|
1579
|
+
device_name=device_name,
|
|
1580
|
+
gen_params=gen_params,
|
|
1581
|
+
input_tokens=input_tokens,
|
|
1582
|
+
output_tokens=output_tokens,
|
|
1583
|
+
cost=cost)
|
|
1508
1584
|
def retrieve_last_conversation(
|
|
1509
1585
|
command_history: CommandHistory, conversation_id: str
|
|
1510
1586
|
) -> str:
|