npcpy 1.0.26__py3-none-any.whl → 1.2.32__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 (148) hide show
  1. npcpy/__init__.py +0 -7
  2. npcpy/data/audio.py +16 -99
  3. npcpy/data/image.py +43 -42
  4. npcpy/data/load.py +83 -124
  5. npcpy/data/text.py +28 -28
  6. npcpy/data/video.py +8 -32
  7. npcpy/data/web.py +51 -23
  8. npcpy/ft/diff.py +110 -0
  9. npcpy/ft/ge.py +115 -0
  10. npcpy/ft/memory_trainer.py +171 -0
  11. npcpy/ft/model_ensembler.py +357 -0
  12. npcpy/ft/rl.py +360 -0
  13. npcpy/ft/sft.py +248 -0
  14. npcpy/ft/usft.py +128 -0
  15. npcpy/gen/audio_gen.py +24 -0
  16. npcpy/gen/embeddings.py +13 -13
  17. npcpy/gen/image_gen.py +262 -117
  18. npcpy/gen/response.py +615 -415
  19. npcpy/gen/video_gen.py +53 -7
  20. npcpy/llm_funcs.py +1869 -437
  21. npcpy/main.py +1 -1
  22. npcpy/memory/command_history.py +844 -510
  23. npcpy/memory/kg_vis.py +833 -0
  24. npcpy/memory/knowledge_graph.py +892 -1845
  25. npcpy/memory/memory_processor.py +81 -0
  26. npcpy/memory/search.py +188 -90
  27. npcpy/mix/debate.py +192 -3
  28. npcpy/npc_compiler.py +1672 -801
  29. npcpy/npc_sysenv.py +593 -1266
  30. npcpy/serve.py +3120 -0
  31. npcpy/sql/ai_function_tools.py +257 -0
  32. npcpy/sql/database_ai_adapters.py +186 -0
  33. npcpy/sql/database_ai_functions.py +163 -0
  34. npcpy/sql/model_runner.py +19 -19
  35. npcpy/sql/npcsql.py +706 -507
  36. npcpy/sql/sql_model_compiler.py +156 -0
  37. npcpy/tools.py +183 -0
  38. npcpy/work/plan.py +13 -279
  39. npcpy/work/trigger.py +3 -3
  40. npcpy-1.2.32.dist-info/METADATA +803 -0
  41. npcpy-1.2.32.dist-info/RECORD +54 -0
  42. npcpy/data/dataframes.py +0 -171
  43. npcpy/memory/deep_research.py +0 -125
  44. npcpy/memory/sleep.py +0 -557
  45. npcpy/modes/_state.py +0 -78
  46. npcpy/modes/alicanto.py +0 -1075
  47. npcpy/modes/guac.py +0 -785
  48. npcpy/modes/mcp_npcsh.py +0 -822
  49. npcpy/modes/npc.py +0 -213
  50. npcpy/modes/npcsh.py +0 -1158
  51. npcpy/modes/plonk.py +0 -409
  52. npcpy/modes/pti.py +0 -234
  53. npcpy/modes/serve.py +0 -1637
  54. npcpy/modes/spool.py +0 -312
  55. npcpy/modes/wander.py +0 -549
  56. npcpy/modes/yap.py +0 -572
  57. npcpy/npc_team/alicanto.npc +0 -2
  58. npcpy/npc_team/alicanto.png +0 -0
  59. npcpy/npc_team/assembly_lines/test_pipeline.py +0 -181
  60. npcpy/npc_team/corca.npc +0 -13
  61. npcpy/npc_team/foreman.npc +0 -7
  62. npcpy/npc_team/frederic.npc +0 -6
  63. npcpy/npc_team/frederic4.png +0 -0
  64. npcpy/npc_team/guac.png +0 -0
  65. npcpy/npc_team/jinxs/automator.jinx +0 -18
  66. npcpy/npc_team/jinxs/bash_executer.jinx +0 -31
  67. npcpy/npc_team/jinxs/calculator.jinx +0 -11
  68. npcpy/npc_team/jinxs/edit_file.jinx +0 -96
  69. npcpy/npc_team/jinxs/file_chat.jinx +0 -14
  70. npcpy/npc_team/jinxs/gui_controller.jinx +0 -28
  71. npcpy/npc_team/jinxs/image_generation.jinx +0 -29
  72. npcpy/npc_team/jinxs/internet_search.jinx +0 -30
  73. npcpy/npc_team/jinxs/local_search.jinx +0 -152
  74. npcpy/npc_team/jinxs/npcsh_executor.jinx +0 -31
  75. npcpy/npc_team/jinxs/python_executor.jinx +0 -8
  76. npcpy/npc_team/jinxs/screen_cap.jinx +0 -25
  77. npcpy/npc_team/jinxs/sql_executor.jinx +0 -33
  78. npcpy/npc_team/kadiefa.npc +0 -3
  79. npcpy/npc_team/kadiefa.png +0 -0
  80. npcpy/npc_team/npcsh.ctx +0 -9
  81. npcpy/npc_team/npcsh_sibiji.png +0 -0
  82. npcpy/npc_team/plonk.npc +0 -2
  83. npcpy/npc_team/plonk.png +0 -0
  84. npcpy/npc_team/plonkjr.npc +0 -2
  85. npcpy/npc_team/plonkjr.png +0 -0
  86. npcpy/npc_team/sibiji.npc +0 -5
  87. npcpy/npc_team/sibiji.png +0 -0
  88. npcpy/npc_team/spool.png +0 -0
  89. npcpy/npc_team/templates/analytics/celona.npc +0 -0
  90. npcpy/npc_team/templates/hr_support/raone.npc +0 -0
  91. npcpy/npc_team/templates/humanities/eriane.npc +0 -4
  92. npcpy/npc_team/templates/it_support/lineru.npc +0 -0
  93. npcpy/npc_team/templates/marketing/slean.npc +0 -4
  94. npcpy/npc_team/templates/philosophy/maurawa.npc +0 -0
  95. npcpy/npc_team/templates/sales/turnic.npc +0 -4
  96. npcpy/npc_team/templates/software/welxor.npc +0 -0
  97. npcpy/npc_team/yap.png +0 -0
  98. npcpy/routes.py +0 -958
  99. npcpy/work/mcp_helpers.py +0 -357
  100. npcpy/work/mcp_server.py +0 -194
  101. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.npc +0 -2
  102. npcpy-1.0.26.data/data/npcpy/npc_team/alicanto.png +0 -0
  103. npcpy-1.0.26.data/data/npcpy/npc_team/automator.jinx +0 -18
  104. npcpy-1.0.26.data/data/npcpy/npc_team/bash_executer.jinx +0 -31
  105. npcpy-1.0.26.data/data/npcpy/npc_team/calculator.jinx +0 -11
  106. npcpy-1.0.26.data/data/npcpy/npc_team/celona.npc +0 -0
  107. npcpy-1.0.26.data/data/npcpy/npc_team/corca.npc +0 -13
  108. npcpy-1.0.26.data/data/npcpy/npc_team/edit_file.jinx +0 -96
  109. npcpy-1.0.26.data/data/npcpy/npc_team/eriane.npc +0 -4
  110. npcpy-1.0.26.data/data/npcpy/npc_team/file_chat.jinx +0 -14
  111. npcpy-1.0.26.data/data/npcpy/npc_team/foreman.npc +0 -7
  112. npcpy-1.0.26.data/data/npcpy/npc_team/frederic.npc +0 -6
  113. npcpy-1.0.26.data/data/npcpy/npc_team/frederic4.png +0 -0
  114. npcpy-1.0.26.data/data/npcpy/npc_team/guac.png +0 -0
  115. npcpy-1.0.26.data/data/npcpy/npc_team/gui_controller.jinx +0 -28
  116. npcpy-1.0.26.data/data/npcpy/npc_team/image_generation.jinx +0 -29
  117. npcpy-1.0.26.data/data/npcpy/npc_team/internet_search.jinx +0 -30
  118. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.npc +0 -3
  119. npcpy-1.0.26.data/data/npcpy/npc_team/kadiefa.png +0 -0
  120. npcpy-1.0.26.data/data/npcpy/npc_team/lineru.npc +0 -0
  121. npcpy-1.0.26.data/data/npcpy/npc_team/local_search.jinx +0 -152
  122. npcpy-1.0.26.data/data/npcpy/npc_team/maurawa.npc +0 -0
  123. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh.ctx +0 -9
  124. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_executor.jinx +0 -31
  125. npcpy-1.0.26.data/data/npcpy/npc_team/npcsh_sibiji.png +0 -0
  126. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.npc +0 -2
  127. npcpy-1.0.26.data/data/npcpy/npc_team/plonk.png +0 -0
  128. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.npc +0 -2
  129. npcpy-1.0.26.data/data/npcpy/npc_team/plonkjr.png +0 -0
  130. npcpy-1.0.26.data/data/npcpy/npc_team/python_executor.jinx +0 -8
  131. npcpy-1.0.26.data/data/npcpy/npc_team/raone.npc +0 -0
  132. npcpy-1.0.26.data/data/npcpy/npc_team/screen_cap.jinx +0 -25
  133. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.npc +0 -5
  134. npcpy-1.0.26.data/data/npcpy/npc_team/sibiji.png +0 -0
  135. npcpy-1.0.26.data/data/npcpy/npc_team/slean.npc +0 -4
  136. npcpy-1.0.26.data/data/npcpy/npc_team/spool.png +0 -0
  137. npcpy-1.0.26.data/data/npcpy/npc_team/sql_executor.jinx +0 -33
  138. npcpy-1.0.26.data/data/npcpy/npc_team/test_pipeline.py +0 -181
  139. npcpy-1.0.26.data/data/npcpy/npc_team/turnic.npc +0 -4
  140. npcpy-1.0.26.data/data/npcpy/npc_team/welxor.npc +0 -0
  141. npcpy-1.0.26.data/data/npcpy/npc_team/yap.png +0 -0
  142. npcpy-1.0.26.dist-info/METADATA +0 -827
  143. npcpy-1.0.26.dist-info/RECORD +0 -139
  144. npcpy-1.0.26.dist-info/entry_points.txt +0 -11
  145. /npcpy/{modes → ft}/__init__.py +0 -0
  146. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/WHEEL +0 -0
  147. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/licenses/LICENSE +0 -0
  148. {npcpy-1.0.26.dist-info → npcpy-1.2.32.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,171 @@
1
+ try:
2
+ from torch.utils.data import Dataset
3
+ import torch
4
+ import torch.nn as nn
5
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments
6
+
7
+ import json
8
+ from typing import List, Dict, Tuple
9
+ import random
10
+
11
+ class MemoryDataset(Dataset):
12
+ def __init__(self, examples: List[Dict], tokenizer, max_length=512):
13
+ self.examples = examples
14
+ self.tokenizer = tokenizer
15
+ self.max_length = max_length
16
+
17
+ def __len__(self):
18
+ return len(self.examples)
19
+
20
+ def __getitem__(self, idx):
21
+ example = self.examples[idx]
22
+
23
+
24
+ text = f"Memory: {example['memory']}\nContext: {example.get('context', '')}"
25
+
26
+ encoding = self.tokenizer(
27
+ text,
28
+ truncation=True,
29
+ padding='max_length',
30
+ max_length=self.max_length,
31
+ return_tensors='pt'
32
+ )
33
+
34
+ return {
35
+ 'input_ids': encoding['input_ids'].flatten(),
36
+ 'attention_mask': encoding['attention_mask'].flatten(),
37
+ 'labels': torch.tensor(example['label'], dtype=torch.long)
38
+ }
39
+
40
+ class MemoryTrainer:
41
+ def __init__(self, model_name="google/gemma-2b", device="cpu"):
42
+ self.device = device
43
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
44
+ if self.tokenizer.pad_token is None:
45
+ self.tokenizer.pad_token = self.tokenizer.eos_token
46
+
47
+
48
+ self.model = AutoModelForSequenceClassification.from_pretrained(
49
+ model_name,
50
+ num_labels=3
51
+ ).to(device)
52
+
53
+ def prepare_training_data(self, approved_memories: List[Dict],
54
+ rejected_memories: List[Dict]) -> List[Dict]:
55
+ """Prepare training data from memory examples"""
56
+ examples = []
57
+
58
+
59
+ for memory in approved_memories:
60
+ examples.append({
61
+ "memory": memory.get("final_memory") or memory.get("initial_memory"),
62
+ "context": memory.get("context", ""),
63
+ "label": 1
64
+ })
65
+
66
+
67
+ for memory in rejected_memories:
68
+ examples.append({
69
+ "memory": memory.get("initial_memory"),
70
+ "context": memory.get("context", ""),
71
+ "label": 0
72
+ })
73
+
74
+
75
+ edited_examples = []
76
+ for memory in approved_memories[:len(rejected_memories)//2]:
77
+ if memory.get("final_memory") and memory.get("initial_memory"):
78
+
79
+ edited_examples.append({
80
+ "memory": memory.get("initial_memory"),
81
+ "context": memory.get("context", ""),
82
+ "label": 2
83
+ })
84
+
85
+ examples.extend(edited_examples)
86
+ random.shuffle(examples)
87
+ return examples
88
+
89
+ def train(self, approved_memories: List[Dict], rejected_memories: List[Dict],
90
+ output_dir: str = "./memory_model", epochs: int = 3):
91
+ """Train the memory classification model"""
92
+
93
+ if len(approved_memories) < 10 or len(rejected_memories) < 10:
94
+ print("Not enough training data. Need at least 10 approved and 10 rejected memories.")
95
+ return False
96
+
97
+ training_data = self.prepare_training_data(approved_memories, rejected_memories)
98
+
99
+
100
+ split_idx = int(0.8 * len(training_data))
101
+ train_data = training_data[:split_idx]
102
+ val_data = training_data[split_idx:]
103
+
104
+ train_dataset = MemoryDataset(train_data, self.tokenizer)
105
+ val_dataset = MemoryDataset(val_data, self.tokenizer)
106
+
107
+ training_args = TrainingArguments(
108
+ output_dir=output_dir,
109
+ num_train_epochs=epochs,
110
+ per_device_train_batch_size=4,
111
+ per_device_eval_batch_size=4,
112
+ warmup_steps=100,
113
+ weight_decay=0.01,
114
+ logging_dir='./logs',
115
+ evaluation_strategy="epoch",
116
+ save_strategy="epoch",
117
+ load_best_model_at_end=True,
118
+ )
119
+
120
+ trainer = Trainer(
121
+ model=self.model,
122
+ args=training_args,
123
+ train_dataset=train_dataset,
124
+ eval_dataset=val_dataset,
125
+ )
126
+
127
+ trainer.train()
128
+ trainer.save_model()
129
+ self.tokenizer.save_pretrained(output_dir)
130
+
131
+ print(f"Model trained and saved to {output_dir}")
132
+ return True
133
+
134
+ def predict_memory_action(self, memory_content: str, context: str = "") -> Tuple[str, float]:
135
+ """Predict what action to take on a memory"""
136
+ text = f"Memory: {memory_content}\nContext: {context}"
137
+
138
+ encoding = self.tokenizer(
139
+ text,
140
+ truncation=True,
141
+ padding=True,
142
+ max_length=512,
143
+ return_tensors='pt'
144
+ ).to(self.device)
145
+
146
+ with torch.no_grad():
147
+ outputs = self.model(**encoding)
148
+ probabilities = torch.softmax(outputs.logits, dim=-1)
149
+ predicted_class = torch.argmax(probabilities, dim=-1).item()
150
+ confidence = probabilities[0][predicted_class].item()
151
+
152
+ actions = {0: "model-rejected", 1: "model-approved", 2: "needs-editing"}
153
+ return actions[predicted_class], confidence
154
+
155
+ def auto_approve_memory(self, memory_content: str, context: str = "",
156
+ confidence_threshold: float = 0.8) -> Dict:
157
+ """Auto-approve memory if confidence is high enough"""
158
+ action, confidence = self.predict_memory_action(memory_content, context)
159
+
160
+ if confidence >= confidence_threshold:
161
+ return {"action": action, "confidence": confidence, "auto_processed": True}
162
+ else:
163
+ return {"action": "pending_approval", "confidence": confidence, "auto_processed": False}
164
+ except:
165
+ Dataset = None
166
+ nn = None
167
+ Trainer = None
168
+ TrainingArguments = None
169
+
170
+ MemoryDataset = None
171
+ MemoryTrainer = None
@@ -0,0 +1,357 @@
1
+ import time
2
+ import copy
3
+ import random
4
+ from dataclasses import dataclass, field
5
+ from typing import List, Dict, Any, Optional
6
+ from npcpy.llm_funcs import get_llm_response
7
+
8
+ try:
9
+ from npcpy.ft.sft import predict_sft, load_sft_model
10
+ except:
11
+ pass
12
+
13
+ @dataclass
14
+ class ModelGene:
15
+ """
16
+ Represents a specialized model with trigger patterns
17
+ and confidence threshold
18
+ """
19
+ sft_path: Optional[str] = None
20
+ rl_path: Optional[str] = None
21
+ base_model: str = "Qwen/Qwen3-0.6B"
22
+ specialization: str = "general"
23
+ trigger_patterns: List[str] = field(default_factory=list)
24
+ confidence_threshold: float = 0.7
25
+
26
+
27
+ def generate_trigger_patterns(specialization: str) -> List[str]:
28
+ """
29
+ Generate trigger patterns for a given specialization domain
30
+ """
31
+ patterns = {
32
+ 'math': ['calculate', 'solve', 'equation', 'number'],
33
+ 'code': ['function', 'class', 'bug', 'debug', 'code'],
34
+ 'creative': ['story', 'poem', 'creative', 'imagine'],
35
+ 'factual': ['what is', 'who is', 'when did', 'where is'],
36
+ 'analysis': ['analyze', 'compare', 'evaluate', 'assess']
37
+ }
38
+
39
+ return patterns.get(specialization, ['general'])
40
+
41
+
42
+ def create_model_genome(
43
+ specializations: List[str],
44
+ base_model: str = "Qwen/Qwen3-0.6B"
45
+ ) -> List[ModelGene]:
46
+ """
47
+ Initialize a genome of specialized models
48
+ """
49
+ genome = []
50
+
51
+ for spec in specializations:
52
+ gene = ModelGene(
53
+ base_model=base_model,
54
+ specialization=spec,
55
+ trigger_patterns=generate_trigger_patterns(spec),
56
+ confidence_threshold=random.uniform(0.6, 0.9)
57
+ )
58
+ genome.append(gene)
59
+
60
+ return genome
61
+
62
+
63
+ def mutate_model_genome(
64
+ genome: List[ModelGene],
65
+ mutation_type: str = 'random'
66
+ ) -> List[ModelGene]:
67
+ """
68
+ Apply genetic mutation to model genome
69
+ """
70
+ new_genome = copy.deepcopy(genome)
71
+
72
+ mutations = [
73
+ 'adjust_threshold',
74
+ 'add_trigger',
75
+ 'remove_gene',
76
+ 'duplicate_gene'
77
+ ]
78
+
79
+ if mutation_type == 'random':
80
+ mutation_type = random.choice(mutations)
81
+
82
+ if mutation_type == 'adjust_threshold':
83
+ gene = random.choice(new_genome)
84
+ gene.confidence_threshold += random.uniform(-0.1, 0.1)
85
+ gene.confidence_threshold = max(
86
+ 0.5,
87
+ min(0.95, gene.confidence_threshold)
88
+ )
89
+
90
+ elif mutation_type == 'add_trigger':
91
+ gene = random.choice(new_genome)
92
+ new_trigger = f"pattern_{random.randint(1, 100)}"
93
+ if new_trigger not in gene.trigger_patterns:
94
+ gene.trigger_patterns.append(new_trigger)
95
+
96
+ elif mutation_type == 'remove_gene' and len(new_genome) > 1:
97
+ new_genome.pop(random.randint(0, len(new_genome) - 1))
98
+
99
+ elif mutation_type == 'duplicate_gene':
100
+ gene = random.choice(new_genome)
101
+ new_gene = copy.deepcopy(gene)
102
+ new_gene.specialization = f"{gene.specialization}_variant"
103
+ new_genome.append(new_gene)
104
+
105
+ return new_genome
106
+
107
+
108
+ def crossover_model_genomes(
109
+ genome1: List[ModelGene],
110
+ genome2: List[ModelGene]
111
+ ) -> List[ModelGene]:
112
+ """
113
+ Crossover two model genomes to create child genome
114
+ """
115
+ if not genome1 or not genome2:
116
+ return genome1 or genome2
117
+
118
+ split = random.randint(1, min(len(genome1), len(genome2)) - 1)
119
+
120
+ child = genome1[:split] + genome2[split:]
121
+
122
+ return child
123
+
124
+
125
+ def evaluate_model_genome(
126
+ genome: List[ModelGene],
127
+ test_cases: List[Dict[str, Any]],
128
+ router: 'ResponseRouter'
129
+ ) -> float:
130
+ """
131
+ Evaluate fitness of a model genome based on accuracy,
132
+ speed and efficiency
133
+ """
134
+ correct = 0
135
+ total_time = 0
136
+ fast_responses = 0
137
+
138
+ for test_case in test_cases:
139
+ result = router.route_query(
140
+ test_case['query'],
141
+ genome,
142
+ test_case.get('ground_truth')
143
+ )
144
+
145
+ if result['correct']:
146
+ correct += 1
147
+
148
+ total_time += result['response_time']
149
+
150
+ if result['used_fast_path']:
151
+ fast_responses += 1
152
+
153
+ accuracy = correct / len(test_cases)
154
+ speed_bonus = fast_responses / len(test_cases)
155
+ efficiency = 1.0 / (total_time / len(test_cases))
156
+
157
+ fitness = (
158
+ accuracy * 0.6 +
159
+ speed_bonus * 0.2 +
160
+ efficiency * 0.2
161
+ )
162
+
163
+ return fitness
164
+
165
+
166
+ class ResponseRouter:
167
+ """
168
+ Routes queries through fast path, ensemble or full reasoning
169
+ based on confidence thresholds
170
+ """
171
+ def __init__(
172
+ self,
173
+ fast_threshold: float = 0.8,
174
+ ensemble_threshold: float = 0.6
175
+ ):
176
+ self.fast_threshold = fast_threshold
177
+ self.ensemble_threshold = ensemble_threshold
178
+ self.response_cache = {}
179
+
180
+ def route_query(
181
+ self,
182
+ query: str,
183
+ genome: List[ModelGene],
184
+ ground_truth: Optional[str] = None
185
+ ) -> Dict[str, Any]:
186
+ """
187
+ Route query through system 1 fast path,
188
+ ensemble or system 2 reasoning
189
+ """
190
+ start_time = time.time()
191
+
192
+ fast_response = self._try_fast_path(query, genome)
193
+
194
+ if fast_response and fast_response['confidence'] > (
195
+ self.fast_threshold
196
+ ):
197
+ response_time = time.time() - start_time
198
+
199
+ return {
200
+ 'response': fast_response['answer'],
201
+ 'confidence': fast_response['confidence'],
202
+ 'used_fast_path': True,
203
+ 'response_time': response_time,
204
+ 'correct': (
205
+ ground_truth is None or
206
+ self._check_correctness(
207
+ fast_response['answer'],
208
+ ground_truth
209
+ )
210
+ )
211
+ }
212
+
213
+ ensemble_response = self._try_ensemble(query, genome)
214
+
215
+ if ensemble_response['confidence'] > (
216
+ self.ensemble_threshold
217
+ ):
218
+ response_time = time.time() - start_time
219
+
220
+ return {
221
+ 'response': ensemble_response['answer'],
222
+ 'confidence': ensemble_response['confidence'],
223
+ 'used_fast_path': False,
224
+ 'used_ensemble': True,
225
+ 'response_time': response_time,
226
+ 'correct': (
227
+ ground_truth is None or
228
+ self._check_correctness(
229
+ ensemble_response['answer'],
230
+ ground_truth
231
+ )
232
+ )
233
+ }
234
+
235
+ full_response = self._full_reasoning(query)
236
+ response_time = time.time() - start_time
237
+
238
+ return {
239
+ 'response': full_response,
240
+ 'confidence': 0.5,
241
+ 'used_fast_path': False,
242
+ 'used_ensemble': False,
243
+ 'response_time': response_time,
244
+ 'correct': (
245
+ ground_truth is None or
246
+ self._check_correctness(
247
+ full_response,
248
+ ground_truth
249
+ )
250
+ )
251
+ }
252
+
253
+ def _try_fast_path(
254
+ self,
255
+ query: str,
256
+ genome: List[ModelGene]
257
+ ) -> Optional[Dict[str, Any]]:
258
+ """
259
+ Try fast system 1 gut reaction using pattern matching
260
+ """
261
+ query_lower = query.lower()
262
+
263
+ for gene in genome:
264
+ if any(
265
+ pattern in query_lower
266
+ for pattern in gene.trigger_patterns
267
+ ):
268
+ if gene.sft_path:
269
+ model, tokenizer = load_sft_model(gene.sft_path)
270
+
271
+ response = predict_sft(
272
+ model,
273
+ tokenizer,
274
+ query,
275
+ temperature=0.1
276
+ )
277
+
278
+ return {
279
+ 'answer': response,
280
+ 'confidence': gene.confidence_threshold
281
+ }
282
+
283
+ return None
284
+
285
+ def _try_ensemble(
286
+ self,
287
+ query: str,
288
+ genome: List[ModelGene]
289
+ ) -> Dict[str, Any]:
290
+ """
291
+ Try ensemble voting across specialized models
292
+ """
293
+ responses = []
294
+
295
+ for gene in genome:
296
+ if gene.sft_path or gene.rl_path:
297
+ model_path = gene.rl_path or gene.sft_path
298
+
299
+ model, tokenizer = load_sft_model(model_path)
300
+
301
+ response = predict_sft(
302
+ model,
303
+ tokenizer,
304
+ query,
305
+ temperature=0.3
306
+ )
307
+
308
+ responses.append({
309
+ 'answer': response,
310
+ 'weight': gene.confidence_threshold
311
+ })
312
+
313
+ if not responses:
314
+ return {'answer': '', 'confidence': 0.0}
315
+
316
+ best_response = max(responses, key=lambda x: x['weight'])
317
+
318
+ avg_confidence = sum(
319
+ r['weight'] for r in responses
320
+ ) / len(responses)
321
+
322
+ return {
323
+ 'answer': best_response['answer'],
324
+ 'confidence': avg_confidence
325
+ }
326
+
327
+ def _full_reasoning(
328
+ self,
329
+ query: str,
330
+ model: str = "qwen3:1.7b",
331
+ provider: str = "ollama"
332
+ ) -> str:
333
+ """
334
+ Fall back to full system 2 reasoning
335
+ """
336
+ response = get_llm_response(
337
+ query,
338
+ model=model,
339
+ provider=provider
340
+ )
341
+
342
+ return response.get('response', '')
343
+
344
+ def _check_correctness(
345
+ self,
346
+ response: str,
347
+ ground_truth: str
348
+ ) -> bool:
349
+ """
350
+ Check if response matches ground truth
351
+ """
352
+ response_lower = response.lower().strip()
353
+ truth_lower = ground_truth.lower().strip()
354
+
355
+ return response_lower == truth_lower or (
356
+ truth_lower in response_lower
357
+ )