aura-state 0.2.0__py3-none-any.whl

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.
aura_state/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ """
2
+ Aura-State: LLM State Machine Compiler.
3
+
4
+ Usage:
5
+ from aura_state import AuraEngine, Node, CompiledTransition
6
+ """
7
+ from .core.engine import AuraEngine, Node, CompiledTransition
8
+ from .execution.tracer import AuraTrace
9
+ from .compiler.dspy_tuner import BootstrapTeleprompter
10
+ from .execution.sandbox import SandboxedInterpreter, SandboxExecutionError
11
+ from .memory.pruner import ContextPruner
12
+ from .consensus.auto_vote import ConsensusStrategy
13
+ from .core.exceptions import AuraStateError, StateTransitionError, MaxRetriesExceededError
14
+ from .loaders.json_graph import JSONGraphLoader
15
+
16
+ # ── Core Innovations ──
17
+ from .core.adaptive_graph import AdaptiveDAG, NodeHealthMetrics, EdgeStats
18
+ from .core.verification_loop import VerificationLoop, ReflectionMemory, Reflection
19
+ from .core.providers import LLMProvider, CostTracker
20
+ from .compiler.schema_compiler import compile_schema, compile_openapi_schemas, levenshtein_distance
21
+
22
+ from .verification.temporal_verifier import (
23
+ compile_kripke, verify_engine, verify_property,
24
+ reachability, always_before, mutual_exclusion, eventual_completion,
25
+ PropertyResult, VerificationResult,
26
+ )
27
+ from .verification.conformal import (
28
+ conformal_interval, conformal_from_extractions,
29
+ PredictionInterval, ConformalResult,
30
+ )
31
+ from .verification.proof_engine import (
32
+ prove_extraction, prove_consistency, ProofResult,
33
+ prove_obligations_satisfiable, field_bounds_from_model, SatResult,
34
+ )
35
+ from .verification.pipeline_conformal import PipelineConformal
36
+ from .compiler.spec_compiler import (
37
+ compile_contract, diff_contracts, check_faithfulness,
38
+ AuraContract, NodeContract, PropertyVerdict, TaintContract, TaintPath, ContractError,
39
+ )
40
+ from .verification.taint import (
41
+ analyze_taint, TaintResult, TaintViolation,
42
+ analyze_field_taint, FieldTaintResult, FieldTaintViolation,
43
+ )
44
+ from .verification.risk_control import RiskController, learn_then_test
45
+ from .core.replan import (
46
+ counterexample_guided_repair, default_repair,
47
+ ctl_to_repair, z3_to_repair, taint_to_repair,
48
+ RepairSignal, ReplanResult, RepairIteration,
49
+ )
50
+
51
+ __all__ = [
52
+ "AuraEngine",
53
+ "Node",
54
+ "CompiledTransition",
55
+ "AuraTrace",
56
+ "BootstrapTeleprompter",
57
+ "AdaptiveDAG",
58
+ "NodeHealthMetrics",
59
+ "EdgeStats",
60
+ "VerificationLoop",
61
+ "ReflectionMemory",
62
+ "Reflection",
63
+ "LLMProvider",
64
+ "CostTracker",
65
+ "compile_schema",
66
+ "compile_openapi_schemas",
67
+ "levenshtein_distance",
68
+ "ContextPruner",
69
+ "ConsensusStrategy",
70
+ "SandboxedInterpreter",
71
+ "SandboxExecutionError",
72
+ "JSONGraphLoader",
73
+ "AuraStateError",
74
+ "StateTransitionError",
75
+ "MaxRetriesExceededError",
76
+ # Formal Verification
77
+ "compile_kripke",
78
+ "verify_engine",
79
+ "verify_property",
80
+ "reachability",
81
+ "always_before",
82
+ "mutual_exclusion",
83
+ "eventual_completion",
84
+ "PropertyResult",
85
+ "VerificationResult",
86
+ "conformal_interval",
87
+ "conformal_from_extractions",
88
+ "PredictionInterval",
89
+ "ConformalResult",
90
+ "prove_extraction",
91
+ "prove_consistency",
92
+ "ProofResult",
93
+ "prove_obligations_satisfiable",
94
+ "field_bounds_from_model",
95
+ "SatResult",
96
+ "PipelineConformal",
97
+ # Design→spec compiler
98
+ "compile_contract",
99
+ "diff_contracts",
100
+ "check_faithfulness",
101
+ "AuraContract",
102
+ "NodeContract",
103
+ "PropertyVerdict",
104
+ "TaintContract",
105
+ "TaintPath",
106
+ "ContractError",
107
+ # Capability-typed dataflow (injection-proof)
108
+ "analyze_taint",
109
+ "TaintResult",
110
+ "TaintViolation",
111
+ "analyze_field_taint",
112
+ "FieldTaintResult",
113
+ "FieldTaintViolation",
114
+ # Risk-controlled abstention
115
+ "RiskController",
116
+ "learn_then_test",
117
+ # Counterexample-guided replanning
118
+ "counterexample_guided_repair",
119
+ "default_repair",
120
+ "ctl_to_repair",
121
+ "z3_to_repair",
122
+ "taint_to_repair",
123
+ "RepairSignal",
124
+ "ReplanResult",
125
+ "RepairIteration",
126
+ ]
@@ -0,0 +1 @@
1
+ # Compiler sub-package
@@ -0,0 +1,124 @@
1
+ import json
2
+ import logging
3
+ import math
4
+ from typing import Callable, List, Dict, Any, Optional
5
+
6
+ logger = logging.getLogger("aura_state")
7
+
8
+
9
+ def char_stub_embedder(text: str) -> List[float]:
10
+ """Deterministic character-based pseudo-embedding for OFFLINE TESTS ONLY.
11
+
12
+ Ranks by byte value, not semantics -- meaningless for real similarity.
13
+ Never used by default; must be injected explicitly via
14
+ ``BootstrapTeleprompter(embedder=char_stub_embedder)``.
15
+ """
16
+ vec = [0.0] * 256
17
+ for i, char in enumerate(text[:256]):
18
+ vec[i] = float(ord(char))
19
+ mag = math.sqrt(sum(v * v for v in vec))
20
+ if mag > 0:
21
+ vec = [v / mag for v in vec]
22
+ return vec
23
+
24
+
25
+ def _openai_embedder(client, model: str = "text-embedding-3-small") -> Callable[[str], List[float]]:
26
+ """Build a real embedding function backed by an OpenAI client."""
27
+ def embed(text: str) -> List[float]:
28
+ resp = client.embeddings.create(model=model, input=text)
29
+ return list(resp.data[0].embedding)
30
+ return embed
31
+
32
+
33
+ class BootstrapTeleprompter:
34
+ """
35
+ KNN-based few-shot optimizer.
36
+
37
+ Finds the K most similar successful past executions and injects them
38
+ as few-shot demonstrations into the system prompt.
39
+
40
+ An embedder is required to compute similarity. Provide one of:
41
+ - ``embedder``: any ``Callable[[str], List[float]]``
42
+ - ``openai_client``: a real embedding model is used
43
+ If neither is given, embedding fails loud (no meaningless default).
44
+ """
45
+ def __init__(
46
+ self,
47
+ k_neighbors: int = 3,
48
+ embedder: Optional[Callable[[str], List[float]]] = None,
49
+ openai_client: Optional[Any] = None,
50
+ ):
51
+ self.k = k_neighbors
52
+ self.successful_traces: Dict[str, List[Dict[str, Any]]] = {}
53
+ if embedder is not None:
54
+ self._embedder = embedder
55
+ elif openai_client is not None:
56
+ self._embedder = _openai_embedder(openai_client)
57
+ else:
58
+ self._embedder = None
59
+
60
+ def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float:
61
+ dot_product = sum(a * b for a, b in zip(vec1, vec2))
62
+ magnitude1 = math.sqrt(sum(a * a for a in vec1))
63
+ magnitude2 = math.sqrt(sum(b * b for b in vec2))
64
+ if magnitude1 == 0 or magnitude2 == 0:
65
+ return 0.0
66
+ return dot_product / (magnitude1 * magnitude2)
67
+
68
+ def _embed(self, text: str) -> List[float]:
69
+ if self._embedder is None:
70
+ raise RuntimeError(
71
+ "BootstrapTeleprompter has no embedder. Pass embedder=... or "
72
+ "openai_client=... (or char_stub_embedder for offline tests). "
73
+ "The old ord()-based default was removed -- it ranked by byte "
74
+ "value, not meaning."
75
+ )
76
+ return self._embedder(text)
77
+
78
+ def _bootstrap_dataset(self, dataset: List[Dict[str, Any]]):
79
+ """Filters the dataset to keep only successful executions."""
80
+ for trace in dataset:
81
+ if trace.get("success"):
82
+ node_name = trace["node"]
83
+ if node_name not in self.successful_traces:
84
+ self.successful_traces[node_name] = []
85
+
86
+ trace["embedding"] = self._embed(trace["input"])
87
+ self.successful_traces[node_name].append(trace)
88
+
89
+ def optimize_node(self, node_name: str, current_prompt: str, new_user_input: str) -> str:
90
+ """
91
+ Finds the K most similar past successes and appends them
92
+ as few-shot examples to the system prompt.
93
+ """
94
+ if node_name not in self.successful_traces or not self.successful_traces[node_name]:
95
+ return current_prompt
96
+
97
+ # O(N) KNN Search
98
+ query_vec = self._embed(new_user_input)
99
+ scored_traces = []
100
+ for trace in self.successful_traces[node_name]:
101
+ score = self._cosine_similarity(query_vec, trace["embedding"])
102
+ scored_traces.append((score, trace))
103
+
104
+ # Sort by similarity descending, take Top K
105
+ scored_traces.sort(key=lambda x: x[0], reverse=True)
106
+ top_k = [x[1] for x in scored_traces[:self.k]]
107
+
108
+ # Abstract the Few-Shot injection prompt
109
+ few_shot_block = "\n\n--- FEW-SHOT DEMONSTRATIONS ---\n"
110
+ few_shot_block += "Follow the structure of these examples:\n\n"
111
+ for i, trace in enumerate(top_k):
112
+ few_shot_block += f"EXAMPLE {i+1}:\n"
113
+ few_shot_block += f"User Context: {trace['input']}\n"
114
+ few_shot_block += f"Extracted Schema Validation Response:\n{json.dumps(trace['output'], indent=2)}\n\n"
115
+
116
+ optimized_prompt = current_prompt + few_shot_block
117
+ logger.info(f"Injected {len(top_k)} few-shot examples into node '{node_name}'")
118
+ return optimized_prompt
119
+
120
+ def compile(self, dataset: List[Dict[str, Any]]):
121
+ """Loads and indexes the dataset for KNN lookup."""
122
+ self._bootstrap_dataset(dataset)
123
+ total = sum(len(v) for v in self.successful_traces.values())
124
+ logger.info(f"Indexed {total} successful traces across {len(self.successful_traces)} nodes")
@@ -0,0 +1,69 @@
1
+ import json
2
+ from typing import Dict, Type, List, Any
3
+ from pydantic import BaseModel
4
+
5
+ from ..core.engine import Node, CompiledTransition
6
+
7
+
8
+ def _pydantic_to_json_extracts(model: Type[BaseModel]) -> Dict[str, Any]:
9
+ """Converts a Pydantic model into the 'extracts' dict format expected by JSONGraphLoader."""
10
+ extracts = {}
11
+
12
+ type_names = {
13
+ str: "str",
14
+ int: "int",
15
+ float: "float",
16
+ bool: "bool",
17
+ list[str]: "list[str]",
18
+ list[int]: "list[int]"
19
+ }
20
+
21
+ for field_name, model_field in model.model_fields.items():
22
+ field_type_str = type_names.get(model_field.annotation, "str")
23
+ extracts[field_name] = {
24
+ "type": field_type_str,
25
+ "description": model_field.description or ""
26
+ }
27
+
28
+ return extracts
29
+
30
+
31
+ def generate_flow_json(nodes: Dict[str, Type[Node]], transitions: List[CompiledTransition], output_path: str = "flow.json"):
32
+ """
33
+ The execution compiler.
34
+ Translates declarative Python Node classes down into the universal flow.json DAG artifact.
35
+ """
36
+ graph = {
37
+ "nodes": [],
38
+ "edges": []
39
+ }
40
+
41
+ # 1. Compile Nodes
42
+ for name, node_cls in nodes.items():
43
+ node_def = {
44
+ "id": name,
45
+ "system_prompt": node_cls.system_prompt,
46
+ }
47
+
48
+ if node_cls.extracts:
49
+ node_def["extracts"] = _pydantic_to_json_extracts(node_cls.extracts)
50
+
51
+ if node_cls.sandbox_rule:
52
+ node_def["sandbox_rule"] = node_cls.sandbox_rule
53
+
54
+ graph["nodes"].append(node_def)
55
+
56
+ # 2. Compile Edges
57
+ for t in transitions:
58
+ edge = {
59
+ "from": t.from_node.__name__,
60
+ "to": t.to_node.__name__,
61
+ "condition": t.condition
62
+ }
63
+ graph["edges"].append(edge)
64
+
65
+ # 3. Serialize to disk
66
+ with open(output_path, "w") as f:
67
+ json.dump(graph, f, indent=4)
68
+
69
+ return graph
@@ -0,0 +1,221 @@
1
+ """
2
+ Schema Compiler: JSON Schema → Node classes with Pydantic extraction models.
3
+
4
+ Given a JSON Schema or OpenAPI spec, generates Node subclasses with
5
+ auto-generated system prompts and Levenshtein fuzzy matching for field correction.
6
+ """
7
+ import logging
8
+ from typing import Any, Dict, List, Optional, Type
9
+ from pydantic import BaseModel, Field, create_model
10
+
11
+ logger = logging.getLogger("aura_state")
12
+
13
+
14
+ # --- Levenshtein Distance ---
15
+
16
+ def levenshtein_distance(a: str, b: str) -> int:
17
+ """Compute Levenshtein edit distance between two strings."""
18
+ la, lb = len(a), len(b)
19
+ if la == 0:
20
+ return lb
21
+ if lb == 0:
22
+ return la
23
+
24
+ prev = list(range(lb + 1))
25
+ curr = [0] * (lb + 1)
26
+
27
+ for i in range(1, la + 1):
28
+ curr[0] = i
29
+ for j in range(1, lb + 1):
30
+ cost = 0 if a[i - 1] == b[j - 1] else 1
31
+ ins = prev[j] + 1
32
+ delete = curr[j - 1] + 1
33
+ sub = prev[j - 1] + cost
34
+ curr[j] = min(ins, delete, sub)
35
+ prev, curr = curr, prev
36
+
37
+ return prev[lb]
38
+
39
+
40
+ def suggest_field(name: str, available: List[str], max_distance: int = 3) -> Optional[str]:
41
+ """
42
+ Find the closest field name using Levenshtein distance.
43
+ Returns None if no match within max_distance.
44
+ """
45
+ if not available:
46
+ return None
47
+
48
+ best_dist = -1
49
+ best_name = ""
50
+
51
+ for field_name in available:
52
+ d = levenshtein_distance(name.lower(), field_name.lower())
53
+ if best_dist < 0 or d < best_dist:
54
+ best_dist = d
55
+ best_name = field_name
56
+
57
+ if 0 <= best_dist <= max_distance:
58
+ return best_name
59
+ return None
60
+
61
+
62
+ # --- JSON Schema → Pydantic Model ---
63
+
64
+ # JSON Schema types → Python types
65
+ SCHEMA_TYPE_MAP = {
66
+ "string": str,
67
+ "integer": int,
68
+ "number": float,
69
+ "boolean": bool,
70
+ "array": list,
71
+ }
72
+
73
+
74
+ def _resolve_type(prop: Dict[str, Any]) -> type:
75
+ """Resolve a JSON Schema property to a Python type."""
76
+ schema_type = prop.get("type", "string")
77
+
78
+ if schema_type == "array":
79
+ items_type = prop.get("items", {}).get("type", "string")
80
+ inner = SCHEMA_TYPE_MAP.get(items_type, str)
81
+ return List[inner]
82
+
83
+ return SCHEMA_TYPE_MAP.get(schema_type, str)
84
+
85
+
86
+ def compile_pydantic_model(
87
+ model_name: str,
88
+ schema: Dict[str, Any],
89
+ ) -> Type[BaseModel]:
90
+ """
91
+ Compile a JSON Schema object into a Pydantic model.
92
+
93
+ Supports:
94
+ - type mapping (string, integer, number, boolean, array)
95
+ - descriptions → Field(description=...)
96
+ - required fields
97
+ - default values
98
+ - enum constraints
99
+ - minimum/maximum validation
100
+ """
101
+ properties = schema.get("properties", {})
102
+ required = set(schema.get("required", []))
103
+
104
+ fields = {}
105
+ for field_name, prop in properties.items():
106
+ python_type = _resolve_type(prop)
107
+ description = prop.get("description", "")
108
+ default_value = prop.get("default", ...)
109
+
110
+ # Build Field kwargs
111
+ field_kwargs = {"description": description}
112
+
113
+ if "enum" in prop:
114
+ field_kwargs["json_schema_extra"] = {"enum": prop["enum"]}
115
+
116
+ if "minimum" in prop:
117
+ field_kwargs["ge"] = prop["minimum"]
118
+ if "maximum" in prop:
119
+ field_kwargs["le"] = prop["maximum"]
120
+
121
+ if field_name in required:
122
+ fields[field_name] = (python_type, Field(default=..., **field_kwargs))
123
+ elif default_value is not ...:
124
+ fields[field_name] = (python_type, Field(default=default_value, **field_kwargs))
125
+ else:
126
+ fields[field_name] = (Optional[python_type], Field(default=None, **field_kwargs))
127
+
128
+ return create_model(model_name, **fields)
129
+
130
+
131
+ # --- Schema → Node ---
132
+
133
+ def _generate_system_prompt(model_name: str, schema: Dict[str, Any]) -> str:
134
+ """Auto-generate a system prompt from JSON Schema field descriptions."""
135
+ properties = schema.get("properties", {})
136
+ title = schema.get("title", model_name)
137
+ desc = schema.get("description", f"Extract data for {title}")
138
+
139
+ lines = [desc, "", "Extract the following fields from the user input:"]
140
+ for field_name, prop in properties.items():
141
+ field_desc = prop.get("description", "")
142
+ field_type = prop.get("type", "string")
143
+ constraint = ""
144
+
145
+ if "enum" in prop:
146
+ constraint = f" (one of: {', '.join(str(v) for v in prop['enum'])})"
147
+ if "minimum" in prop and "maximum" in prop:
148
+ constraint = f" (between {prop['minimum']} and {prop['maximum']})"
149
+
150
+ lines.append(f"- {field_name} ({field_type}): {field_desc}{constraint}")
151
+
152
+ return "\n".join(lines)
153
+
154
+
155
+ def compile_schema(
156
+ schema: Dict[str, Any],
157
+ node_name: Optional[str] = None,
158
+ ) -> type:
159
+ """
160
+ Compile a JSON Schema into a Node subclass with:
161
+ - Auto-generated Pydantic extraction model
162
+ - Auto-generated system prompt from field descriptions
163
+ - Levenshtein fuzzy matching for field correction
164
+ """
165
+ # Import here to avoid circular dependency
166
+ from ..core.engine import Node
167
+
168
+ name = node_name or schema.get("title", "CompiledNode")
169
+ model = compile_pydantic_model(f"{name}Extraction", schema)
170
+ prompt = _generate_system_prompt(name, schema)
171
+
172
+ expected_fields = list(schema.get("properties", {}).keys())
173
+
174
+ def _handle(self, user_text, extracted_data=None, memory=None):
175
+ if extracted_data:
176
+ data = extracted_data.model_dump()
177
+ # Fuzzy field correction: if LLM returned unexpected key names,
178
+ # try Levenshtein matching to recover
179
+ corrected = {}
180
+ for key, value in data.items():
181
+ if key in expected_fields:
182
+ corrected[key] = value
183
+ else:
184
+ suggestion = suggest_field(key, expected_fields)
185
+ if suggestion:
186
+ logger.info(f"[SchemaCompiler] Fuzzy match: '{key}' → '{suggestion}'")
187
+ corrected[suggestion] = value
188
+ else:
189
+ corrected[key] = value
190
+
191
+ return "END", {"extracted": corrected, "source": "schema_compiled"}
192
+ return "END", {"extracted": {}, "source": "schema_compiled"}
193
+
194
+ node_cls = type(name, (Node,), {
195
+ "system_prompt": prompt,
196
+ "extracts": model,
197
+ "handle": _handle,
198
+ })
199
+
200
+ return node_cls
201
+
202
+
203
+ def compile_openapi_schemas(
204
+ spec: Dict[str, Any],
205
+ ) -> List[type]:
206
+ """
207
+ Compile all schemas from an OpenAPI spec into Node subclasses.
208
+
209
+ Each schema in components/schemas becomes a Node with an
210
+ auto-generated extraction model and system prompt.
211
+ """
212
+ schemas = spec.get("components", {}).get("schemas", {})
213
+ nodes = []
214
+
215
+ for name, schema in schemas.items():
216
+ if schema.get("type") == "object" and "properties" in schema:
217
+ node = compile_schema(schema, node_name=name)
218
+ nodes.append(node)
219
+ logger.info(f"[SchemaCompiler] Compiled OpenAPI schema '{name}' → Node class")
220
+
221
+ return nodes