npcpy 1.3.13__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.13/npcpy.egg-info → npcpy-1.3.14}/PKG-INFO +4 -4
  2. {npcpy-1.3.13 → npcpy-1.3.14}/README.md +3 -3
  3. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/diff.py +45 -30
  4. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/rl.py +134 -51
  5. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/serve.py +42 -15
  6. {npcpy-1.3.13 → npcpy-1.3.14/npcpy.egg-info}/PKG-INFO +4 -4
  7. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy.egg-info/SOURCES.txt +1 -0
  8. {npcpy-1.3.13 → npcpy-1.3.14}/setup.py +1 -1
  9. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_command_history.py +114 -91
  10. npcpy-1.3.14/tests/test_documentation_examples.py +436 -0
  11. npcpy-1.3.14/tests/test_load.py +291 -0
  12. npcpy-1.3.14/tests/test_serve.py +132 -0
  13. npcpy-1.3.14/tests/test_text.py +215 -0
  14. npcpy-1.3.14/tests/test_tools.py +211 -0
  15. npcpy-1.3.13/tests/test_load.py +0 -284
  16. npcpy-1.3.13/tests/test_serve.py +0 -150
  17. npcpy-1.3.13/tests/test_text.py +0 -256
  18. npcpy-1.3.13/tests/test_tools.py +0 -989
  19. {npcpy-1.3.13 → npcpy-1.3.14}/LICENSE +0 -0
  20. {npcpy-1.3.13 → npcpy-1.3.14}/MANIFEST.in +0 -0
  21. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/__init__.py +0 -0
  22. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/build_funcs.py +0 -0
  23. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/__init__.py +0 -0
  24. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/audio.py +0 -0
  25. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/data_models.py +0 -0
  26. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/image.py +0 -0
  27. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/load.py +0 -0
  28. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/text.py +0 -0
  29. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/video.py +0 -0
  30. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/data/web.py +0 -0
  31. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/__init__.py +0 -0
  32. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/ge.py +0 -0
  33. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/memory_trainer.py +0 -0
  34. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/model_ensembler.py +0 -0
  35. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/sft.py +0 -0
  36. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ft/usft.py +0 -0
  37. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/__init__.py +0 -0
  38. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/audio_gen.py +0 -0
  39. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/embeddings.py +0 -0
  40. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/image_gen.py +0 -0
  41. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/ocr.py +0 -0
  42. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/response.py +0 -0
  43. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/video_gen.py +0 -0
  44. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/gen/world_gen.py +0 -0
  45. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/llm_funcs.py +0 -0
  46. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/main.py +0 -0
  47. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/__init__.py +0 -0
  48. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/command_history.py +0 -0
  49. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/kg_vis.py +0 -0
  50. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/knowledge_graph.py +0 -0
  51. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/memory_processor.py +0 -0
  52. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/memory/search.py +0 -0
  53. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/mix/__init__.py +0 -0
  54. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/mix/debate.py +0 -0
  55. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/ml_funcs.py +0 -0
  56. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/npc_array.py +0 -0
  57. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/npc_compiler.py +0 -0
  58. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/npc_sysenv.py +0 -0
  59. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/npcs.py +0 -0
  60. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/__init__.py +0 -0
  61. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/ai_function_tools.py +0 -0
  62. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/database_ai_adapters.py +0 -0
  63. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/database_ai_functions.py +0 -0
  64. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/model_runner.py +0 -0
  65. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/npcsql.py +0 -0
  66. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/sql/sql_model_compiler.py +0 -0
  67. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/tools.py +0 -0
  68. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/work/__init__.py +0 -0
  69. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/work/browser.py +0 -0
  70. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/work/desktop.py +0 -0
  71. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/work/plan.py +0 -0
  72. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy/work/trigger.py +0 -0
  73. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy.egg-info/dependency_links.txt +0 -0
  74. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy.egg-info/requires.txt +0 -0
  75. {npcpy-1.3.13 → npcpy-1.3.14}/npcpy.egg-info/top_level.txt +0 -0
  76. {npcpy-1.3.13 → npcpy-1.3.14}/setup.cfg +0 -0
  77. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_audio.py +0 -0
  78. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_image.py +0 -0
  79. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_llm_funcs.py +0 -0
  80. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_npc_array.py +0 -0
  81. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_npc_compiler.py +0 -0
  82. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_npcsql.py +0 -0
  83. {npcpy-1.3.13 → npcpy-1.3.14}/tests/test_response.py +0 -0
  84. {npcpy-1.3.13 → 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.13
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
@@ -51,6 +51,7 @@ from npcpy.memory.command_history import setup_chroma_db
51
51
  from npcpy.memory.search import execute_rag_command, execute_brainblast_command
52
52
  from npcpy.data.load import load_file_contents
53
53
  from npcpy.data.web import search_web
54
+ from npcpy.data.image import capture_screenshot
54
55
 
55
56
 
56
57
  import base64
@@ -1008,7 +1009,7 @@ def get_attachment(attachment_id):
1008
1009
  @app.route("/api/capture_screenshot", methods=["GET"])
1009
1010
  def capture():
1010
1011
 
1011
- screenshot = capture_screenshot(None, full=True)
1012
+ screenshot = capture_screenshot(full=True)
1012
1013
 
1013
1014
 
1014
1015
  if not screenshot:
@@ -1895,10 +1896,26 @@ def finetune_diffusers():
1895
1896
  'output_dir': output_dir,
1896
1897
  'epochs': num_epochs,
1897
1898
  'current_epoch': 0,
1899
+ 'current_batch': 0,
1900
+ 'total_batches': 0,
1901
+ 'current_loss': None,
1902
+ 'loss_history': [],
1903
+ 'step': 0,
1898
1904
  'start_time': datetime.datetime.now().isoformat()
1899
1905
  }
1900
1906
  print(f"🌋 Finetuning job {job_id} initialized. Output directory: {output_dir}")
1901
-
1907
+
1908
+ def progress_callback(progress_data):
1909
+ """Callback to update job progress from training loop."""
1910
+ finetune_jobs[job_id]['current_epoch'] = progress_data.get('epoch', 0)
1911
+ finetune_jobs[job_id]['epochs'] = progress_data.get('total_epochs', num_epochs)
1912
+ finetune_jobs[job_id]['current_batch'] = progress_data.get('batch', 0)
1913
+ finetune_jobs[job_id]['total_batches'] = progress_data.get('total_batches', 0)
1914
+ finetune_jobs[job_id]['step'] = progress_data.get('step', 0)
1915
+ finetune_jobs[job_id]['current_loss'] = progress_data.get('loss')
1916
+ if progress_data.get('loss_history'):
1917
+ finetune_jobs[job_id]['loss_history'] = progress_data['loss_history']
1918
+
1902
1919
  def run_training_async():
1903
1920
  print(f"🌋 Finetuning job {job_id}: Starting asynchronous training thread...")
1904
1921
  try:
@@ -1908,16 +1925,15 @@ def finetune_diffusers():
1908
1925
  learning_rate=learning_rate,
1909
1926
  output_model_path=output_dir
1910
1927
  )
1911
-
1928
+
1912
1929
  print(f"🌋 Finetuning job {job_id}: Calling train_diffusion with config: {config}")
1913
- # Assuming train_diffusion might print its own progress or allow callbacks
1914
- # For more granular logging, you'd need to modify train_diffusion itself
1915
1930
  model_path = train_diffusion(
1916
1931
  expanded_images,
1917
1932
  captions,
1918
- config=config
1933
+ config=config,
1934
+ progress_callback=progress_callback
1919
1935
  )
1920
-
1936
+
1921
1937
  finetune_jobs[job_id]['status'] = 'complete'
1922
1938
  finetune_jobs[job_id]['model_path'] = model_path
1923
1939
  finetune_jobs[job_id]['end_time'] = datetime.datetime.now().isoformat()
@@ -1947,21 +1963,32 @@ def finetune_diffusers():
1947
1963
  def finetune_status(job_id):
1948
1964
  if job_id not in finetune_jobs:
1949
1965
  return jsonify({'error': 'Job not found'}), 404
1950
-
1966
+
1951
1967
  job = finetune_jobs[job_id]
1952
-
1968
+
1953
1969
  if job['status'] == 'complete':
1954
1970
  return jsonify({
1971
+ 'status': 'complete',
1955
1972
  'complete': True,
1956
- 'outputPath': job.get('model_path', job['output_dir'])
1973
+ 'outputPath': job.get('model_path', job['output_dir']),
1974
+ 'loss_history': job.get('loss_history', [])
1957
1975
  })
1958
1976
  elif job['status'] == 'error':
1959
- return jsonify({'error': job.get('error_msg', 'Unknown error')})
1960
-
1977
+ return jsonify({
1978
+ 'status': 'error',
1979
+ 'error': job.get('error_msg', 'Unknown error')
1980
+ })
1981
+
1961
1982
  return jsonify({
1962
- 'step': job.get('current_epoch', 0),
1963
- 'total': job['epochs'],
1964
- 'status': 'running'
1983
+ 'status': 'running',
1984
+ 'epoch': job.get('current_epoch', 0),
1985
+ 'total_epochs': job.get('epochs', 0),
1986
+ 'batch': job.get('current_batch', 0),
1987
+ 'total_batches': job.get('total_batches', 0),
1988
+ 'step': job.get('step', 0),
1989
+ 'loss': job.get('current_loss'),
1990
+ 'loss_history': job.get('loss_history', []),
1991
+ 'start_time': job.get('start_time')
1965
1992
  })
1966
1993
 
1967
1994
  @app.route("/api/ml/train", methods=["POST"])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.3.13
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
  ```
@@ -64,6 +64,7 @@ npcpy/work/plan.py
64
64
  npcpy/work/trigger.py
65
65
  tests/test_audio.py
66
66
  tests/test_command_history.py
67
+ tests/test_documentation_examples.py
67
68
  tests/test_image.py
68
69
  tests/test_llm_funcs.py
69
70
  tests/test_load.py
@@ -83,7 +83,7 @@ extra_files = package_files("npcpy/npc_team/")
83
83
 
84
84
  setup(
85
85
  name="npcpy",
86
- version="1.3.13",
86
+ version="1.3.14",
87
87
  packages=find_packages(exclude=["tests*"]),
88
88
  install_requires=base_requirements,
89
89
  extras_require={