prompture 0.0.50.dev1__py3-none-any.whl → 0.0.51__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.
prompture/__init__.py CHANGED
@@ -8,6 +8,7 @@ from .agent_types import (
8
8
  AgentResult,
9
9
  AgentState,
10
10
  AgentStep,
11
+ ApprovalRequired,
11
12
  GuardrailError,
12
13
  ModelRetry,
13
14
  RunContext,
@@ -15,6 +16,15 @@ from .agent_types import (
15
16
  StreamEvent,
16
17
  StreamEventType,
17
18
  )
19
+ from .analysis import (
20
+ CodeAnalysis,
21
+ CodeFeatures,
22
+ FeatureExtractor,
23
+ RiskAssessment,
24
+ RiskLevel,
25
+ analyze_python,
26
+ calculate_risk,
27
+ )
18
28
  from .async_agent import AsyncAgent, AsyncAgentIterator, AsyncStreamedAgentResult
19
29
  from .async_conversation import AsyncConversation
20
30
  from .async_driver import AsyncDriver
@@ -106,6 +116,14 @@ from .groups import (
106
116
  RouterAgent,
107
117
  SequentialGroup,
108
118
  )
119
+ from .history import (
120
+ calculate_cost_breakdown,
121
+ export_result_json,
122
+ filter_steps,
123
+ get_tool_call_summary,
124
+ result_to_dict,
125
+ search_messages,
126
+ )
109
127
  from .image import (
110
128
  ImageContent,
111
129
  ImageInput,
@@ -141,6 +159,20 @@ from .persona import (
141
159
  reset_trait_registry,
142
160
  )
143
161
  from .runner import run_suite_from_spec
162
+ from .sandbox import (
163
+ ALWAYS_BLOCKED_IMPORTS,
164
+ ImportRestrictions,
165
+ ImportViolationError,
166
+ PathRestrictions,
167
+ PathViolationError,
168
+ PythonSandbox,
169
+ ResourceContext,
170
+ ResourceLimitError,
171
+ ResourceLimits,
172
+ SandboxError,
173
+ SandboxResult,
174
+ SandboxTimeoutError,
175
+ )
144
176
  from .serialization import (
145
177
  EXPORT_VERSION,
146
178
  export_conversation,
@@ -183,6 +215,9 @@ except Exception:
183
215
  __version__ = "0.0.0"
184
216
 
185
217
  __all__ = [
218
+ # Sandbox module
219
+ "ALWAYS_BLOCKED_IMPORTS",
220
+ # Core exports
186
221
  "EXPORT_VERSION",
187
222
  "FIELD_DEFINITIONS",
188
223
  "PERSONAS",
@@ -194,6 +229,7 @@ __all__ = [
194
229
  "AgentState",
195
230
  "AgentStep",
196
231
  "AirLLMDriver",
232
+ "ApprovalRequired",
197
233
  "AsyncAgent",
198
234
  "AsyncAgentIterator",
199
235
  "AsyncConversation",
@@ -205,11 +241,15 @@ __all__ = [
205
241
  "AzureDriver",
206
242
  "CacheBackend",
207
243
  "ClaudeDriver",
244
+ # Analysis module
245
+ "CodeAnalysis",
246
+ "CodeFeatures",
208
247
  "Conversation",
209
248
  "ConversationStore",
210
249
  "Driver",
211
250
  "DriverCallbacks",
212
251
  "ErrorPolicy",
252
+ "FeatureExtractor",
213
253
  "GoogleDriver",
214
254
  "GrokDriver",
215
255
  "GroqDriver",
@@ -220,6 +260,8 @@ __all__ = [
220
260
  "GuardrailError",
221
261
  "ImageContent",
222
262
  "ImageInput",
263
+ "ImportRestrictions",
264
+ "ImportViolationError",
223
265
  "JSONFormatter",
224
266
  "LMStudioDriver",
225
267
  "LocalHTTPDriver",
@@ -232,12 +274,23 @@ __all__ = [
232
274
  "OpenAIDriver",
233
275
  "OpenRouterDriver",
234
276
  "ParallelGroup",
277
+ "PathRestrictions",
278
+ "PathViolationError",
235
279
  "Persona",
280
+ "PythonSandbox",
236
281
  "RedisCacheBackend",
282
+ "ResourceContext",
283
+ "ResourceLimitError",
284
+ "ResourceLimits",
237
285
  "ResponseCache",
286
+ "RiskAssessment",
287
+ "RiskLevel",
238
288
  "RouterAgent",
239
289
  "RunContext",
240
290
  "SQLiteCacheBackend",
291
+ "SandboxError",
292
+ "SandboxResult",
293
+ "SandboxTimeoutError",
241
294
  "SequentialGroup",
242
295
  "StepType",
243
296
  "StreamEvent",
@@ -248,7 +301,11 @@ __all__ = [
248
301
  "UsageSession",
249
302
  "add_field_definition",
250
303
  "add_field_definitions",
304
+ "analyze_python",
251
305
  "ask_for_json",
306
+ # History module
307
+ "calculate_cost_breakdown",
308
+ "calculate_risk",
252
309
  "clean_json_text",
253
310
  "clean_json_text_with_ai",
254
311
  "clean_toon_text",
@@ -258,12 +315,14 @@ __all__ = [
258
315
  "configure_cache",
259
316
  "configure_logging",
260
317
  "export_conversation",
318
+ "export_result_json",
261
319
  "export_usage_session",
262
320
  "extract_and_jsonify",
263
321
  "extract_from_data",
264
322
  "extract_from_pandas",
265
323
  "extract_with_model",
266
324
  "field_from_registry",
325
+ "filter_steps",
267
326
  "get_available_models",
268
327
  "get_cache",
269
328
  "get_driver",
@@ -279,6 +338,7 @@ __all__ = [
279
338
  "get_recently_used_models",
280
339
  "get_registry_snapshot",
281
340
  "get_required_fields",
341
+ "get_tool_call_summary",
282
342
  "get_trait",
283
343
  "get_trait_names",
284
344
  "image_from_base64",
@@ -307,7 +367,9 @@ __all__ = [
307
367
  "reset_persona_registry",
308
368
  "reset_registry",
309
369
  "reset_trait_registry",
370
+ "result_to_dict",
310
371
  "run_suite_from_spec",
372
+ "search_messages",
311
373
  "set_azure_config_resolver",
312
374
  "stepwise_extract_with_model",
313
375
  "tool_from_function",
prompture/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.50.dev1'
32
- __version_tuple__ = version_tuple = (0, 0, 50, 'dev1')
31
+ __version__ = version = '0.0.51'
32
+ __version_tuple__ = version_tuple = (0, 0, 51)
33
33
 
34
34
  __commit_id__ = commit_id = None
prompture/agent.py CHANGED
@@ -30,6 +30,7 @@ from .agent_types import (
30
30
  AgentResult,
31
31
  AgentState,
32
32
  AgentStep,
33
+ ApprovalRequired,
33
34
  DepsType,
34
35
  ModelRetry,
35
36
  RunContext,
@@ -338,6 +339,26 @@ class Agent(Generic[DepsType]):
338
339
  result = _fn(ctx, **kwargs)
339
340
  else:
340
341
  result = _fn(**kwargs)
342
+ except ApprovalRequired as exc:
343
+ # Handle approval request
344
+ if _cb.on_approval_needed:
345
+ approved = _cb.on_approval_needed(exc.tool_name, exc.action, exc.details)
346
+ if approved:
347
+ # Retry the tool call after approval
348
+ try:
349
+ if _wants:
350
+ result = _fn(ctx, **kwargs)
351
+ else:
352
+ result = _fn(**kwargs)
353
+ except ApprovalRequired:
354
+ # Tool raised ApprovalRequired again - don't loop
355
+ result = f"Error: Tool '{_name}' requires approval but approval was already granted"
356
+ except ModelRetry as retry_exc:
357
+ result = f"Error: {retry_exc.message}"
358
+ else:
359
+ result = f"Error: Tool '{_name}' execution denied - approval required: {exc.action}"
360
+ else:
361
+ result = f"Error: Tool '{_name}' requires approval but no approval handler is configured"
341
362
  except ModelRetry as exc:
342
363
  result = f"Error: {exc.message}"
343
364
  if _cb.on_tool_end:
@@ -607,12 +628,28 @@ class Agent(Generic[DepsType]):
607
628
  all_tool_calls: list[dict[str, Any]],
608
629
  ) -> None:
609
630
  """Scan conversation messages and populate steps and tool_calls."""
631
+
610
632
  now = time.time()
611
633
 
612
634
  for msg in messages:
613
635
  role = msg.get("role", "")
614
636
 
615
637
  if role == "assistant":
638
+ content = msg.get("content", "") or ""
639
+
640
+ # Extract thinking content from <think> tags
641
+ thinking_text = self._extract_thinking(content)
642
+ if thinking_text and self._agent_callbacks.on_thinking:
643
+ self._agent_callbacks.on_thinking(thinking_text)
644
+ # Also record as a think step
645
+ steps.append(
646
+ AgentStep(
647
+ step_type=StepType.think,
648
+ timestamp=now,
649
+ content=thinking_text,
650
+ )
651
+ )
652
+
616
653
  tc_list = msg.get("tool_calls", [])
617
654
  if tc_list:
618
655
  # Assistant message with tool calls
@@ -632,7 +669,7 @@ class Agent(Generic[DepsType]):
632
669
  AgentStep(
633
670
  step_type=StepType.tool_call,
634
671
  timestamp=now,
635
- content=msg.get("content", ""),
672
+ content=content,
636
673
  tool_name=name,
637
674
  tool_args=args,
638
675
  )
@@ -644,7 +681,7 @@ class Agent(Generic[DepsType]):
644
681
  AgentStep(
645
682
  step_type=StepType.output,
646
683
  timestamp=now,
647
- content=msg.get("content", ""),
684
+ content=content,
648
685
  )
649
686
  )
650
687
 
@@ -658,6 +695,28 @@ class Agent(Generic[DepsType]):
658
695
  )
659
696
  )
660
697
 
698
+ def _extract_thinking(self, content: str) -> str | None:
699
+ """Extract thinking content from <think> tags.
700
+
701
+ Some models (like DeepSeek, Qwen) emit chain-of-thought reasoning
702
+ within <think>...</think> tags. This method extracts that content.
703
+
704
+ Args:
705
+ content: The assistant message content.
706
+
707
+ Returns:
708
+ The thinking text if found, None otherwise.
709
+ """
710
+ import re
711
+
712
+ # Match <think>...</think> tags (case-insensitive, allows multiline)
713
+ pattern = r"<think>(.*?)</think>"
714
+ matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
715
+ if matches:
716
+ # Join multiple thinking blocks with newlines
717
+ return "\n".join(match.strip() for match in matches)
718
+ return None
719
+
661
720
  def _parse_output(
662
721
  self,
663
722
  conv: Conversation,
prompture/agent_types.py CHANGED
@@ -6,8 +6,10 @@ Defines enums, dataclasses, and exceptions used by :class:`~prompture.agent.Agen
6
6
  from __future__ import annotations
7
7
 
8
8
  import enum
9
+ import json
9
10
  from collections.abc import Callable
10
11
  from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
11
13
  from typing import Any, Generic, TypeVar
12
14
 
13
15
  DepsType = TypeVar("DepsType")
@@ -51,6 +53,33 @@ class GuardrailError(Exception):
51
53
  super().__init__(message)
52
54
 
53
55
 
56
+ class ApprovalRequired(Exception):
57
+ """Raised by tools that require human approval before execution.
58
+
59
+ When a tool raises this exception, the agent will invoke the
60
+ ``on_approval_needed`` callback if configured. If the callback
61
+ returns True, the tool will be executed; if False, the tool
62
+ execution will be skipped and an error message returned to the LLM.
63
+
64
+ Attributes:
65
+ tool_name: Name of the tool requesting approval.
66
+ action: Description of the action requiring approval.
67
+ details: Additional details about what will be executed.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ tool_name: str,
73
+ action: str,
74
+ details: dict[str, Any] | None = None,
75
+ ) -> None:
76
+ self.tool_name = tool_name
77
+ self.action = action
78
+ self.details = details or {}
79
+ message = f"Tool '{tool_name}' requires approval: {action}"
80
+ super().__init__(message)
81
+
82
+
54
83
  @dataclass
55
84
  class RunContext(Generic[DepsType]):
56
85
  """Dependency-injection context available to tools and guardrails.
@@ -83,6 +112,19 @@ class AgentCallbacks:
83
112
  Fired at the logical agent layer (steps, tool invocations, output),
84
113
  separate from :class:`~prompture.callbacks.DriverCallbacks` which
85
114
  fires at the HTTP/driver layer.
115
+
116
+ Attributes:
117
+ on_step: Called for each step during execution.
118
+ on_tool_start: Called before a tool is invoked with (name, args).
119
+ on_tool_end: Called after a tool completes with (name, result).
120
+ on_iteration: Called at the start of each iteration with the index.
121
+ on_output: Called when the agent produces final output.
122
+ on_thinking: Called when the agent emits thinking/reasoning content.
123
+ The callback receives the thinking text (e.g., content within
124
+ <think> tags for models that support chain-of-thought).
125
+ on_approval_needed: Called when a tool raises ApprovalRequired.
126
+ The callback receives (tool_name, action, details) and should
127
+ return True to approve execution or False to deny.
86
128
  """
87
129
 
88
130
  on_step: Callable[[AgentStep], None] | None = None
@@ -90,6 +132,8 @@ class AgentCallbacks:
90
132
  on_tool_end: Callable[[str, Any], None] | None = None
91
133
  on_iteration: Callable[[int], None] | None = None
92
134
  on_output: Callable[[AgentResult], None] | None = None
135
+ on_thinking: Callable[[str], None] | None = None
136
+ on_approval_needed: Callable[[str, str, dict[str, Any]], bool] | None = None
93
137
 
94
138
 
95
139
  @dataclass
@@ -130,6 +174,60 @@ class AgentResult:
130
174
  state: AgentState = AgentState.idle
131
175
  run_usage: dict[str, Any] = field(default_factory=dict)
132
176
 
177
+ def to_dict(self, include_messages: bool = True) -> dict[str, Any]:
178
+ """Convert this result to a dictionary for serialization.
179
+
180
+ Args:
181
+ include_messages: Whether to include the full message history.
182
+
183
+ Returns:
184
+ Dictionary representation of this result.
185
+ """
186
+ data: dict[str, Any] = {
187
+ "output": str(self.output) if self.output is not None else None,
188
+ "output_text": self.output_text,
189
+ "state": self.state.value if hasattr(self.state, "value") else str(self.state),
190
+ "usage": self.usage,
191
+ "run_usage": self.run_usage,
192
+ "steps": [
193
+ {
194
+ "step_type": s.step_type.value if hasattr(s.step_type, "value") else str(s.step_type),
195
+ "timestamp": s.timestamp,
196
+ "content": s.content,
197
+ "tool_name": s.tool_name,
198
+ "tool_args": s.tool_args,
199
+ "tool_result": s.tool_result,
200
+ "duration_ms": s.duration_ms,
201
+ }
202
+ for s in self.steps
203
+ ],
204
+ "all_tool_calls": self.all_tool_calls,
205
+ "exported_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
206
+ }
207
+
208
+ if include_messages:
209
+ data["messages"] = self.messages
210
+
211
+ return data
212
+
213
+ def export_json(self, include_messages: bool = True) -> str:
214
+ """Export this result to a JSON string.
215
+
216
+ Args:
217
+ include_messages: Whether to include the full message history.
218
+
219
+ Returns:
220
+ JSON string representation of this result.
221
+
222
+ Example::
223
+
224
+ result = agent.run("What is 2+2?")
225
+ json_str = result.export_json()
226
+ with open("agent_history.json", "w") as f:
227
+ f.write(json_str)
228
+ """
229
+ return json.dumps(self.to_dict(include_messages=include_messages), indent=2, default=str)
230
+
133
231
 
134
232
  class StreamEventType(str, enum.Enum):
135
233
  """Classification for events emitted during streaming agent execution."""
@@ -0,0 +1,19 @@
1
+ """Code analysis module for Python code security assessment.
2
+
3
+ Provides AST-based analysis to detect potentially dangerous operations
4
+ and calculate risk scores for code execution in sandboxed environments.
5
+ """
6
+
7
+ from .analyzer import CodeAnalysis, analyze_python
8
+ from .ast_visitors import CodeFeatures, FeatureExtractor
9
+ from .risk_scoring import RiskAssessment, RiskLevel, calculate_risk
10
+
11
+ __all__ = [
12
+ "CodeAnalysis",
13
+ "CodeFeatures",
14
+ "FeatureExtractor",
15
+ "RiskAssessment",
16
+ "RiskLevel",
17
+ "analyze_python",
18
+ "calculate_risk",
19
+ ]
@@ -0,0 +1,142 @@
1
+ """Main code analysis interface.
2
+
3
+ Provides the primary analyze_python() function for code security assessment.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+
10
+ from .ast_visitors import CodeFeatures, extract_features
11
+ from .risk_scoring import RiskAssessment, RiskLevel, calculate_risk
12
+
13
+
14
+ @dataclass
15
+ class CodeAnalysis:
16
+ """Complete analysis result for Python code.
17
+
18
+ Attributes:
19
+ source: The original source code analyzed.
20
+ features: Extracted code features from AST analysis.
21
+ risk: Risk assessment with level, score, and reasons.
22
+ is_safe: Whether the code is considered safe for execution.
23
+ syntax_valid: Whether the code has valid Python syntax.
24
+ syntax_error: Syntax error message if parsing failed.
25
+ """
26
+
27
+ source: str
28
+ features: CodeFeatures
29
+ risk: RiskAssessment
30
+ is_safe: bool
31
+ syntax_valid: bool = True
32
+ syntax_error: str | None = None
33
+
34
+ @property
35
+ def risk_level(self) -> RiskLevel:
36
+ """Convenience property to access risk level directly."""
37
+ return self.risk.level
38
+
39
+ @property
40
+ def risk_score(self) -> int:
41
+ """Convenience property to access risk score directly."""
42
+ return self.risk.score
43
+
44
+ def to_dict(self) -> dict:
45
+ """Convert analysis to a dictionary for serialization."""
46
+ return {
47
+ "source": self.source,
48
+ "syntax_valid": self.syntax_valid,
49
+ "syntax_error": self.syntax_error,
50
+ "is_safe": self.is_safe,
51
+ "risk": {
52
+ "level": self.risk.level.value,
53
+ "score": self.risk.score,
54
+ "reasons": self.risk.reasons,
55
+ "warnings": self.risk.warnings,
56
+ "blocked_imports": list(self.risk.blocked_imports),
57
+ },
58
+ "features": {
59
+ "imports": list(self.features.imports),
60
+ "file_operations": self.features.file_operations,
61
+ "network_calls": self.features.network_calls,
62
+ "system_calls": self.features.system_calls,
63
+ "exec_eval_usage": self.features.exec_eval_usage,
64
+ "dangerous_builtins": list(self.features.dangerous_builtins),
65
+ "function_calls": list(self.features.function_calls),
66
+ "has_global_statements": self.features.has_global_statements,
67
+ "has_nonlocal_statements": self.features.has_nonlocal_statements,
68
+ "class_definitions": list(self.features.class_definitions),
69
+ "async_operations": self.features.async_operations,
70
+ },
71
+ }
72
+
73
+
74
+ def analyze_python(
75
+ source: str,
76
+ *,
77
+ safe_threshold: RiskLevel = RiskLevel.MEDIUM,
78
+ ) -> CodeAnalysis:
79
+ """Analyze Python source code for security risks.
80
+
81
+ Performs AST-based analysis to detect potentially dangerous operations
82
+ and calculates a risk score.
83
+
84
+ Args:
85
+ source: Python source code as a string.
86
+ safe_threshold: Maximum risk level considered safe.
87
+ Defaults to MEDIUM (allowing LOW and MEDIUM risk code).
88
+
89
+ Returns:
90
+ CodeAnalysis with features, risk assessment, and safety determination.
91
+
92
+ Example::
93
+
94
+ from prompture.analysis import analyze_python, RiskLevel
95
+
96
+ analysis = analyze_python("import subprocess; subprocess.run(['ls'])")
97
+ print(f"Risk: {analysis.risk_level}") # RiskLevel.CRITICAL
98
+ print(f"Safe: {analysis.is_safe}") # False
99
+
100
+ # More permissive threshold
101
+ analysis = analyze_python("import json", safe_threshold=RiskLevel.HIGH)
102
+ print(f"Safe: {analysis.is_safe}") # True
103
+ """
104
+ # Try to parse the source code
105
+ try:
106
+ features = extract_features(source)
107
+ syntax_valid = True
108
+ syntax_error = None
109
+ except SyntaxError as e:
110
+ # Return analysis with syntax error
111
+ features = CodeFeatures()
112
+ risk = RiskAssessment(
113
+ level=RiskLevel.CRITICAL,
114
+ score=100,
115
+ reasons=[f"Syntax error: {e}"],
116
+ )
117
+ return CodeAnalysis(
118
+ source=source,
119
+ features=features,
120
+ risk=risk,
121
+ is_safe=False,
122
+ syntax_valid=False,
123
+ syntax_error=str(e),
124
+ )
125
+
126
+ # Calculate risk
127
+ risk = calculate_risk(features)
128
+
129
+ # Determine safety based on threshold
130
+ threshold_order = [RiskLevel.LOW, RiskLevel.MEDIUM, RiskLevel.HIGH, RiskLevel.CRITICAL]
131
+ risk_index = threshold_order.index(risk.level)
132
+ threshold_index = threshold_order.index(safe_threshold)
133
+ is_safe = risk_index <= threshold_index
134
+
135
+ return CodeAnalysis(
136
+ source=source,
137
+ features=features,
138
+ risk=risk,
139
+ is_safe=is_safe,
140
+ syntax_valid=syntax_valid,
141
+ syntax_error=syntax_error,
142
+ )