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 +127 -0
- longguard/config.py +133 -0
- longguard/core/__init__.py +34 -0
- longguard/core/breaker.py +480 -0
- longguard/core/detectors/__init__.py +24 -0
- longguard/core/detectors/base.py +110 -0
- longguard/core/detectors/dead_end.py +252 -0
- longguard/core/detectors/semantic_osc.py +201 -0
- longguard/core/detectors/token_velocity.py +165 -0
- longguard/core/detectors/tool_repeat.py +84 -0
- longguard/core/pivot.py +177 -0
- longguard/core/reporter.py +187 -0
- longguard/core/step.py +133 -0
- longguard/integrations/__init__.py +14 -0
- longguard/integrations/langchain.py +284 -0
- longguard/integrations/langgraph.py +361 -0
- longguard/integrations/strands.py +222 -0
- longguard-0.1.1.dist-info/METADATA +275 -0
- longguard-0.1.1.dist-info/RECORD +21 -0
- longguard-0.1.1.dist-info/WHEEL +4 -0
- longguard-0.1.1.dist-info/licenses/LICENSE +21 -0
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
|
+
]
|