aiptx 2.0.2__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.

Potentially problematic release.


This version of aiptx might be problematic. Click here for more details.

Files changed (165) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. aiptx-2.0.2.dist-info/top_level.txt +1 -0
aipt_v2/agents/ptt.py ADDED
@@ -0,0 +1,406 @@
1
+ """
2
+ AIPT Penetration Testing Tree (PTT) - Hierarchical task tracking
3
+ Tracks progress through pentest phases with visual feedback.
4
+
5
+ Inspired by: PentestGPT's PTT structure
6
+ Format:
7
+ 1. Reconnaissance - [completed]
8
+ 1.1 Passive Information Gathering - (completed)
9
+ 1.2 Active Scanning - (in-progress)
10
+ 2. Enumeration - [to-do]
11
+ 2.1 Service Enumeration - (to-do)
12
+ """
13
+
14
+ import json
15
+ from enum import Enum
16
+ from typing import Optional
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+
22
+ class TaskStatus(str, Enum):
23
+ """Status of a task"""
24
+ TODO = "to-do"
25
+ IN_PROGRESS = "in-progress"
26
+ COMPLETED = "completed"
27
+ BLOCKED = "blocked"
28
+
29
+
30
+ class PhaseType(str, Enum):
31
+ """Penetration testing phases"""
32
+ RECON = "recon"
33
+ SCANNING = "enum"
34
+ EXPLOITATION = "exploit"
35
+ POST_EXPLOITATION = "post"
36
+ REPORTING = "report"
37
+
38
+
39
+ @dataclass
40
+ class Task:
41
+ """A single task in the PTT"""
42
+ id: str
43
+ description: str
44
+ status: TaskStatus = TaskStatus.TODO
45
+ findings: list[dict] = field(default_factory=list)
46
+ started_at: Optional[str] = None
47
+ completed_at: Optional[str] = None
48
+ notes: str = ""
49
+
50
+
51
+ @dataclass
52
+ class Phase:
53
+ """A phase containing multiple tasks"""
54
+ name: str
55
+ description: str
56
+ status: TaskStatus = TaskStatus.TODO
57
+ tasks: list[Task] = field(default_factory=list)
58
+
59
+
60
+ class PTT:
61
+ """
62
+ Penetration Testing Tree - Hierarchical task tracking.
63
+
64
+ Provides:
65
+ - Visual progress tracking
66
+ - Phase-based organization
67
+ - Finding association
68
+ - Session persistence
69
+ """
70
+
71
+ # Standard pentest phases
72
+ PHASES = [
73
+ ("recon", "Reconnaissance and information gathering"),
74
+ ("enum", "Enumeration of services and vulnerabilities"),
75
+ ("exploit", "Exploitation of discovered vulnerabilities"),
76
+ ("post", "Post-exploitation and privilege escalation"),
77
+ ("report", "Documentation and report generation"),
78
+ ]
79
+
80
+ def __init__(self, session_dir: Optional[str] = None):
81
+ self.session_dir = Path(session_dir or "~/.aipt/sessions").expanduser()
82
+ self.session_dir.mkdir(parents=True, exist_ok=True)
83
+
84
+ self.target: str = ""
85
+ self.phases: dict[str, Phase] = {}
86
+ self.current_phase: str = "recon"
87
+ self.session_id: Optional[str] = None
88
+
89
+ def initialize(self, target: str) -> dict:
90
+ """
91
+ Initialize PTT for a new target.
92
+
93
+ Args:
94
+ target: Target being tested
95
+
96
+ Returns:
97
+ Initial PTT state as dict
98
+ """
99
+ self.target = target
100
+ self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
101
+
102
+ # Initialize phases
103
+ self.phases = {}
104
+ for phase_name, phase_desc in self.PHASES:
105
+ self.phases[phase_name] = Phase(
106
+ name=phase_name,
107
+ description=phase_desc,
108
+ status=TaskStatus.TODO,
109
+ tasks=[],
110
+ )
111
+
112
+ # Set first phase to in-progress
113
+ self.phases["recon"].status = TaskStatus.IN_PROGRESS
114
+ self.current_phase = "recon"
115
+
116
+ return self.to_dict()
117
+
118
+ def add_task(
119
+ self,
120
+ phase: str,
121
+ description: str,
122
+ status: TaskStatus = TaskStatus.TODO,
123
+ ) -> str:
124
+ """
125
+ Add a task to a phase.
126
+
127
+ Args:
128
+ phase: Phase name (recon, enum, exploit, post, report)
129
+ description: Task description
130
+
131
+ Returns:
132
+ Task ID
133
+ """
134
+ if phase not in self.phases:
135
+ raise ValueError(f"Unknown phase: {phase}")
136
+
137
+ phase_obj = self.phases[phase]
138
+ task_num = len(phase_obj.tasks) + 1
139
+
140
+ # Generate task ID (e.g., "R1", "E2")
141
+ phase_prefix = phase[0].upper()
142
+ task_id = f"{phase_prefix}{task_num}"
143
+
144
+ task = Task(
145
+ id=task_id,
146
+ description=description,
147
+ status=status,
148
+ )
149
+
150
+ if status == TaskStatus.IN_PROGRESS:
151
+ task.started_at = datetime.now().isoformat()
152
+
153
+ phase_obj.tasks.append(task)
154
+
155
+ return task_id
156
+
157
+ def update_task(
158
+ self,
159
+ task_id: str,
160
+ status: Optional[TaskStatus] = None,
161
+ findings: Optional[list[dict]] = None,
162
+ notes: Optional[str] = None,
163
+ ) -> bool:
164
+ """
165
+ Update a task's status or findings.
166
+
167
+ Args:
168
+ task_id: Task identifier
169
+ status: New status
170
+ findings: Findings to add
171
+ notes: Additional notes
172
+
173
+ Returns:
174
+ True if task was found and updated
175
+ """
176
+ task = self._find_task(task_id)
177
+ if not task:
178
+ return False
179
+
180
+ if status:
181
+ task.status = status
182
+ if status == TaskStatus.IN_PROGRESS:
183
+ task.started_at = datetime.now().isoformat()
184
+ elif status == TaskStatus.COMPLETED:
185
+ task.completed_at = datetime.now().isoformat()
186
+
187
+ if findings:
188
+ task.findings.extend(findings)
189
+
190
+ if notes:
191
+ task.notes = notes
192
+
193
+ return True
194
+
195
+ def complete_task(self, task_id: str, findings: Optional[list[dict]] = None) -> bool:
196
+ """Mark a task as completed"""
197
+ return self.update_task(task_id, TaskStatus.COMPLETED, findings)
198
+
199
+ def add_findings(self, phase: str, findings: list[dict]) -> None:
200
+ """
201
+ Add findings to the current active task in a phase.
202
+ If no active task, create one.
203
+ """
204
+ if phase not in self.phases:
205
+ return
206
+
207
+ phase_obj = self.phases[phase]
208
+
209
+ # Find active task
210
+ active_task = None
211
+ for task in phase_obj.tasks:
212
+ if task.status == TaskStatus.IN_PROGRESS:
213
+ active_task = task
214
+ break
215
+
216
+ # Create task if none active
217
+ if not active_task:
218
+ task_id = self.add_task(phase, "Auto-generated task", TaskStatus.IN_PROGRESS)
219
+ active_task = self._find_task(task_id)
220
+
221
+ if active_task:
222
+ active_task.findings.extend(findings)
223
+
224
+ def advance_phase(self) -> str:
225
+ """
226
+ Advance to the next phase.
227
+
228
+ Returns:
229
+ New phase name
230
+ """
231
+ phase_names = [p[0] for p in self.PHASES]
232
+ current_idx = phase_names.index(self.current_phase)
233
+
234
+ if current_idx < len(phase_names) - 1:
235
+ # Complete current phase
236
+ self.phases[self.current_phase].status = TaskStatus.COMPLETED
237
+
238
+ # Move to next phase
239
+ self.current_phase = phase_names[current_idx + 1]
240
+ self.phases[self.current_phase].status = TaskStatus.IN_PROGRESS
241
+
242
+ return self.current_phase
243
+
244
+ def set_phase(self, phase: str) -> None:
245
+ """Set current phase directly"""
246
+ if phase in self.phases:
247
+ self.current_phase = phase
248
+ self.phases[phase].status = TaskStatus.IN_PROGRESS
249
+
250
+ def _find_task(self, task_id: str) -> Optional[Task]:
251
+ """Find a task by ID"""
252
+ for phase in self.phases.values():
253
+ for task in phase.tasks:
254
+ if task.id == task_id:
255
+ return task
256
+ return None
257
+
258
+ def to_prompt(self) -> str:
259
+ """
260
+ Generate PTT string for LLM prompt.
261
+
262
+ Format:
263
+ 1. Reconnaissance - [completed]
264
+ R1. Port scanning - (completed) - 5 findings
265
+ R2. Service detection - (in-progress)
266
+ 2. Enumeration - [in-progress]
267
+ E1. Directory brute-force - (to-do)
268
+ """
269
+ lines = [f"Target: {self.target}", ""]
270
+
271
+ for i, (phase_name, _) in enumerate(self.PHASES, 1):
272
+ phase = self.phases.get(phase_name)
273
+ if not phase:
274
+ continue
275
+
276
+ # Phase line
277
+ status_str = f"[{phase.status.value}]"
278
+ phase_title = phase_name.title()
279
+ lines.append(f"{i}. {phase_title} - {status_str}")
280
+
281
+ # Task lines
282
+ for task in phase.tasks:
283
+ finding_count = len(task.findings)
284
+ finding_str = f" - {finding_count} findings" if finding_count else ""
285
+ status_str = f"({task.status.value})"
286
+ lines.append(f" {task.id}. {task.description} - {status_str}{finding_str}")
287
+
288
+ if not phase.tasks:
289
+ lines.append(" (no tasks yet)")
290
+
291
+ return "\n".join(lines)
292
+
293
+ def to_dict(self) -> dict:
294
+ """Export PTT as dictionary"""
295
+ return {
296
+ "target": self.target,
297
+ "session_id": self.session_id,
298
+ "current_phase": self.current_phase,
299
+ "phases": {
300
+ name: {
301
+ "name": phase.name,
302
+ "description": phase.description,
303
+ "status": phase.status.value,
304
+ "tasks": [
305
+ {
306
+ "id": task.id,
307
+ "description": task.description,
308
+ "status": task.status.value,
309
+ "findings": task.findings,
310
+ "started_at": task.started_at,
311
+ "completed_at": task.completed_at,
312
+ "notes": task.notes,
313
+ }
314
+ for task in phase.tasks
315
+ ],
316
+ }
317
+ for name, phase in self.phases.items()
318
+ },
319
+ }
320
+
321
+ def save(self, filename: Optional[str] = None) -> str:
322
+ """
323
+ Save PTT to file.
324
+
325
+ Returns:
326
+ Path to saved file
327
+ """
328
+ if not filename:
329
+ filename = f"ptt_{self.session_id}_{self.target.replace('/', '_')}.json"
330
+
331
+ filepath = self.session_dir / filename
332
+
333
+ with open(filepath, "w") as f:
334
+ json.dump(self.to_dict(), f, indent=2)
335
+
336
+ return str(filepath)
337
+
338
+ def load(self, filepath: str) -> None:
339
+ """Load PTT from file"""
340
+ with open(filepath, "r") as f:
341
+ data = json.load(f)
342
+
343
+ self.target = data.get("target", "")
344
+ self.session_id = data.get("session_id")
345
+ self.current_phase = data.get("current_phase", "recon")
346
+
347
+ self.phases = {}
348
+ for name, phase_data in data.get("phases", {}).items():
349
+ tasks = [
350
+ Task(
351
+ id=t["id"],
352
+ description=t["description"],
353
+ status=TaskStatus(t["status"]),
354
+ findings=t.get("findings", []),
355
+ started_at=t.get("started_at"),
356
+ completed_at=t.get("completed_at"),
357
+ notes=t.get("notes", ""),
358
+ )
359
+ for t in phase_data.get("tasks", [])
360
+ ]
361
+
362
+ self.phases[name] = Phase(
363
+ name=phase_data["name"],
364
+ description=phase_data["description"],
365
+ status=TaskStatus(phase_data["status"]),
366
+ tasks=tasks,
367
+ )
368
+
369
+ def get_summary(self) -> dict:
370
+ """Get summary statistics"""
371
+ total_tasks = 0
372
+ completed_tasks = 0
373
+ total_findings = 0
374
+
375
+ for phase in self.phases.values():
376
+ for task in phase.tasks:
377
+ total_tasks += 1
378
+ if task.status == TaskStatus.COMPLETED:
379
+ completed_tasks += 1
380
+ total_findings += len(task.findings)
381
+
382
+ completed_phases = sum(
383
+ 1 for p in self.phases.values()
384
+ if p.status == TaskStatus.COMPLETED
385
+ )
386
+
387
+ return {
388
+ "target": self.target,
389
+ "current_phase": self.current_phase,
390
+ "phases_completed": completed_phases,
391
+ "phases_total": len(self.phases),
392
+ "tasks_completed": completed_tasks,
393
+ "tasks_total": total_tasks,
394
+ "total_findings": total_findings,
395
+ }
396
+
397
+ def get_tasks_by_phase(self, phase: PhaseType) -> list[Task]:
398
+ """Get all tasks for a specific phase"""
399
+ phase_name = phase.value if isinstance(phase, PhaseType) else phase
400
+ if phase_name in self.phases:
401
+ return self.phases[phase_name].tasks
402
+ return []
403
+
404
+ def update_task_status(self, task_id: str, status: TaskStatus) -> bool:
405
+ """Update a task's status by ID"""
406
+ return self.update_task(task_id, status=status)
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Optional, List, Dict
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ def _generate_agent_id() -> str:
10
+ return f"agent_{uuid.uuid4().hex[:8]}"
11
+
12
+
13
+ def _now_iso() -> str:
14
+ return datetime.now(timezone.utc).isoformat()
15
+
16
+
17
+ class AgentState(BaseModel):
18
+ agent_id: str = Field(default_factory=_generate_agent_id)
19
+ agent_name: str = "AIPT Agent"
20
+ parent_id: Optional[str] = None
21
+ sandbox_id: Optional[str] = None
22
+ sandbox_token: Optional[str] = None
23
+ sandbox_info: Optional[Dict[str, Any]] = None
24
+
25
+ task: str = ""
26
+ iteration: int = 0
27
+ max_iterations: int = 300
28
+ completed: bool = False
29
+ stop_requested: bool = False
30
+ waiting_for_input: bool = False
31
+ llm_failed: bool = False
32
+ waiting_start_time: Optional[datetime] = None
33
+ final_result: Optional[Dict[str, Any]] = None
34
+ max_iterations_warning_sent: bool = False
35
+
36
+ messages: List[Dict[str, Any]] = Field(default_factory=list)
37
+ context: Dict[str, Any] = Field(default_factory=dict)
38
+
39
+ start_time: str = Field(default_factory=_now_iso)
40
+ last_updated: str = Field(default_factory=_now_iso)
41
+
42
+ actions_taken: List[Dict[str, Any]] = Field(default_factory=list)
43
+ observations: List[Dict[str, Any]] = Field(default_factory=list)
44
+
45
+ errors: List[str] = Field(default_factory=list)
46
+
47
+ def increment_iteration(self) -> None:
48
+ self.iteration += 1
49
+ self.last_updated = _now_iso()
50
+
51
+ def add_message(self, role: str, content: Any) -> None:
52
+ self.messages.append({"role": role, "content": content})
53
+ self.last_updated = _now_iso()
54
+
55
+ def add_action(self, action: Dict[str, Any]) -> None:
56
+ self.actions_taken.append(
57
+ {
58
+ "iteration": self.iteration,
59
+ "timestamp": _now_iso(),
60
+ "action": action,
61
+ }
62
+ )
63
+
64
+ def add_observation(self, observation: Dict[str, Any]) -> None:
65
+ self.observations.append(
66
+ {
67
+ "iteration": self.iteration,
68
+ "timestamp": _now_iso(),
69
+ "observation": observation,
70
+ }
71
+ )
72
+
73
+ def add_error(self, error: str) -> None:
74
+ self.errors.append(f"Iteration {self.iteration}: {error}")
75
+ self.last_updated = _now_iso()
76
+
77
+ def update_context(self, key: str, value: Any) -> None:
78
+ self.context[key] = value
79
+ self.last_updated = _now_iso()
80
+
81
+ def set_completed(self, final_result: Optional[Dict[str, Any]] = None) -> None:
82
+ self.completed = True
83
+ self.final_result = final_result
84
+ self.last_updated = _now_iso()
85
+
86
+ def request_stop(self) -> None:
87
+ self.stop_requested = True
88
+ self.last_updated = _now_iso()
89
+
90
+ def should_stop(self) -> bool:
91
+ return self.stop_requested or self.completed or self.has_reached_max_iterations()
92
+
93
+ def is_waiting_for_input(self) -> bool:
94
+ return self.waiting_for_input
95
+
96
+ def enter_waiting_state(self, llm_failed: bool = False) -> None:
97
+ self.waiting_for_input = True
98
+ self.waiting_start_time = datetime.now(timezone.utc)
99
+ self.llm_failed = llm_failed
100
+ self.last_updated = _now_iso()
101
+
102
+ def resume_from_waiting(self, new_task: Optional[str] = None) -> None:
103
+ self.waiting_for_input = False
104
+ self.waiting_start_time = None
105
+ self.stop_requested = False
106
+ self.completed = False
107
+ self.llm_failed = False
108
+ if new_task:
109
+ self.task = new_task
110
+ self.last_updated = _now_iso()
111
+
112
+ def has_reached_max_iterations(self) -> bool:
113
+ return self.iteration >= self.max_iterations
114
+
115
+ def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
116
+ return self.iteration >= int(self.max_iterations * threshold)
117
+
118
+ def has_waiting_timeout(self) -> bool:
119
+ if not self.waiting_for_input or not self.waiting_start_time:
120
+ return False
121
+
122
+ if (
123
+ self.stop_requested
124
+ or self.llm_failed
125
+ or self.completed
126
+ or self.has_reached_max_iterations()
127
+ ):
128
+ return False
129
+
130
+ elapsed = (datetime.now(timezone.utc) - self.waiting_start_time).total_seconds()
131
+ return elapsed > 600
132
+
133
+ def has_empty_last_messages(self, count: int = 3) -> bool:
134
+ if len(self.messages) < count:
135
+ return False
136
+
137
+ last_messages = self.messages[-count:]
138
+
139
+ for message in last_messages:
140
+ content = message.get("content", "")
141
+ if isinstance(content, str) and content.strip():
142
+ return False
143
+
144
+ return True
145
+
146
+ def get_conversation_history(self) -> list[dict[str, Any]]:
147
+ return self.messages
148
+
149
+ def get_execution_summary(self) -> dict[str, Any]:
150
+ return {
151
+ "agent_id": self.agent_id,
152
+ "agent_name": self.agent_name,
153
+ "parent_id": self.parent_id,
154
+ "sandbox_id": self.sandbox_id,
155
+ "sandbox_info": self.sandbox_info,
156
+ "task": self.task,
157
+ "iteration": self.iteration,
158
+ "max_iterations": self.max_iterations,
159
+ "completed": self.completed,
160
+ "final_result": self.final_result,
161
+ "start_time": self.start_time,
162
+ "last_updated": self.last_updated,
163
+ "total_actions": len(self.actions_taken),
164
+ "total_observations": len(self.observations),
165
+ "total_errors": len(self.errors),
166
+ "has_errors": len(self.errors) > 0,
167
+ "max_iterations_reached": self.has_reached_max_iterations() and not self.completed,
168
+ }