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.
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/PKG-INFO +1 -1
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/core.py +93 -56
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/PKG-INFO +1 -1
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/test.py +27 -8
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.env.copy +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/FUNDING.yml +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/scripts/update_wrapper_version.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/workflows/dev.yml +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/.github/workflows/publish.yml +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/LICENSE +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/MANIFEST.in +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/README.md +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/VERSION +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/README.md +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/llm_to_json/__init__.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/pyproject.toml +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/packages/test.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/__init__.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/cli.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/__init__.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/azure_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/claude_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/google_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/grok_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/groq_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/hugging_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/lmstudio_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/local_http_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/ollama_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/openai_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/drivers/openrouter_driver.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/runner.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/settings.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/tools.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture/validator.py +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/SOURCES.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/dependency_links.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/entry_points.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/requires.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/prompture.egg-info/top_level.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/requirements.txt +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/setup.cfg +0 -0
- {prompture-0.0.26.dev2 → prompture-0.0.26.dev4}/setup.py +0 -0
|
@@ -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
|
|
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
|
|
446
|
+
# Create field schema that expects a direct value rather than a dict
|
|
475
447
|
field_schema = {
|
|
476
|
-
"value":
|
|
477
|
-
|
|
478
|
-
field_info.
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
555
|
-
|
|
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
|
-
#
|
|
558
|
-
|
|
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":
|
|
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
|
-
#
|
|
566
|
-
|
|
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
|
-
})(
|
|
589
|
+
})(result)
|
|
570
590
|
except Exception as e:
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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)
|
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|