npcpy 1.3.12__tar.gz → 1.3.14__tar.gz

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 (84) hide show
  1. {npcpy-1.3.12/npcpy.egg-info → npcpy-1.3.14}/PKG-INFO +4 -4
  2. {npcpy-1.3.12 → npcpy-1.3.14}/README.md +3 -3
  3. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/diff.py +45 -30
  4. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/rl.py +134 -51
  5. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/response.py +36 -0
  6. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/knowledge_graph.py +1 -3
  7. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/npc_compiler.py +83 -2
  8. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/serve.py +414 -40
  9. {npcpy-1.3.12 → npcpy-1.3.14/npcpy.egg-info}/PKG-INFO +4 -4
  10. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy.egg-info/SOURCES.txt +1 -0
  11. {npcpy-1.3.12 → npcpy-1.3.14}/setup.py +1 -1
  12. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_command_history.py +114 -91
  13. npcpy-1.3.14/tests/test_documentation_examples.py +436 -0
  14. npcpy-1.3.14/tests/test_load.py +291 -0
  15. npcpy-1.3.14/tests/test_serve.py +132 -0
  16. npcpy-1.3.14/tests/test_text.py +215 -0
  17. npcpy-1.3.14/tests/test_tools.py +211 -0
  18. npcpy-1.3.12/tests/test_load.py +0 -284
  19. npcpy-1.3.12/tests/test_serve.py +0 -150
  20. npcpy-1.3.12/tests/test_text.py +0 -256
  21. npcpy-1.3.12/tests/test_tools.py +0 -989
  22. {npcpy-1.3.12 → npcpy-1.3.14}/LICENSE +0 -0
  23. {npcpy-1.3.12 → npcpy-1.3.14}/MANIFEST.in +0 -0
  24. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/__init__.py +0 -0
  25. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/build_funcs.py +0 -0
  26. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/__init__.py +0 -0
  27. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/audio.py +0 -0
  28. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/data_models.py +0 -0
  29. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/image.py +0 -0
  30. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/load.py +0 -0
  31. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/text.py +0 -0
  32. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/video.py +0 -0
  33. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/data/web.py +0 -0
  34. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/__init__.py +0 -0
  35. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/ge.py +0 -0
  36. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/memory_trainer.py +0 -0
  37. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/model_ensembler.py +0 -0
  38. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/sft.py +0 -0
  39. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ft/usft.py +0 -0
  40. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/__init__.py +0 -0
  41. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/audio_gen.py +0 -0
  42. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/embeddings.py +0 -0
  43. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/image_gen.py +0 -0
  44. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/ocr.py +0 -0
  45. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/video_gen.py +0 -0
  46. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/gen/world_gen.py +0 -0
  47. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/llm_funcs.py +0 -0
  48. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/main.py +0 -0
  49. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/__init__.py +0 -0
  50. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/command_history.py +0 -0
  51. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/kg_vis.py +0 -0
  52. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/memory_processor.py +0 -0
  53. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/memory/search.py +0 -0
  54. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/mix/__init__.py +0 -0
  55. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/mix/debate.py +0 -0
  56. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/ml_funcs.py +0 -0
  57. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/npc_array.py +0 -0
  58. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/npc_sysenv.py +0 -0
  59. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/npcs.py +0 -0
  60. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/__init__.py +0 -0
  61. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/ai_function_tools.py +0 -0
  62. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/database_ai_adapters.py +0 -0
  63. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/database_ai_functions.py +0 -0
  64. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/model_runner.py +0 -0
  65. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/npcsql.py +0 -0
  66. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/sql/sql_model_compiler.py +0 -0
  67. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/tools.py +0 -0
  68. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/work/__init__.py +0 -0
  69. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/work/browser.py +0 -0
  70. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/work/desktop.py +0 -0
  71. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/work/plan.py +0 -0
  72. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy/work/trigger.py +0 -0
  73. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy.egg-info/dependency_links.txt +0 -0
  74. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy.egg-info/requires.txt +0 -0
  75. {npcpy-1.3.12 → npcpy-1.3.14}/npcpy.egg-info/top_level.txt +0 -0
  76. {npcpy-1.3.12 → npcpy-1.3.14}/setup.cfg +0 -0
  77. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_audio.py +0 -0
  78. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_image.py +0 -0
  79. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_llm_funcs.py +0 -0
  80. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_npc_array.py +0 -0
  81. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_npc_compiler.py +0 -0
  82. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_npcsql.py +0 -0
  83. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_response.py +0 -0
  84. {npcpy-1.3.12 → npcpy-1.3.14}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.3.12
3
+ Version: 1.3.14
4
4
  Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcpy
6
6
  Author: Christopher Agostino
@@ -305,7 +305,7 @@ ggm = NPC(
305
305
  isabel = NPC(
306
306
  name='Isabel Allende',
307
307
  primary_directive='You are Isabel Allende, weaving stories with emotion and history. Analyze texts and provide insight.',
308
- model='llama3.2:8b',
308
+ model='llama3.2',
309
309
  provider='ollama',
310
310
 
311
311
  )
@@ -359,7 +359,7 @@ LLM responses can be obtained without NPCs as well.
359
359
 
360
360
  ```python
361
361
  from npcpy.llm_funcs import get_llm_response
362
- response = get_llm_response("Who was the celtic Messenger god?", model='mistral:7b', provider='ollama')
362
+ response = get_llm_response("Who was the celtic Messenger god?", model='qwen3:4b', provider='ollama')
363
363
  print(response['response'])
364
364
  ```
365
365
 
@@ -400,7 +400,7 @@ Return structured outputs by specifying `format='json'` or passing a Pydantic sc
400
400
 
401
401
  ```python
402
402
  from npcpy.llm_funcs import get_llm_response
403
- response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='claude-4-5-haiku-latest', provider='deepseek', format='json')
403
+ response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='deepseek-chat', provider='deepseek', format='json')
404
404
 
405
405
  print(response['response'])
406
406
  ```
@@ -209,7 +209,7 @@ ggm = NPC(
209
209
  isabel = NPC(
210
210
  name='Isabel Allende',
211
211
  primary_directive='You are Isabel Allende, weaving stories with emotion and history. Analyze texts and provide insight.',
212
- model='llama3.2:8b',
212
+ model='llama3.2',
213
213
  provider='ollama',
214
214
 
215
215
  )
@@ -263,7 +263,7 @@ LLM responses can be obtained without NPCs as well.
263
263
 
264
264
  ```python
265
265
  from npcpy.llm_funcs import get_llm_response
266
- response = get_llm_response("Who was the celtic Messenger god?", model='mistral:7b', provider='ollama')
266
+ response = get_llm_response("Who was the celtic Messenger god?", model='qwen3:4b', provider='ollama')
267
267
  print(response['response'])
268
268
  ```
269
269
 
@@ -304,7 +304,7 @@ Return structured outputs by specifying `format='json'` or passing a Pydantic sc
304
304
 
305
305
  ```python
306
306
  from npcpy.llm_funcs import get_llm_response
307
- response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='claude-4-5-haiku-latest', provider='deepseek', format='json')
307
+ response = get_llm_response("What is the sentiment of the american people towards the repeal of Roe v Wade? Return a json object with `sentiment` as the key and a float value from -1 to 1 as the value", model='deepseek-chat', provider='deepseek', format='json')
308
308
 
309
309
  print(response['response'])
310
310
  ```
@@ -180,52 +180,66 @@ if TORCH_AVAILABLE:
180
180
  noise = torch.randn_like(x)
181
181
  return sqrt_alpha * x + sqrt_one_minus * noise, noise
182
182
 
183
- def train(self, dataloader):
183
+ def train(self, dataloader, progress_callback=None):
184
184
  optimizer = torch.optim.AdamW(
185
- self.model.parameters(),
185
+ self.model.parameters(),
186
186
  lr=self.config.learning_rate
187
187
  )
188
-
188
+
189
189
  os.makedirs(self.config.output_model_path, exist_ok=True)
190
190
  checkpoint_dir = os.path.join(
191
- self.config.output_model_path,
191
+ self.config.output_model_path,
192
192
  'checkpoints'
193
193
  )
194
194
  os.makedirs(checkpoint_dir, exist_ok=True)
195
-
195
+
196
196
  global_step = 0
197
-
197
+ total_batches = len(dataloader)
198
+ loss_history = []
199
+
198
200
  for epoch in range(self.config.num_epochs):
199
201
  self.model.train()
200
202
  epoch_loss = 0.0
201
-
203
+
202
204
  pbar = tqdm(dataloader, desc=f'Epoch {epoch+1}')
203
205
  for batch_idx, (images, captions) in enumerate(pbar):
204
206
  images = images.to(self.device)
205
207
  batch_size = images.shape[0]
206
-
208
+
207
209
  t = torch.randint(
208
- 0,
209
- self.config.timesteps,
210
- (batch_size,),
210
+ 0,
211
+ self.config.timesteps,
212
+ (batch_size,),
211
213
  device=self.device
212
214
  ).long()
213
-
215
+
214
216
  noisy_images, noise = self.add_noise(images, t)
215
-
217
+
216
218
  predicted_noise = self.model(noisy_images, t)
217
-
219
+
218
220
  loss = F.mse_loss(predicted_noise, noise)
219
-
221
+
220
222
  optimizer.zero_grad()
221
223
  loss.backward()
222
224
  optimizer.step()
223
-
225
+
224
226
  epoch_loss += loss.item()
225
227
  global_step += 1
226
-
228
+
227
229
  pbar.set_postfix({'loss': loss.item()})
228
-
230
+
231
+ # Report progress via callback
232
+ if progress_callback:
233
+ progress_callback({
234
+ 'epoch': epoch + 1,
235
+ 'total_epochs': self.config.num_epochs,
236
+ 'batch': batch_idx + 1,
237
+ 'total_batches': total_batches,
238
+ 'step': global_step,
239
+ 'loss': loss.item(),
240
+ 'loss_history': loss_history[-100:], # Last 100 losses
241
+ })
242
+
229
243
  if global_step % self.config.checkpoint_frequency == 0:
230
244
  ckpt_path = os.path.join(
231
245
  checkpoint_dir,
@@ -238,8 +252,9 @@ if TORCH_AVAILABLE:
238
252
  'optimizer_state_dict': optimizer.state_dict(),
239
253
  'loss': loss.item(),
240
254
  }, ckpt_path)
241
-
255
+
242
256
  avg_loss = epoch_loss / len(dataloader)
257
+ loss_history.append(avg_loss)
243
258
  print(f'Epoch {epoch+1} avg loss: {avg_loss:.6f}')
244
259
 
245
260
  final_path = os.path.join(
@@ -300,35 +315,35 @@ else:
300
315
  DiffusionTrainer = None
301
316
 
302
317
 
303
- def train_diffusion(image_paths, captions=None, config=None,
304
- resume_from=None):
318
+ def train_diffusion(image_paths, captions=None, config=None,
319
+ resume_from=None, progress_callback=None):
305
320
  if not TORCH_AVAILABLE:
306
321
  raise ImportError(
307
322
  "PyTorch not available. Install: pip install torch torchvision"
308
323
  )
309
-
324
+
310
325
  if config is None:
311
326
  config = DiffusionConfig()
312
-
327
+
313
328
  if captions is None:
314
329
  captions = [''] * len(image_paths)
315
-
330
+
316
331
  dataset = ImageDataset(image_paths, captions, config.image_size)
317
332
  dataloader = DataLoader(
318
- dataset,
319
- batch_size=config.batch_size,
333
+ dataset,
334
+ batch_size=config.batch_size,
320
335
  shuffle=True,
321
336
  num_workers=0
322
337
  )
323
-
338
+
324
339
  trainer = DiffusionTrainer(config)
325
-
340
+
326
341
  if resume_from and os.path.exists(resume_from):
327
342
  checkpoint = torch.load(resume_from, map_location=trainer.device)
328
343
  trainer.model.load_state_dict(checkpoint['model_state_dict'])
329
344
  print(f'Resumed from {resume_from}')
330
-
331
- output_path = trainer.train(dataloader)
345
+
346
+ output_path = trainer.train(dataloader, progress_callback=progress_callback)
332
347
 
333
348
  gc.collect()
334
349
  if torch.cuda.is_available():
@@ -1,4 +1,5 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
+ from typing import List
2
3
 
3
4
  from datetime import datetime
4
5
  import glob
@@ -12,7 +13,8 @@ try:
12
13
  import torch
13
14
  from transformers import (
14
15
  AutoModelForCausalLM,
15
- AutoTokenizer
16
+ AutoTokenizer,
17
+ BitsAndBytesConfig
16
18
  )
17
19
  from trl import DPOTrainer, DPOConfig
18
20
  except:
@@ -23,6 +25,7 @@ except:
23
25
  torch = None
24
26
  AutoModelForCausalLM = None
25
27
  AutoTokenizer = None
28
+ BitsAndBytesConfig = None
26
29
 
27
30
 
28
31
  import random
@@ -44,6 +47,24 @@ class RLConfig:
44
47
  beta: float = 0.5
45
48
  max_length: int = 512
46
49
  max_prompt_length: int = 256
50
+ # Quantization options
51
+ use_4bit: bool = False
52
+ use_8bit: bool = False
53
+ # Precision options
54
+ fp16: bool = False
55
+ bf16: bool = False
56
+ # LoRA configuration
57
+ lora_r: int = 8
58
+ lora_alpha: int = 16
59
+ lora_dropout: float = 0.1
60
+ lora_target_modules: List[str] = field(
61
+ default_factory=lambda: ["q_proj", "k_proj", "v_proj", "o_proj"]
62
+ )
63
+ # Training options
64
+ max_pairs: int = 200
65
+ warmup_steps: int = 5
66
+ logging_steps: int = 5
67
+ save_steps: int = 20
47
68
 
48
69
 
49
70
  class TaskExecutor:
@@ -207,8 +228,8 @@ def create_preference_pairs(
207
228
  f"Warning: Only {len(pairs)} pairs found. "
208
229
  "May overfit."
209
230
  )
210
-
211
- return Dataset.from_list(pairs[:100])
231
+
232
+ return Dataset.from_list(pairs)
212
233
 
213
234
 
214
235
  def train_with_dpo(
@@ -218,84 +239,121 @@ def train_with_dpo(
218
239
 
219
240
  if config is None:
220
241
  config = RLConfig()
221
-
242
+
222
243
  preference_dataset = create_preference_pairs(
223
244
  traces,
224
245
  min_reward_gap=config.min_reward_gap
225
246
  )
226
-
247
+
227
248
  if preference_dataset is None or len(preference_dataset) == 0:
228
249
  print("No valid preference pairs. Cannot train.")
229
250
  return None
230
-
251
+
252
+ # Limit pairs if specified
253
+ if config.max_pairs and len(preference_dataset) > config.max_pairs:
254
+ preference_dataset = preference_dataset.select(range(config.max_pairs))
255
+
256
+ print(f"Training with {len(preference_dataset)} preference pairs")
257
+
258
+ # Build model loading kwargs
259
+ model_kwargs = {
260
+ "device_map": "auto",
261
+ "trust_remote_code": True,
262
+ "low_cpu_mem_usage": True
263
+ }
264
+
265
+ # Handle quantization
266
+ if config.use_4bit:
267
+ if BitsAndBytesConfig is None:
268
+ raise ImportError("bitsandbytes required for 4-bit. pip install bitsandbytes")
269
+ model_kwargs["quantization_config"] = BitsAndBytesConfig(
270
+ load_in_4bit=True,
271
+ bnb_4bit_quant_type="nf4",
272
+ bnb_4bit_compute_dtype=torch.float16,
273
+ bnb_4bit_use_double_quant=True
274
+ )
275
+ print("Using 4-bit quantization")
276
+ elif config.use_8bit:
277
+ if BitsAndBytesConfig is None:
278
+ raise ImportError("bitsandbytes required for 8-bit. pip install bitsandbytes")
279
+ model_kwargs["quantization_config"] = BitsAndBytesConfig(
280
+ load_in_8bit=True
281
+ )
282
+ print("Using 8-bit quantization")
283
+ else:
284
+ # Set dtype based on precision config
285
+ if config.bf16:
286
+ model_kwargs["torch_dtype"] = torch.bfloat16
287
+ elif config.fp16:
288
+ model_kwargs["torch_dtype"] = torch.float16
289
+ else:
290
+ model_kwargs["torch_dtype"] = torch.float32
291
+
231
292
  model = AutoModelForCausalLM.from_pretrained(
232
293
  config.base_model_name,
233
- torch_dtype=torch.float32,
234
- device_map="auto",
235
- low_cpu_mem_usage=True
294
+ **model_kwargs
236
295
  )
237
-
296
+
238
297
  tokenizer = AutoTokenizer.from_pretrained(
239
298
  config.base_model_name,
240
299
  trust_remote_code=True
241
300
  )
242
-
301
+
243
302
  if tokenizer.pad_token is None:
244
303
  tokenizer.pad_token = tokenizer.eos_token
245
-
304
+
246
305
  peft_config = LoraConfig(
247
- r=8,
248
- lora_alpha=16,
249
- lora_dropout=0.1,
306
+ r=config.lora_r,
307
+ lora_alpha=config.lora_alpha,
308
+ lora_dropout=config.lora_dropout,
250
309
  bias="none",
251
310
  task_type="CAUSAL_LM",
252
- target_modules=[
253
- "q_proj",
254
- "k_proj",
255
- "v_proj",
256
- "o_proj"
257
- ]
311
+ target_modules=config.lora_target_modules
258
312
  )
259
-
313
+
314
+ # Select optimizer based on quantization
315
+ if config.use_4bit or config.use_8bit:
316
+ optim = "paged_adamw_8bit"
317
+ else:
318
+ optim = "adamw_torch"
319
+
260
320
  training_args = DPOConfig(
261
321
  output_dir="./dpo_results",
262
- per_device_train_batch_size=(
263
- config.per_device_train_batch_size
264
- ),
265
- gradient_accumulation_steps=(
266
- config.gradient_accumulation_steps
267
- ),
322
+ per_device_train_batch_size=config.per_device_train_batch_size,
323
+ gradient_accumulation_steps=config.gradient_accumulation_steps,
268
324
  learning_rate=config.learning_rate,
269
325
  num_train_epochs=config.num_train_epochs,
270
326
  weight_decay=0.1,
271
327
  beta=config.beta,
272
- logging_steps=2,
273
- save_steps=10,
328
+ logging_steps=config.logging_steps,
329
+ save_steps=config.save_steps,
274
330
  remove_unused_columns=False,
275
331
  max_length=config.max_length,
276
332
  max_prompt_length=config.max_prompt_length,
277
333
  dataloader_num_workers=0,
278
- fp16=False,
279
- bf16=False,
280
- optim="adamw_torch",
281
- warmup_steps=2,
334
+ fp16=config.fp16 or config.use_4bit,
335
+ bf16=config.bf16,
336
+ optim=optim,
337
+ warmup_steps=config.warmup_steps,
282
338
  save_strategy="steps",
283
- save_total_limit=3
339
+ save_total_limit=2
284
340
  )
285
-
341
+
286
342
  trainer = DPOTrainer(
287
343
  model,
288
344
  args=training_args,
289
345
  train_dataset=preference_dataset,
290
- peft_config=peft_config
346
+ peft_config=peft_config,
347
+ tokenizer=tokenizer
291
348
  )
292
-
349
+
293
350
  print("Starting DPO training...")
294
351
  trainer.train()
295
-
352
+
353
+ os.makedirs(config.adapter_path, exist_ok=True)
296
354
  trainer.save_model(config.adapter_path)
297
355
  print(f"Adapter saved to {config.adapter_path}")
298
-
356
+
299
357
  return config.adapter_path
300
358
 
301
359
 
@@ -333,28 +391,53 @@ def run_rl_training(
333
391
 
334
392
  def load_rl_model(
335
393
  base_model_id: str,
336
- adapter_path: str
394
+ adapter_path: str,
395
+ use_4bit: bool = False,
396
+ use_8bit: bool = False,
397
+ merge_adapter: bool = True
337
398
  ):
338
-
339
399
  print(f"Loading base model: {base_model_id}")
400
+
401
+ model_kwargs = {
402
+ "device_map": "auto",
403
+ "trust_remote_code": True
404
+ }
405
+
406
+ if use_4bit:
407
+ if BitsAndBytesConfig is None:
408
+ raise ImportError("bitsandbytes required for 4-bit")
409
+ model_kwargs["quantization_config"] = BitsAndBytesConfig(
410
+ load_in_4bit=True,
411
+ bnb_4bit_quant_type="nf4",
412
+ bnb_4bit_compute_dtype=torch.float16,
413
+ bnb_4bit_use_double_quant=True
414
+ )
415
+ elif use_8bit:
416
+ if BitsAndBytesConfig is None:
417
+ raise ImportError("bitsandbytes required for 8-bit")
418
+ model_kwargs["quantization_config"] = BitsAndBytesConfig(
419
+ load_in_8bit=True
420
+ )
421
+ else:
422
+ model_kwargs["torch_dtype"] = torch.float16
423
+
340
424
  model = AutoModelForCausalLM.from_pretrained(
341
425
  base_model_id,
342
- torch_dtype=torch.float32,
343
- device_map="auto",
344
- attn_implementation='eager'
426
+ **model_kwargs
345
427
  )
346
-
428
+
347
429
  tokenizer = AutoTokenizer.from_pretrained(
348
430
  base_model_id,
349
431
  trust_remote_code=True
350
432
  )
351
-
433
+
352
434
  if tokenizer.pad_token is None:
353
435
  tokenizer.pad_token = tokenizer.eos_token
354
-
436
+
355
437
  if adapter_path and os.path.exists(adapter_path):
356
438
  print(f"Loading adapter: {adapter_path}")
357
439
  model = PeftModel.from_pretrained(model, adapter_path)
358
- model = model.merge_and_unload()
359
-
440
+ if merge_adapter and not (use_4bit or use_8bit):
441
+ model = model.merge_and_unload()
442
+
360
443
  return model, tokenizer
@@ -259,6 +259,24 @@ def get_ollama_response(
259
259
  prompt = f"Content from CSV: {os.path.basename(attachment)} (first 100 rows):\n{csv_sample} \n csv description: {csv_data.describe()}"
260
260
  except Exception:
261
261
  pass
262
+ else:
263
+ # Handle text-based files
264
+ text_extensions = {'.txt', '.text', '.log', '.md', '.markdown', '.rst', '.json', '.yaml', '.yml', '.toml', '.ini', '.conf', '.cfg', '.xml', '.html', '.htm', '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.h', '.cpp', '.hpp', '.go', '.rs', '.rb', '.php', '.sh', '.bash', '.sql', '.css', '.scss'}
265
+ filename = os.path.basename(attachment)
266
+ if ext in text_extensions or ext == '':
267
+ try:
268
+ with open(attachment, 'r', encoding='utf-8', errors='replace') as f:
269
+ text_content = f.read()
270
+ max_chars = 50000
271
+ if len(text_content) > max_chars:
272
+ text_content = text_content[:max_chars] + f"\n\n... [truncated]"
273
+ if text_content.strip():
274
+ if prompt:
275
+ prompt += f"\n\nContent from {filename}:\n```\n{text_content}\n```"
276
+ else:
277
+ prompt = f"Content from {filename}:\n```\n{text_content}\n```"
278
+ except Exception:
279
+ pass
262
280
 
263
281
 
264
282
  if prompt:
@@ -797,6 +815,24 @@ def get_litellm_response(
797
815
  prompt = f"Content from CSV: {os.path.basename(attachment)} (first 10 rows):\n{csv_sample}"
798
816
  except Exception:
799
817
  pass
818
+ else:
819
+ # Handle text-based files
820
+ text_extensions = {'.txt', '.text', '.log', '.md', '.markdown', '.rst', '.json', '.yaml', '.yml', '.toml', '.ini', '.conf', '.cfg', '.xml', '.html', '.htm', '.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.c', '.h', '.cpp', '.hpp', '.go', '.rs', '.rb', '.php', '.sh', '.bash', '.sql', '.css', '.scss'}
821
+ filename = os.path.basename(attachment)
822
+ if ext in text_extensions or ext == '':
823
+ try:
824
+ with open(attachment, 'r', encoding='utf-8', errors='replace') as f:
825
+ text_content = f.read()
826
+ max_chars = 50000
827
+ if len(text_content) > max_chars:
828
+ text_content = text_content[:max_chars] + f"\n\n... [truncated]"
829
+ if text_content.strip():
830
+ if prompt:
831
+ prompt += f"\n\nContent from {filename}:\n```\n{text_content}\n```"
832
+ else:
833
+ prompt = f"Content from {filename}:\n```\n{text_content}\n```"
834
+ except Exception:
835
+ pass
800
836
 
801
837
  if prompt:
802
838
  if result['messages'] and result['messages'][-1]["role"] == "user":
@@ -344,7 +344,6 @@ def kg_evolve_incremental(existing_kg,
344
344
 
345
345
  current_gen = existing_kg.get('generation', 0)
346
346
  next_gen = current_gen + 1
347
- print(f"\n--- ABSORBING INFO: Gen {current_gen} -> Gen {next_gen} ---")
348
347
 
349
348
  newly_added_concepts = []
350
349
  concept_links = list(existing_kg.get('concept_links', []))
@@ -359,8 +358,7 @@ def kg_evolve_incremental(existing_kg,
359
358
  all_concept_names = list(existing_concept_names)
360
359
 
361
360
  all_new_facts = []
362
- print(npc, npc.model, npc.provider)
363
-
361
+
364
362
  if new_facts:
365
363
  all_new_facts = new_facts
366
364
  print(f'using pre-approved facts: {len(all_new_facts)}')
@@ -7,6 +7,41 @@ import sqlite3
7
7
  import numpy as np
8
8
  import pandas as pd
9
9
  import matplotlib.pyplot as plt
10
+ import matplotlib as mpl
11
+
12
+ # Professional plot styling (from kg-research matplotlibrc)
13
+ mpl.rcParams.update({
14
+ 'font.family': 'serif',
15
+ 'axes.labelsize': 20,
16
+ 'axes.grid.axis': 'both',
17
+ 'axes.grid.which': 'major',
18
+ 'axes.prop_cycle': mpl.cycler('color', ['k', 'b', 'r', 'g', 'c', 'm', 'y', 'k']),
19
+ 'xtick.top': True,
20
+ 'xtick.direction': 'in',
21
+ 'xtick.major.size': 10,
22
+ 'xtick.minor.size': 5,
23
+ 'xtick.labelsize': 20,
24
+ 'xtick.minor.visible': True,
25
+ 'xtick.major.top': True,
26
+ 'xtick.major.bottom': True,
27
+ 'xtick.minor.top': True,
28
+ 'xtick.minor.bottom': True,
29
+ 'ytick.left': True,
30
+ 'ytick.right': True,
31
+ 'ytick.direction': 'in',
32
+ 'ytick.major.size': 10,
33
+ 'ytick.minor.size': 5,
34
+ 'ytick.labelsize': 20,
35
+ 'ytick.minor.visible': True,
36
+ 'ytick.major.left': True,
37
+ 'ytick.major.right': True,
38
+ 'ytick.minor.left': True,
39
+ 'ytick.minor.right': True,
40
+ 'legend.frameon': False,
41
+ 'legend.fontsize': 12,
42
+ 'image.cmap': 'plasma',
43
+ 'errorbar.capsize': 1,
44
+ })
10
45
  import re
11
46
  import random
12
47
  from datetime import datetime
@@ -31,9 +66,31 @@ from npcpy.npc_sysenv import (
31
66
  from npcpy.memory.command_history import CommandHistory, generate_message_id
32
67
 
33
68
  class SilentUndefined(Undefined):
69
+ """Undefined that silently returns empty string instead of raising errors"""
34
70
  def _fail_with_undefined_error(self, *args, **kwargs):
35
71
  return ""
36
72
 
73
+ def __str__(self):
74
+ return ""
75
+
76
+ def __repr__(self):
77
+ return ""
78
+
79
+ def __bool__(self):
80
+ return False
81
+
82
+ def __eq__(self, other):
83
+ return other == "" or other is None or isinstance(other, Undefined)
84
+
85
+ def __ne__(self, other):
86
+ return not self.__eq__(other)
87
+
88
+ def __iter__(self):
89
+ return iter([])
90
+
91
+ def __len__(self):
92
+ return 0
93
+
37
94
  import math
38
95
  from PIL import Image
39
96
  from jinja2 import Environment, ChainableUndefined
@@ -152,11 +209,35 @@ def get_log_entries(entity_id, entry_type=None, limit=10, db_path="~/npcsh_histo
152
209
  ]
153
210
 
154
211
 
212
+ def _json_dumps_with_undefined(obj, **kwargs):
213
+ """Custom JSON dumps that handles SilentUndefined objects"""
214
+ def default_handler(o):
215
+ if isinstance(o, Undefined):
216
+ return ""
217
+ raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable")
218
+ return json.dumps(obj, default=default_handler, **kwargs)
219
+
220
+
155
221
  def load_yaml_file(file_path):
156
- """Load a YAML file with error handling"""
222
+ """Load a YAML file with error handling, rendering Jinja2 first"""
157
223
  try:
158
224
  with open(os.path.expanduser(file_path), 'r') as f:
159
- return yaml.safe_load(f)
225
+ content = f.read()
226
+
227
+ # Check if file has Jinja2 control structures that need pre-rendering
228
+ # Only render if there are {% %} blocks, otherwise parse directly
229
+ if '{%' not in content:
230
+ return yaml.safe_load(content)
231
+
232
+ # First pass: render Jinja2 templates to produce valid YAML
233
+ # This allows {% if %} and other control structures to work
234
+ jinja_env = Environment(undefined=SilentUndefined)
235
+ # Configure tojson filter to handle SilentUndefined
236
+ jinja_env.policies['json.dumps_function'] = _json_dumps_with_undefined
237
+ template = jinja_env.from_string(content)
238
+ rendered_content = template.render({})
239
+
240
+ return yaml.safe_load(rendered_content)
160
241
  except Exception as e:
161
242
  print(f"Error loading YAML file {file_path}: {e}")
162
243
  return None