prompture 0.0.26.dev2__tar.gz → 0.0.26.dev4__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 (44) hide show
  1. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/PKG-INFO +1 -1
  2. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/core.py +93 -56
  3. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/PKG-INFO +1 -1
  4. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/test.py +27 -8
  5. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.env.copy +0 -0
  6. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/FUNDING.yml +0 -0
  7. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/scripts/update_wrapper_version.py +0 -0
  8. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/workflows/dev.yml +0 -0
  9. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/workflows/publish.yml +0 -0
  10. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/LICENSE +0 -0
  11. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/MANIFEST.in +0 -0
  12. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/README.md +0 -0
  13. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/VERSION +0 -0
  14. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/README.md +0 -0
  15. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/llm_to_json/__init__.py +0 -0
  16. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/pyproject.toml +0 -0
  17. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/test.py +0 -0
  18. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/__init__.py +0 -0
  19. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/cli.py +0 -0
  20. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/driver.py +0 -0
  21. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/__init__.py +0 -0
  22. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/azure_driver.py +0 -0
  23. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/claude_driver.py +0 -0
  24. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/google_driver.py +0 -0
  25. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/grok_driver.py +0 -0
  26. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/groq_driver.py +0 -0
  27. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/hugging_driver.py +0 -0
  28. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/lmstudio_driver.py +0 -0
  29. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/local_http_driver.py +0 -0
  30. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/ollama_driver.py +0 -0
  31. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/openai_driver.py +0 -0
  32. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/openrouter_driver.py +0 -0
  33. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/runner.py +0 -0
  34. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/settings.py +0 -0
  35. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/tools.py +0 -0
  36. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/validator.py +0 -0
  37. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/SOURCES.txt +0 -0
  38. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/dependency_links.txt +0 -0
  39. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/entry_points.txt +0 -0
  40. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/requires.txt +0 -0
  41. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/top_level.txt +0 -0
  42. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/requirements.txt +0 -0
  43. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/setup.cfg +0 -0
  44. {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.26.dev2
3
+ Version: 0.0.26.dev4
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Home-page: https://github.com/jhd3197/prompture
6
6
  Author: Juan Denis
@@ -6,10 +6,12 @@ import re
6
6
  import requests
7
7
  import sys
8
8
  import warnings
9
+ from datetime import datetime, date
10
+ from decimal import Decimal
9
11
  from typing import Any, Dict, Type, Optional, List, Union
10
12
  import pytest
11
13
 
12
- from pydantic import BaseModel
14
+ from pydantic import BaseModel, Field
13
15
 
14
16
  from .drivers import get_driver, get_driver_for_model
15
17
  from .driver import Driver
@@ -121,37 +123,6 @@ def ask_for_json(
121
123
  # Explicitly re-raise the original JSONDecodeError
122
124
  raise e
123
125
 
124
- def _old_extract_and_jsonify(
125
- driver: Driver,
126
- text: str,
127
- json_schema: Dict[str, Any],
128
- model_name: str = "",
129
- instruction_template: str = "Extract information from the following text:",
130
- ai_cleanup: bool = True,
131
- options: Dict[str, Any] = {},
132
- ) -> Dict[str, Any]:
133
- """[DEPRECATED] Legacy version of extract_and_jsonify that takes an explicit driver.
134
-
135
- Use the new extract_and_jsonify(text, json_schema, model_name, ...) instead.
136
- This version will be removed in a future release.
137
- """
138
- warnings.warn(
139
- "This function is deprecated. Use extract_and_jsonify(text, json_schema, model_name, ...) instead.",
140
- DeprecationWarning,
141
- stacklevel=2
142
- )
143
- import sys
144
-
145
- model = model_name or getattr(driver, "model", "")
146
- return extract_and_jsonify(
147
- text=text,
148
- json_schema=json_schema,
149
- model_name=model,
150
- instruction_template=instruction_template,
151
- ai_cleanup=ai_cleanup,
152
- options={"driver": driver, "model": model, **options}
153
- )
154
-
155
126
  def extract_and_jsonify(
156
127
  text: Union[str, Driver], # Can be either text or driver for backward compatibility
157
128
  json_schema: Dict[str, Any],
@@ -304,7 +275,6 @@ def manual_extract_and_jsonify(
304
275
  log_debug(LogLevel.TRACE, verbose_level, {"result": result}, prefix="[manual]")
305
276
 
306
277
  return result
307
-
308
278
 
309
279
  def extract_with_model(
310
280
  model_cls: Union[Type[BaseModel], str], # Can be model class or model name string for legacy support
@@ -395,11 +365,12 @@ def extract_with_model(
395
365
  def stepwise_extract_with_model(
396
366
  model_cls: Type[BaseModel],
397
367
  text: str,
368
+ *, # Force keyword arguments for remaining params
398
369
  model_name: str,
399
370
  instruction_template: str = "Extract the {field_name} from the following text:",
400
371
  ai_cleanup: bool = True,
401
372
  fields: Optional[List[str]] = None,
402
- options: Dict[str, Any] = {},
373
+ options: Optional[Dict[str, Any]] = None,
403
374
  verbose_level: LogLevel | int = LogLevel.OFF,
404
375
  ) -> Dict[str, Union[str, Dict[str, Any]]]:
405
376
  """Extracts structured information into a Pydantic model by processing each field individually.
@@ -439,6 +410,7 @@ def stepwise_extract_with_model(
439
410
 
440
411
  data = {}
441
412
  validation_errors = []
413
+ options = options or {}
442
414
 
443
415
  # Initialize usage accumulator
444
416
  accumulated_usage = {
@@ -446,7 +418,7 @@ def stepwise_extract_with_model(
446
418
  "completion_tokens": 0,
447
419
  "total_tokens": 0,
448
420
  "cost": 0.0,
449
- "model_name": model_name or getattr(driver, "model", ""),
421
+ "model_name": model_name, # Use provided model_name directly
450
422
  "field_usages": {}
451
423
  }
452
424
 
@@ -471,13 +443,12 @@ def stepwise_extract_with_model(
471
443
  "field_type": str(field_info.annotation)
472
444
  }, prefix="[stepwise]")
473
445
 
474
- # Create field schema using tools.create_field_schema
446
+ # Create field schema that expects a direct value rather than a dict
475
447
  field_schema = {
476
- "value": create_field_schema(
477
- field_name,
478
- field_info.annotation,
479
- field_info.description
480
- )
448
+ "value": {
449
+ "type": "integer" if field_info.annotation == int else "string",
450
+ "description": field_info.description or f"Value for {field_name}"
451
+ }
481
452
  }
482
453
 
483
454
  # Add structured logging for field schema and prompt
@@ -508,12 +479,24 @@ def stepwise_extract_with_model(
508
479
  accumulated_usage["cost"] += field_usage.get("cost", 0.0)
509
480
  accumulated_usage["field_usages"][field_name] = field_usage
510
481
 
482
+ # Extract the raw value from the response - handle both dict and direct value formats
511
483
  extracted_value = result["json_object"]["value"]
484
+ log_debug(LogLevel.DEBUG, verbose_level, f"Raw extracted value for {field_name}", prefix="[stepwise]")
485
+ log_debug(LogLevel.DEBUG, verbose_level, {"extracted_value": extracted_value}, prefix="[stepwise]")
486
+
487
+ if isinstance(extracted_value, dict) and "value" in extracted_value:
488
+ raw_value = extracted_value["value"]
489
+ log_debug(LogLevel.DEBUG, verbose_level, f"Extracted inner value from dict for {field_name}", prefix="[stepwise]")
490
+ else:
491
+ raw_value = extracted_value
492
+ log_debug(LogLevel.DEBUG, verbose_level, f"Using direct value for {field_name}", prefix="[stepwise]")
493
+
494
+ log_debug(LogLevel.DEBUG, verbose_level, {"field_name": field_name, "raw_value": raw_value}, prefix="[stepwise]")
512
495
 
513
- # Convert value using tools.convert_value
496
+ # Convert value using tools.convert_value with logging
514
497
  try:
515
498
  converted_value = convert_value(
516
- extracted_value,
499
+ raw_value,
517
500
  field_info.annotation,
518
501
  allow_shorthand=True
519
502
  )
@@ -536,9 +519,16 @@ def stepwise_extract_with_model(
536
519
  except Exception as e:
537
520
  error_msg = f"Extraction failed for {field_name}: {str(e)}"
538
521
  validation_errors.append(error_msg)
522
+ data[field_name] = None # Store None for failed fields
539
523
 
540
524
  # Add structured logging for extraction error
541
525
  log_debug(LogLevel.ERROR, verbose_level, error_msg, prefix="[stepwise]")
526
+
527
+ # Store error details in field_usages
528
+ accumulated_usage["field_usages"][field_name] = {
529
+ "error": str(e),
530
+ "status": "failed"
531
+ }
542
532
 
543
533
  # Add structured logging for validation errors
544
534
  if validation_errors:
@@ -546,28 +536,75 @@ def stepwise_extract_with_model(
546
536
  for error in validation_errors:
547
537
  log_debug(LogLevel.ERROR, verbose_level, error, prefix="[stepwise]")
548
538
 
539
+ # If there are validation errors, include them in the result
540
+ if validation_errors:
541
+ accumulated_usage["validation_errors"] = validation_errors
542
+
549
543
  try:
544
+ # Create model instance with collected data
545
+ # Create model instance with collected data
550
546
  model_instance = model_cls(**data)
551
- # Convert model to dict for json_object
552
547
  model_dict = model_instance.model_dump()
553
548
 
554
- # Create JSON string from the dict
555
- json_string = json.dumps(model_dict)
549
+ # Enhanced DateTimeEncoder to handle both datetime and date objects
550
+ class ExtendedJSONEncoder(json.JSONEncoder):
551
+ def default(self, obj):
552
+ if isinstance(obj, (datetime, date)):
553
+ return obj.isoformat()
554
+ if isinstance(obj, Decimal):
555
+ return str(obj)
556
+ return super().default(obj)
556
557
 
557
- # Create result dictionary with backwards compatibility
558
- result_dict = {
558
+ # Use enhanced encoder for JSON serialization
559
+ json_string = json.dumps(model_dict, cls=ExtendedJSONEncoder)
560
+
561
+ # Also modify return value to use ExtendedJSONEncoder
562
+ if 'json_string' in result:
563
+ result['json_string'] = json.dumps(result['json_object'], cls=ExtendedJSONEncoder)
564
+
565
+ # Define ExtendedJSONEncoder for handling special types
566
+ class ExtendedJSONEncoder(json.JSONEncoder):
567
+ def default(self, obj):
568
+ if isinstance(obj, (datetime, date)):
569
+ return obj.isoformat()
570
+ if isinstance(obj, Decimal):
571
+ return str(obj)
572
+ return super().default(obj)
573
+
574
+ # Create json string with custom encoder
575
+ json_string = json.dumps(model_dict, cls=ExtendedJSONEncoder)
576
+
577
+ # Create result matching extract_with_model format
578
+ result = {
559
579
  "json_string": json_string,
560
- "json_object": model_dict,
580
+ "json_object": json.loads(json_string), # Re-parse to ensure all values are JSON serializable
561
581
  "usage": accumulated_usage,
562
- "model": model_instance # For backwards compatibility
563
582
  }
564
583
 
565
- # Return value can be used both as a dict and accessed as model directly
566
- return type("StepwiseExtractResult", (dict,), {
584
+ # Add model instance as property and make callable
585
+ result["model"] = model_instance
586
+ return type("ExtractResult", (dict,), {
567
587
  "__getattr__": lambda self, key: self.get(key),
568
588
  "__call__": lambda self: self["model"]
569
- })(result_dict)
589
+ })(result)
570
590
  except Exception as e:
571
- # Add structured logging for model validation error
572
- log_debug(LogLevel.ERROR, verbose_level, f"Model validation error: {str(e)}", prefix="[stepwise]")
573
- raise
591
+ error_msg = f"Model validation error: {str(e)}"
592
+ # Add validation error to accumulated usage
593
+ if "validation_errors" not in accumulated_usage:
594
+ accumulated_usage["validation_errors"] = []
595
+ accumulated_usage["validation_errors"].append(error_msg)
596
+
597
+ # Add structured logging
598
+ log_debug(LogLevel.ERROR, verbose_level, error_msg, prefix="[stepwise]")
599
+
600
+ # Create error result with partial data
601
+ error_result = {
602
+ "json_string": "{}",
603
+ "json_object": {},
604
+ "usage": accumulated_usage,
605
+ "error": error_msg
606
+ }
607
+ return type("ExtractResult", (dict,), {
608
+ "__getattr__": lambda self, key: self.get(key),
609
+ "__call__": lambda self: None # Return None when called if validation failed
610
+ })(error_result)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prompture
3
- Version: 0.0.26.dev2
3
+ Version: 0.0.26.dev4
4
4
  Summary: Ask LLMs to return structured JSON and run cross-model tests. API-first.
5
5
  Home-page: https://github.com/jhd3197/prompture
6
6
  Author: Juan Denis
@@ -3,9 +3,10 @@ import argparse
3
3
  import os
4
4
  import sys
5
5
  from typing import Dict, List, Tuple
6
- import pytest
7
6
  from dotenv import load_dotenv
8
7
  from pathlib import Path
8
+ import re
9
+
9
10
 
10
11
  # Add src directory to path for prompture package imports
11
12
  sys.path.append('src')
@@ -132,19 +133,37 @@ def configure_test_environment(args: argparse.Namespace) -> None:
132
133
  print("Error: Provider credentials missing and skip tests not enabled")
133
134
  sys.exit(1)
134
135
 
136
+
137
+ def read_default_model_from_conftest() -> str:
138
+ path = Path('tests') / 'conftest.py'
139
+ text = path.read_text(encoding='utf-8')
140
+ m = re.search(r"DEFAULT_MODEL\s*=\s*['\"]([^'\"]+)['\"]", text)
141
+ if not m:
142
+ raise RuntimeError("Couldn't locate DEFAULT_MODEL in tests/conftest.py")
143
+ return m.group(1)
144
+
145
+ def configure_test_environment_from_model(model: str, args):
146
+ provider = get_provider_from_model(model)
147
+ if provider not in VALID_PROVIDERS:
148
+ print(f"Error: Invalid provider '{provider}' in DEFAULT_MODEL.")
149
+ sys.exit(1)
150
+ # print diagnostics and credential checks same as before, but take `model` param
151
+
135
152
  def main() -> int:
136
- """Main test runner function."""
137
153
  args = parse_args()
138
-
154
+
155
+ # Import pytest only now, inside main, before any test module import
156
+ import pytest
157
+
139
158
  try:
140
- configure_test_environment(args)
141
-
142
- # Run pytest with any additional arguments
159
+ DEFAULT_MODEL = read_default_model_from_conftest()
160
+ configure_test_environment_from_model(DEFAULT_MODEL, args)
161
+
162
+ # safe to run pytest.main() now because pytest is loaded and its import hooks installed
143
163
  return pytest.main(args.pytest_args)
144
-
164
+
145
165
  except Exception as e:
146
166
  print(f"Error running tests: {e}")
147
167
  return 1
148
-
149
168
  if __name__ == '__main__':
150
169
  sys.exit(main())
File without changes
File without changes