context-compiler 0.9.0.dev10__tar.gz → 0.9.0.dev12__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 (37) hide show
  1. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/PKG-INFO +1 -1
  2. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/pyproject.toml +1 -1
  3. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/engine.py +11 -2
  4. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/grammar.py +98 -48
  5. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/.gitignore +0 -0
  6. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/LICENSE +0 -0
  7. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/README.md +0 -0
  8. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/01_llm_contradiction_error.py +0 -0
  9. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/02_llm_constraint_guardrail.py +0 -0
  10. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/03_llm_premise_guardrail.py +0 -0
  11. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/04_llm_tool_denylist_guardrail.py +0 -0
  12. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/05_llm_prompt_drift_vs_state.py +0 -0
  13. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/06_llm_context_compaction.py +0 -0
  14. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/07_llm_prompt_vs_state.py +0 -0
  15. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/08_llm_replacement_precondition.py +0 -0
  16. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/09_llm_confirmation_no_directive.py +0 -0
  17. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/README.md +0 -0
  18. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/__init__.py +0 -0
  19. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/common.py +0 -0
  20. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/llm_client.py +0 -0
  21. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/demos/run_demo.py +0 -0
  22. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/01_persistent_guardrails.py +0 -0
  23. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/02_configuration_and_correction.py +0 -0
  24. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/03_ambiguity_with_error.py +0 -0
  25. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/04_tool_governance_denylist.py +0 -0
  26. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/05_llm_integration_pattern.py +0 -0
  27. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/06_step_sequence_and_state_restore.py +0 -0
  28. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/07_single_policy_correction.py +0 -0
  29. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/README.md +0 -0
  30. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/examples/_util.py +0 -0
  31. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/host_support/__init__.py +0 -0
  32. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/host_support/provider_mode.py +0 -0
  33. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/__init__.py +0 -0
  34. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/const.py +0 -0
  35. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/decision_helpers.py +0 -0
  36. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/py.typed +0 -0
  37. {context_compiler-0.9.0.dev10 → context_compiler-0.9.0.dev12}/src/context_compiler/repl.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: context-compiler
3
- Version: 0.9.0.dev10
3
+ Version: 0.9.0.dev12
4
4
  Summary: Deterministic conversational state engine for LLM applications.
5
5
  Project-URL: Homepage, https://github.com/rlippmann/context-compiler
6
6
  Project-URL: Repository, https://github.com/rlippmann/context-compiler
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "context-compiler"
7
- version = "0.9.0dev10"
7
+ version = "0.9.0dev12"
8
8
  description = "Deterministic conversational state engine for LLM applications."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -276,7 +276,12 @@ def _load_state_obj(raw: object) -> _State:
276
276
  if not isinstance(policies, dict):
277
277
  raise ValueError("Invalid state payload.")
278
278
 
279
+ sanitized_premise = None if premise is None else _sanitize_premise_value(premise)
280
+ if premise is not None and sanitized_premise == "":
281
+ raise ValueError("Invalid state payload.")
282
+
279
283
  normalized_policies: dict[str, PolicyValue] = {}
284
+ seen_policy_sources: dict[str, str] = {}
280
285
  for key, value in policies.items():
281
286
  if not isinstance(key, str):
282
287
  raise ValueError("Invalid state payload.")
@@ -285,10 +290,14 @@ def _load_state_obj(raw: object) -> _State:
285
290
  normalized_key = _normalize_item(key)
286
291
  if normalized_key == "":
287
292
  raise ValueError("Invalid state payload.")
293
+ prior_source = seen_policy_sources.get(normalized_key)
294
+ if prior_source is not None and prior_source != key:
295
+ raise ValueError("Invalid state payload.")
296
+ seen_policy_sources[normalized_key] = key
288
297
  normalized_policies[normalized_key] = value
289
298
 
290
299
  return {
291
- STATE_PREMISE: None if premise is None else _sanitize_premise_value(premise),
300
+ STATE_PREMISE: sanitized_premise,
292
301
  STATE_POLICIES: dict(sorted(normalized_policies.items())),
293
302
  STATE_VERSION: SCHEMA_VERSION,
294
303
  }
@@ -303,7 +312,7 @@ def _sanitize_premise_value(value: str) -> str:
303
312
  def _normalize_item(value: str) -> str:
304
313
  normalized = unicode_normalize("NFKC", value)
305
314
  normalized = normalized.replace("’", "'").replace("`", "'")
306
- normalized = normalized.lower()
315
+ normalized = normalized.casefold()
307
316
  normalized = re.sub(r"\s+", " ", normalized).strip()
308
317
  return normalized
309
318
 
@@ -1,7 +1,7 @@
1
1
  """Immutable canonical grammar helpers for Context Compiler directives."""
2
2
 
3
3
  import re
4
- from collections.abc import Callable
4
+ from collections.abc import Callable, Mapping
5
5
  from dataclasses import dataclass
6
6
  from enum import StrEnum
7
7
  from types import MappingProxyType
@@ -33,14 +33,24 @@ class DirectiveSyntaxFailure(StrEnum):
33
33
  class CanonicalDirective:
34
34
  """Represent one parsed canonical directive and its named operands.
35
35
 
36
- ``text`` preserves the original accepted input text. It may retain caller
37
- formatting or casing and is not canonical serialized directive text.
36
+ ``text`` is the canonical serialized directive text derived from ``kind``
37
+ and ``operands``.
38
38
  """
39
39
 
40
- text: str
41
40
  kind: DirectiveKind
42
41
  operands: MappingProxyType[str, str]
43
42
 
43
+ def __post_init__(self) -> None:
44
+ normalized_kind = _normalize_directive_kind(self.kind)
45
+ normalized_operands = _normalize_canonical_operands(normalized_kind, self.operands)
46
+ object.__setattr__(self, "kind", normalized_kind)
47
+ object.__setattr__(self, "operands", MappingProxyType(normalized_operands))
48
+
49
+ @property
50
+ def text(self) -> str:
51
+ """Return the canonical serialized directive text."""
52
+ return _serialize_canonical_directive(self.kind, self.operands)
53
+
44
54
 
45
55
  @dataclass(frozen=True, slots=True)
46
56
  class InvalidDirectiveSyntax:
@@ -365,7 +375,6 @@ def _parse_replace_use(trimmed_text: str) -> CanonicalDirective | None:
365
375
  if normalized_payload.count(_INSTEAD_OF_DELIMITER) != 1:
366
376
  return None
367
377
  return CanonicalDirective(
368
- text=trimmed_text,
369
378
  kind=DirectiveKind.REPLACE_USE,
370
379
  operands=MappingProxyType({"new_item": new_item, "old_item": old_item}),
371
380
  )
@@ -384,6 +393,74 @@ def _invalid_directive_syntax(
384
393
  )
385
394
 
386
395
 
396
+ def _normalize_directive_kind(kind: DirectiveKind | str) -> DirectiveKind:
397
+ try:
398
+ return kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
399
+ except ValueError as exc:
400
+ raise ValueError(f"Unsupported directive kind: {kind!r}") from exc
401
+
402
+
403
+ def _normalize_canonical_operands(
404
+ kind: DirectiveKind,
405
+ operands: Mapping[str, str],
406
+ ) -> dict[str, str]:
407
+ spec = _DIRECTIVE_SPECS[kind]
408
+ expected_names = set(spec.operand_names)
409
+ actual_names = set(operands)
410
+ unexpected_names = actual_names - expected_names
411
+ missing_names = expected_names - actual_names
412
+ if missing_names:
413
+ missing = ", ".join(sorted(missing_names))
414
+ raise ValueError(f"Missing required operands for {kind.value}: {missing}")
415
+ if unexpected_names:
416
+ unexpected = ", ".join(sorted(unexpected_names))
417
+ raise ValueError(f"Unexpected operands for {kind.value}: {unexpected}")
418
+
419
+ normalized_operands: dict[str, str] = {}
420
+ for name in spec.operand_names:
421
+ raw_value = operands[name]
422
+ if not isinstance(raw_value, str):
423
+ raise ValueError(f"Operand {name!r} for {kind.value} must be a string.")
424
+ if not _operand_has_content(raw_value):
425
+ raise ValueError(f"Operand {name!r} for {kind.value} cannot be empty.")
426
+ normalized_operands[name] = raw_value
427
+
428
+ _validate_operand_constraints(kind, normalized_operands)
429
+ _validate_rendered_canonical_shape(kind, normalized_operands)
430
+ return normalized_operands
431
+
432
+
433
+ def _validate_operand_constraints(kind: DirectiveKind, operands: Mapping[str, str]) -> None:
434
+ if kind is DirectiveKind.SET_PREMISE:
435
+ value = operands["value"]
436
+ if _operand_starts_with_token(value, "to"):
437
+ raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
438
+ return
439
+
440
+ if kind is DirectiveKind.USE_ITEM:
441
+ item = operands["item"]
442
+ normalized_item = _normalized_for_matching(item)
443
+ if normalized_item == "instead of" or normalized_item.startswith("instead of "):
444
+ raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
445
+ if _INSTEAD_OF_DELIMITER in normalized_item:
446
+ raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
447
+ return
448
+
449
+ if kind is DirectiveKind.REPLACE_USE:
450
+ new_item = operands["new_item"]
451
+ old_item = operands["old_item"]
452
+ if _INSTEAD_OF_DELIMITER in _normalized_for_matching(
453
+ new_item
454
+ ) or _INSTEAD_OF_DELIMITER in _normalized_for_matching(old_item):
455
+ raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
456
+
457
+
458
+ def _validate_rendered_canonical_shape(kind: DirectiveKind, operands: Mapping[str, str]) -> None:
459
+ rendered = _serialize_canonical_directive(kind, MappingProxyType(dict(operands)))
460
+ if _contains_multiple_canonical_directives(rendered):
461
+ raise ValueError(f"Operands do not produce a canonical {kind.value} directive.")
462
+
463
+
387
464
  def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSyntax | None:
388
465
  """Parse one canonical directive into its semantic kind and operands.
389
466
 
@@ -405,19 +482,14 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
405
482
  normalized = _normalized_for_matching(trimmed_text)
406
483
 
407
484
  if normalized == _CLEAR_PREMISE_TEXT:
408
- return CanonicalDirective(
409
- text=text, kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({})
410
- )
485
+ return CanonicalDirective(kind=DirectiveKind.CLEAR_PREMISE, operands=MappingProxyType({}))
411
486
  if normalized == _RESET_POLICIES_TEXT:
412
487
  return CanonicalDirective(
413
- text=text,
414
488
  kind=DirectiveKind.RESET_POLICIES,
415
489
  operands=MappingProxyType({}),
416
490
  )
417
491
  if normalized == _CLEAR_STATE_TEXT:
418
- return CanonicalDirective(
419
- text=text, kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({})
420
- )
492
+ return CanonicalDirective(kind=DirectiveKind.CLEAR_STATE, operands=MappingProxyType({}))
421
493
 
422
494
  if normalized == "set premise":
423
495
  return _invalid_directive_syntax(
@@ -440,7 +512,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
440
512
  directive_kind=DirectiveKind.SET_PREMISE,
441
513
  )
442
514
  return CanonicalDirective(
443
- text=text,
444
515
  kind=DirectiveKind.SET_PREMISE,
445
516
  operands=MappingProxyType({"value": value}),
446
517
  )
@@ -468,7 +539,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
468
539
  missing_operand="value",
469
540
  )
470
541
  return CanonicalDirective(
471
- text=text,
472
542
  kind=DirectiveKind.CHANGE_PREMISE,
473
543
  operands=MappingProxyType({"value": value}),
474
544
  )
@@ -518,7 +588,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
518
588
  directive_kind=DirectiveKind.USE_ITEM,
519
589
  )
520
590
  return CanonicalDirective(
521
- text=text,
522
591
  kind=DirectiveKind.USE_ITEM,
523
592
  operands=MappingProxyType({"item": item}),
524
593
  )
@@ -546,7 +615,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
546
615
  missing_operand="item",
547
616
  )
548
617
  return CanonicalDirective(
549
- text=text,
550
618
  kind=DirectiveKind.PROHIBIT_ITEM,
551
619
  operands=MappingProxyType({"item": item}),
552
620
  )
@@ -574,7 +642,6 @@ def decompose_directive(text: str) -> CanonicalDirective | InvalidDirectiveSynta
574
642
  missing_operand="item",
575
643
  )
576
644
  return CanonicalDirective(
577
- text=text,
578
645
  kind=DirectiveKind.REMOVE_POLICY,
579
646
  operands=MappingProxyType({"item": item}),
580
647
  )
@@ -588,40 +655,23 @@ def get_directive_metadata() -> tuple[DirectiveMetadata, ...]:
588
655
  return _PUBLIC_DIRECTIVE_METADATA
589
656
 
590
657
 
591
- def _render_directive(kind: DirectiveKind | str, /, **operands: str) -> str:
592
- """Produce canonical directive text from a semantic kind and operands."""
593
- try:
594
- normalized_kind = kind if isinstance(kind, DirectiveKind) else DirectiveKind(kind)
595
- spec = _DIRECTIVE_SPECS[normalized_kind]
596
- except (KeyError, ValueError) as exc:
597
- raise ValueError(f"Unsupported directive kind: {kind!r}") from exc
658
+ def _serialize_canonical_directive(
659
+ kind: DirectiveKind | str, operands: MappingProxyType[str, str]
660
+ ) -> str:
661
+ """Serialize a validated semantic directive without reparsing it."""
662
+ normalized_kind = _normalize_directive_kind(kind)
663
+ spec = _DIRECTIVE_SPECS[normalized_kind]
598
664
 
599
- expected_names = set(spec.operand_names)
600
- actual_names = set(operands)
601
- unexpected_names = actual_names - expected_names
602
- missing_names = expected_names - actual_names
603
- if missing_names:
604
- missing = ", ".join(sorted(missing_names))
605
- raise ValueError(f"Missing required operands for {normalized_kind.value}: {missing}")
606
- if unexpected_names:
607
- unexpected = ", ".join(sorted(unexpected_names))
608
- raise ValueError(f"Unexpected operands for {normalized_kind.value}: {unexpected}")
665
+ return spec.renderer(operands)
609
666
 
610
- normalized_operands: dict[str, str] = {}
611
- for name in spec.operand_names:
612
- raw_value = operands[name]
613
- if not isinstance(raw_value, str):
614
- raise ValueError(f"Operand {name!r} for {normalized_kind.value} must be a string.")
615
- if raw_value.strip() == "":
616
- raise ValueError(f"Operand {name!r} for {normalized_kind.value} cannot be empty.")
617
- normalized_operands[name] = raw_value
618
667
 
619
- operand_view = MappingProxyType(normalized_operands)
620
- rendered = spec.renderer(operand_view)
621
- decomposed = decompose_directive(rendered)
622
- if not isinstance(decomposed, CanonicalDirective) or decomposed.kind is not normalized_kind:
623
- raise ValueError(f"Operands do not produce a canonical {normalized_kind.value} directive.")
624
- return rendered
668
+ def _render_directive(kind: DirectiveKind | str, /, **operands: str) -> str:
669
+ """Produce canonical directive text from a semantic kind and operands."""
670
+ normalized_kind = _normalize_directive_kind(kind)
671
+ return CanonicalDirective(
672
+ kind=normalized_kind,
673
+ operands=MappingProxyType(dict(operands)),
674
+ ).text
625
675
 
626
676
 
627
677
  __all__ = [