longguard 0.1.1__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.
longguard/__init__.py ADDED
@@ -0,0 +1,127 @@
1
+ """LongGuard — Chain-of-Thought Circuit Breaker for LangGraph / LangChain Agents.
2
+
3
+ LongGuard monitors every step of an LLM agent execution, detects semantic
4
+ reasoning loops, and injects "Reflect & Pivot" prompts to recover — or
5
+ gracefully terminates the agent when recovery fails.
6
+
7
+ Quick start::
8
+
9
+ from longguard import GuardConfig, CircuitBreaker, AgentStep
10
+
11
+ config = GuardConfig(max_tokens_per_run=100_000)
12
+ breaker = CircuitBreaker(config)
13
+
14
+ for step in agent_loop:
15
+ agent_step = AgentStep(
16
+ step_number=i,
17
+ thought=step.thought,
18
+ action=step.tool,
19
+ action_input=step.args,
20
+ observation=step.result,
21
+ tokens_used=step.tokens,
22
+ )
23
+ decision = breaker.check(agent_step)
24
+
25
+ if decision.action == "kill":
26
+ print(f"Agent killed: {decision.reason}")
27
+ break
28
+ elif decision.action == "reflect":
29
+ print(f"Injecting pivot: {decision.inject_prompt}")
30
+ # else: continue normally
31
+
32
+ LangGraph integration (one line)::
33
+
34
+ from longguard.integrations.langgraph import add_guard_to_graph
35
+ workflow = add_guard_to_graph(workflow, GuardConfig())
36
+
37
+ LangChain integration::
38
+
39
+ from longguard.integrations.langchain import GuardedAgentExecutor
40
+ guarded = GuardedAgentExecutor(agent_executor, GuardConfig())
41
+ result = guarded.run("Your query here")
42
+ """
43
+
44
+ __version__ = "0.1.1"
45
+
46
+ from typing import Any
47
+
48
+ from .config import GuardConfig
49
+ from .core.breaker import BreakerDecision, BreakerState, CircuitBreaker
50
+ from .core.detectors import (
51
+ AbstractLoopDetector,
52
+ DeadEndDriftDetector,
53
+ DetectionResult,
54
+ Embedder,
55
+ HashBasedEmbedder,
56
+ SemanticOscillationDetector,
57
+ SentenceTransformerEmbedder,
58
+ TokenVelocityDetector,
59
+ ToolRepeatDetector,
60
+ )
61
+ from .core.pivot import PIVOT_TEMPLATES, ReflectAndPivotInjector
62
+ from .core.reporter import GuardReport
63
+ from .core.step import AgentStep
64
+
65
+ # --- Lazy integration imports ---
66
+ # These are optional and depend on langgraph / langchain being installed.
67
+ # Users who only use the core API should not need those packages.
68
+
69
+ def __getattr__(name: str) -> Any:
70
+ """Lazy-load integration classes on first access."""
71
+ _langgraph_names = {"LongGuard", "add_guard_to_graph"}
72
+ _langchain_names = {"GuardedAgentExecutor", "GuardTerminatedException"}
73
+ _strands_names = {"StrandsGuard", "GuardTerminatedError"}
74
+
75
+ if name in _langgraph_names:
76
+ from .integrations.langgraph import LongGuard, add_guard_to_graph
77
+ return {"LongGuard": LongGuard, "add_guard_to_graph": add_guard_to_graph}[name]
78
+
79
+ if name in _langchain_names:
80
+ from .integrations.langchain import GuardedAgentExecutor, GuardTerminatedException
81
+ return {
82
+ "GuardedAgentExecutor": GuardedAgentExecutor,
83
+ "GuardTerminatedException": GuardTerminatedException,
84
+ }[name]
85
+
86
+ if name in _strands_names:
87
+ from .integrations.strands import GuardTerminatedError, StrandsGuard
88
+ return {
89
+ "StrandsGuard": StrandsGuard,
90
+ "GuardTerminatedError": GuardTerminatedError,
91
+ }[name]
92
+
93
+ raise AttributeError(f"module 'longguard' has no attribute {name!r}")
94
+
95
+
96
+ __all__ = [
97
+ # Config
98
+ "GuardConfig",
99
+ # Core models
100
+ "AgentStep",
101
+ # Detectors
102
+ "AbstractLoopDetector",
103
+ "DetectionResult",
104
+ "Embedder",
105
+ "ToolRepeatDetector",
106
+ "SemanticOscillationDetector",
107
+ "DeadEndDriftDetector",
108
+ "TokenVelocityDetector",
109
+ "HashBasedEmbedder",
110
+ "SentenceTransformerEmbedder",
111
+ # Circuit breaker
112
+ "CircuitBreaker",
113
+ "BreakerState",
114
+ "BreakerDecision",
115
+ # Pivot
116
+ "ReflectAndPivotInjector",
117
+ "PIVOT_TEMPLATES",
118
+ # Reporting
119
+ "GuardReport",
120
+ # Integrations (lazy-loaded)
121
+ "LongGuard",
122
+ "add_guard_to_graph",
123
+ "GuardedAgentExecutor",
124
+ "GuardTerminatedException",
125
+ "StrandsGuard",
126
+ "GuardTerminatedError",
127
+ ]
longguard/config.py ADDED
@@ -0,0 +1,133 @@
1
+ """LongGuard configuration dataclass."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class GuardConfig:
11
+ """Configuration for LongGuard circuit breaker.
12
+
13
+ All parameters have sensible defaults. Override only what you need.
14
+
15
+ Attributes:
16
+ tool_repeat_threshold: Number of identical tool calls that triggers detection.
17
+ tool_repeat_window: Sliding window size (in steps) for tool repeat detection.
18
+ semantic_variance_threshold: Variance below which thoughts are considered oscillating.
19
+ semantic_window: Number of recent thoughts to analyze for oscillation.
20
+ dead_end_threshold: Consecutive no-progress steps before dead-end detection.
21
+ dead_end_progress_threshold: Similarity threshold below which observations
22
+ are considered to have made progress (lower = more different = progress).
23
+ When ``sentence-transformers`` is installed, cosine similarity on
24
+ embeddings is used. Otherwise, Jaccard similarity is used as fallback.
25
+ token_velocity_multiplier: Multiplier over baseline velocity that triggers a spike.
26
+ token_velocity_window: Sliding window for computing rolling token velocity.
27
+ token_velocity_warmup: Number of steps before velocity baseline is established.
28
+ max_tokens_per_run: Hard token budget cap per agent run.
29
+ max_steps: Hard step cap per agent run.
30
+ max_reflections: Number of reflection attempts before hard kill.
31
+ pivot_templates: Custom pivot prompt templates. Keys must be one of:
32
+ "tool_repeat", "semantic_oscillation", "dead_end_drift", "token_velocity".
33
+ Values are strings with template variables in {curly_braces}.
34
+ log_level: Logging level for LongGuard internals.
35
+ emit_events: Whether to emit events for LangSmith / custom callbacks.
36
+ """
37
+
38
+ # Loop detection thresholds
39
+ tool_repeat_threshold: int = 3
40
+ tool_repeat_window: int = 6
41
+ semantic_variance_threshold: float = 0.15
42
+ semantic_window: int = 8
43
+ dead_end_threshold: int = 5
44
+ dead_end_progress_threshold: float = 0.6
45
+ token_velocity_multiplier: float = 3.0
46
+ token_velocity_window: int = 5
47
+ token_velocity_warmup: int = 3
48
+
49
+ # Budget caps
50
+ max_tokens_per_run: int = 50_000
51
+ max_steps: int = 30
52
+
53
+ # Reflection settings
54
+ max_reflections: int = 2
55
+ pivot_templates: dict[str, str] = field(default_factory=dict)
56
+
57
+ # Monitoring
58
+ log_level: str = "WARNING"
59
+ emit_events: bool = True
60
+
61
+ def __post_init__(self) -> None:
62
+ """Validate configuration values."""
63
+ if self.tool_repeat_threshold < 1:
64
+ raise ValueError("tool_repeat_threshold must be >= 1")
65
+ if self.tool_repeat_window < self.tool_repeat_threshold:
66
+ raise ValueError("tool_repeat_window must be >= tool_repeat_threshold")
67
+ if self.semantic_variance_threshold <= 0:
68
+ raise ValueError("semantic_variance_threshold must be > 0")
69
+ if self.semantic_window < 2:
70
+ raise ValueError("semantic_window must be >= 2")
71
+ if self.dead_end_threshold < 1:
72
+ raise ValueError("dead_end_threshold must be >= 1")
73
+ if self.token_velocity_multiplier <= 1.0:
74
+ raise ValueError("token_velocity_multiplier must be > 1.0")
75
+ if self.token_velocity_window < 2:
76
+ raise ValueError("token_velocity_window must be >= 2")
77
+ if self.max_tokens_per_run < 1:
78
+ raise ValueError("max_tokens_per_run must be >= 1")
79
+ if self.max_steps < 1:
80
+ raise ValueError("max_steps must be >= 1")
81
+ if self.max_reflections < 1:
82
+ raise ValueError("max_reflections must be >= 1")
83
+
84
+ @classmethod
85
+ def from_dict(cls, data: dict[str, Any]) -> GuardConfig:
86
+ """Create a GuardConfig from a dictionary.
87
+
88
+ Unknown keys are silently ignored so that config files can contain
89
+ extra metadata without breaking parsing.
90
+
91
+ Args:
92
+ data: Dictionary of configuration values.
93
+
94
+ Returns:
95
+ A GuardConfig instance.
96
+
97
+ Example::
98
+
99
+ config = GuardConfig.from_dict({
100
+ "tool_repeat_threshold": 5,
101
+ "max_tokens_per_run": 100_000,
102
+ })
103
+ """
104
+ valid_keys = {f.name for f in cls.__dataclass_fields__.values()}
105
+ filtered = {k: v for k, v in data.items() if k in valid_keys}
106
+ return cls(**filtered)
107
+
108
+ def to_dict(self) -> dict[str, Any]:
109
+ """Serialize the config to a dictionary."""
110
+ import dataclasses
111
+
112
+ return dataclasses.asdict(self)
113
+
114
+ def merge(self, other: GuardConfig | None = None, **kwargs: Any) -> GuardConfig:
115
+ """Create a new config by merging with another config or keyword overrides.
116
+
117
+ Args:
118
+ other: Another GuardConfig whose non-default values take precedence.
119
+ **kwargs: Individual field overrides (highest precedence).
120
+
121
+ Returns:
122
+ A new GuardConfig with merged values.
123
+ """
124
+ base = self.to_dict()
125
+ if other is not None:
126
+ other_dict = other.to_dict()
127
+ defaults = GuardConfig()
128
+ default_dict = defaults.to_dict()
129
+ for key, value in other_dict.items():
130
+ if value != default_dict[key]:
131
+ base[key] = value
132
+ base.update(kwargs)
133
+ return GuardConfig.from_dict(base)
@@ -0,0 +1,34 @@
1
+ """LongGuard core — step models, detectors, circuit breaker, and reporting."""
2
+
3
+ from .breaker import BreakerDecision, BreakerState, CircuitBreaker
4
+ from .detectors import (
5
+ AbstractLoopDetector,
6
+ DeadEndDriftDetector,
7
+ DetectionResult,
8
+ HashBasedEmbedder,
9
+ SemanticOscillationDetector,
10
+ SentenceTransformerEmbedder,
11
+ TokenVelocityDetector,
12
+ ToolRepeatDetector,
13
+ )
14
+ from .pivot import PIVOT_TEMPLATES, ReflectAndPivotInjector
15
+ from .reporter import GuardReport
16
+ from .step import AgentStep
17
+
18
+ __all__ = [
19
+ "AgentStep",
20
+ "AbstractLoopDetector",
21
+ "DetectionResult",
22
+ "ToolRepeatDetector",
23
+ "SemanticOscillationDetector",
24
+ "DeadEndDriftDetector",
25
+ "TokenVelocityDetector",
26
+ "HashBasedEmbedder",
27
+ "SentenceTransformerEmbedder",
28
+ "CircuitBreaker",
29
+ "BreakerState",
30
+ "BreakerDecision",
31
+ "ReflectAndPivotInjector",
32
+ "PIVOT_TEMPLATES",
33
+ "GuardReport",
34
+ ]