alienrecon 3.0.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.
- alienrecon/__init__.py +1 -0
- alienrecon/__main__.py +5 -0
- alienrecon/agent/__init__.py +0 -0
- alienrecon/agent/assistant.py +88 -0
- alienrecon/agent/brain.py +317 -0
- alienrecon/agent/instructor.py +592 -0
- alienrecon/api.py +188 -0
- alienrecon/cli.py +323 -0
- alienrecon/core/__init__.py +0 -0
- alienrecon/core/exceptions.py +44 -0
- alienrecon/core/flag_celebrator.py +126 -0
- alienrecon/core/input_validator.py +431 -0
- alienrecon/curriculum/__init__.py +0 -0
- alienrecon/curriculum/profile.py +178 -0
- alienrecon/curriculum/rooms.py +229 -0
- alienrecon/data/README.md +80 -0
- alienrecon/data/ctf_info/htb_lame.yaml +23 -0
- alienrecon/data/ctf_info/test_box.yaml +11 -0
- alienrecon/data/ctf_info/thm_basic_pentesting.yaml +24 -0
- alienrecon/data/prompts/mcp_system_prompt.txt +253 -0
- alienrecon/data/rooms/index.yaml +134 -0
- alienrecon/data/rooms/thm-activerecon.yaml +322 -0
- alienrecon/data/rooms/thm-authenticationbypass.yaml +395 -0
- alienrecon/data/rooms/thm-boilerctf.yaml +706 -0
- alienrecon/data/rooms/thm-breachingad.yaml +955 -0
- alienrecon/data/rooms/thm-bruteit.yaml +454 -0
- alienrecon/data/rooms/thm-burpsuitebasics.yaml +371 -0
- alienrecon/data/rooms/thm-burpsuiteextensions.yaml +129 -0
- alienrecon/data/rooms/thm-burpsuiteintruder.yaml +421 -0
- alienrecon/data/rooms/thm-burpsuiteom.yaml +314 -0
- alienrecon/data/rooms/thm-burpsuiterepeater.yaml +289 -0
- alienrecon/data/rooms/thm-careersincyber.yaml +347 -0
- alienrecon/data/rooms/thm-chillhack.yaml +876 -0
- alienrecon/data/rooms/thm-contentdiscovery.yaml +539 -0
- alienrecon/data/rooms/thm-cyborg.yaml +485 -0
- alienrecon/data/rooms/thm-defensivesecurityintro.yaml +380 -0
- alienrecon/data/rooms/thm-exploitingavulnerabilityv2.yaml +349 -0
- alienrecon/data/rooms/thm-fileinc.yaml +410 -0
- alienrecon/data/rooms/thm-fowsniff.yaml +640 -0
- alienrecon/data/rooms/thm-h4cked.yaml +708 -0
- alienrecon/data/rooms/thm-hackpark.yaml +783 -0
- alienrecon/data/rooms/thm-idor.yaml +300 -0
- alienrecon/data/rooms/thm-internal.yaml +506 -0
- alienrecon/data/rooms/thm-introtoc2.yaml +680 -0
- alienrecon/data/rooms/thm-introtoshells.yaml +829 -0
- alienrecon/data/rooms/thm-lazyadmin.yaml +451 -0
- alienrecon/data/rooms/thm-linprivesc.yaml +985 -0
- alienrecon/data/rooms/thm-linuxprivesc.yaml +1347 -0
- alienrecon/data/rooms/thm-metasploitexploitation.yaml +460 -0
- alienrecon/data/rooms/thm-metasploitintro.yaml +517 -0
- alienrecon/data/rooms/thm-meterpreter.yaml +539 -0
- alienrecon/data/rooms/thm-netsecchallenge.yaml +286 -0
- alienrecon/data/rooms/thm-nmap01.yaml +636 -0
- alienrecon/data/rooms/thm-nmap02.yaml +569 -0
- alienrecon/data/rooms/thm-nmap03.yaml +623 -0
- alienrecon/data/rooms/thm-nmap04.yaml +609 -0
- alienrecon/data/rooms/thm-offensivesecurityintro.yaml +314 -0
- alienrecon/data/rooms/thm-oscommandinjection.yaml +239 -0
- alienrecon/data/rooms/thm-passiverecon.yaml +260 -0
- alienrecon/data/rooms/thm-pentestingfundamentals.yaml +373 -0
- alienrecon/data/rooms/thm-principlesofsecurity.yaml +389 -0
- alienrecon/data/rooms/thm-protocolsandservers.yaml +367 -0
- alienrecon/data/rooms/thm-protocolsandservers2.yaml +443 -0
- alienrecon/data/rooms/thm-raceconditionsattacks.yaml +260 -0
- alienrecon/data/rooms/thm-relevant.yaml +642 -0
- alienrecon/data/rooms/thm-solar.yaml +866 -0
- alienrecon/data/rooms/thm-sqlinjectionlm.yaml +957 -0
- alienrecon/data/rooms/thm-ssrfqi.yaml +283 -0
- alienrecon/data/rooms/thm-subdomainenumeration.yaml +327 -0
- alienrecon/data/rooms/thm-ultratech.yaml +572 -0
- alienrecon/data/rooms/thm-vulnerabilities101.yaml +325 -0
- alienrecon/data/rooms/thm-vulnerabilitycapstone.yaml +305 -0
- alienrecon/data/rooms/thm-walkinganapplication.yaml +487 -0
- alienrecon/data/rooms/thm-windowsprivesc20.yaml +741 -0
- alienrecon/data/rooms/thm-wreath.yaml +1966 -0
- alienrecon/data/rooms/thm-xss.yaml +415 -0
- alienrecon/data/rooms/thm-yearoftherabbit.yaml +687 -0
- alienrecon/data/templates/ctf_notes_template.md +118 -0
- alienrecon/display/__init__.py +0 -0
- alienrecon/display/ui.py +140 -0
- alienrecon/prompts/system_prompt.txt +246 -0
- alienrecon/prompts/terminal_simulation_prompt.txt +46 -0
- alienrecon/prompts/welcome_message.txt +7 -0
- alienrecon/prompts/welcome_message_with_target.txt +7 -0
- alienrecon/tools/__init__.py +0 -0
- alienrecon/tools/checker.py +93 -0
- alienrecon/tools/vpn.py +166 -0
- alienrecon/wordlists/dns-fast-clean.txt +58 -0
- alienrecon/wordlists/dns-fast.txt +58 -0
- alienrecon-3.0.0.dist-info/METADATA +187 -0
- alienrecon-3.0.0.dist-info/RECORD +93 -0
- alienrecon-3.0.0.dist-info/WHEEL +4 -0
- alienrecon-3.0.0.dist-info/entry_points.txt +3 -0
alienrecon/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.0.0"
|
alienrecon/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Free mode assistant — student picks target, agent assists."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from ..curriculum.profile import StudentProfile
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Assistant:
|
|
16
|
+
"""Assistant for free-form reconnaissance."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, profile: StudentProfile, dry_run: bool = False):
|
|
19
|
+
self.profile = profile
|
|
20
|
+
self.dry_run = dry_run
|
|
21
|
+
|
|
22
|
+
def start(self, target: str):
|
|
23
|
+
"""Start a free-form recon session."""
|
|
24
|
+
from ..core.input_validator import InputValidator
|
|
25
|
+
|
|
26
|
+
# Validate target
|
|
27
|
+
try:
|
|
28
|
+
validated = InputValidator.validate_target(target)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
console.print(f"[red]Invalid target: {e}[/red]")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
console.print(Panel(
|
|
34
|
+
f"[green]Target:[/green] {validated}\n"
|
|
35
|
+
f"[dim]Free mode — you lead, I assist.[/dim]\n\n"
|
|
36
|
+
f"Type commands to run, or ask me questions.\n"
|
|
37
|
+
f"Type [cyan]help[/cyan] for options, [cyan]exit[/cyan] to quit.",
|
|
38
|
+
title="Free Recon",
|
|
39
|
+
border_style="cyan",
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
# Interactive loop
|
|
43
|
+
while True:
|
|
44
|
+
try:
|
|
45
|
+
cmd = console.input("\n[green] alienrecon> [/green]")
|
|
46
|
+
except (KeyboardInterrupt, EOFError):
|
|
47
|
+
console.print("\n[yellow]Session ended.[/yellow]")
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
cmd = cmd.strip()
|
|
51
|
+
if not cmd:
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
if cmd.lower() in ("exit", "quit", "q"):
|
|
55
|
+
console.print("[yellow]Session ended.[/yellow]")
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
if cmd.lower() == "help":
|
|
59
|
+
self._show_help()
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
# For now, basic command pass-through
|
|
63
|
+
# TODO: Add smart suggestions and explanations
|
|
64
|
+
if self.dry_run:
|
|
65
|
+
console.print(f"[dim][dry-run] Would execute: {cmd}[/dim]")
|
|
66
|
+
else:
|
|
67
|
+
import subprocess
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(
|
|
70
|
+
cmd, shell=True, capture_output=True, text=True, timeout=300
|
|
71
|
+
)
|
|
72
|
+
output = result.stdout + result.stderr
|
|
73
|
+
if output.strip():
|
|
74
|
+
console.print(Panel(output.strip()[:5000], title="Output", border_style="dim"))
|
|
75
|
+
except subprocess.TimeoutExpired:
|
|
76
|
+
console.print("[red]Command timed out.[/red]")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
79
|
+
|
|
80
|
+
def _show_help(self):
|
|
81
|
+
console.print(Panel(
|
|
82
|
+
"[cyan]help[/cyan] — Show this help\n"
|
|
83
|
+
"[cyan]exit[/cyan] — End session\n"
|
|
84
|
+
"[cyan]<command>[/cyan] — Run a shell command\n\n"
|
|
85
|
+
"[dim]Tip: Try nmap, gobuster, nikto, wpscan, etc.[/dim]",
|
|
86
|
+
title="Commands",
|
|
87
|
+
border_style="cyan",
|
|
88
|
+
))
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Claude-powered brain for the AlienRecon instructor.
|
|
2
|
+
|
|
3
|
+
All Claude API calls go through the AlienRecon API proxy.
|
|
4
|
+
Users do not need their own Anthropic API key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .. import api
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_available() -> bool:
|
|
16
|
+
"""Check if the user is logged in and can use the AI instructor."""
|
|
17
|
+
return api.is_logged_in()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Instructor tools ─────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
INSTRUCTOR_TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
"name": "run_command",
|
|
25
|
+
"description": "Execute a shell command on the student's machine against the target. Use this when the student is ready to run a scan, exploit, or other tool. The output will be returned to you for discussion.",
|
|
26
|
+
"input_schema": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"properties": {
|
|
29
|
+
"command": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "The shell command to execute. Use {target} as placeholder for the target IP."
|
|
32
|
+
},
|
|
33
|
+
"explanation": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Brief explanation of what this command does (shown to student before execution)."
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": ["command"]
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "show_hint",
|
|
43
|
+
"description": "Give the student a progressive hint. Use level 1 for nudges, level 2 for more direct guidance, level 3 for nearly the answer. Only use when the student is stuck.",
|
|
44
|
+
"input_schema": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"properties": {
|
|
47
|
+
"hint": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "The hint text."
|
|
50
|
+
},
|
|
51
|
+
"level": {
|
|
52
|
+
"type": "integer",
|
|
53
|
+
"description": "Hint level: 1=nudge, 2=direct, 3=nearly answer",
|
|
54
|
+
"enum": [1, 2, 3]
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"required": ["hint", "level"]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "check_answer",
|
|
62
|
+
"description": "Check if the student's answer to a platform question is correct. Use when the student provides an answer to a TryHackMe question.",
|
|
63
|
+
"input_schema": {
|
|
64
|
+
"type": "object",
|
|
65
|
+
"properties": {
|
|
66
|
+
"question_id": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "The question ID from the YAML (e.g. t1-q1, q1)."
|
|
69
|
+
},
|
|
70
|
+
"student_answer": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "The student's answer to check."
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": ["question_id", "student_answer"]
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "advance_step",
|
|
80
|
+
"description": "Signal that the student has understood the current step and is ready to move on. Only call this when the student has demonstrated understanding of the key concept — not just after running a command.",
|
|
81
|
+
"input_schema": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"properties": {
|
|
84
|
+
"summary": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"description": "Brief summary of what was learned in this step."
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"required": ["summary"]
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# ── System prompt ────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
INSTRUCTOR_SYSTEM = """You are AlienRecon, a cybersecurity mentor walking a student through a CTF room.
|
|
97
|
+
|
|
98
|
+
You are having a CONVERSATION, not reading a script. You are the senior operator — the student is shadowing you.
|
|
99
|
+
|
|
100
|
+
## How You Teach
|
|
101
|
+
|
|
102
|
+
1. ORIENT: Explain what we need to accomplish, ask the student how they'd approach it
|
|
103
|
+
2. GUIDE: Based on their response, guide them toward the right tool/approach
|
|
104
|
+
3. EXECUTE: When ready, use the run_command tool to run the appropriate command
|
|
105
|
+
4. ANALYZE: After seeing output, ask the student what they notice before explaining
|
|
106
|
+
5. CONNECT: Help them understand why this matters and what it means for our next move
|
|
107
|
+
6. ADVANCE: When they understand the key concept, use advance_step to move on
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
- Keep responses to 2-4 sentences. This is a conversation, not a lecture.
|
|
112
|
+
- Ask questions BEFORE showing commands. Make the student think.
|
|
113
|
+
- After command output, ask what they notice before you explain.
|
|
114
|
+
- Never give flags directly — let them find flags in output.
|
|
115
|
+
- Only call advance_step when understanding is demonstrated, not just after running a command.
|
|
116
|
+
- If the student is stuck, use show_hint with progressive levels before giving answers.
|
|
117
|
+
- If they suggest a different approach, evaluate it honestly.
|
|
118
|
+
- Match their energy — if they clearly know nmap, don't explain what port scanning is.
|
|
119
|
+
|
|
120
|
+
## Context
|
|
121
|
+
|
|
122
|
+
Room: {room_name} ({difficulty})
|
|
123
|
+
Brief: {room_brief}
|
|
124
|
+
Phase: {phase_name} — {phase_objective}
|
|
125
|
+
|
|
126
|
+
## Your Cheat Sheet (DO NOT dump this to the student)
|
|
127
|
+
|
|
128
|
+
{step_knowledge}"""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
SYSTEM_PROMPT = """\
|
|
132
|
+
You are AlienRecon, a cybersecurity mentor walking a student through a CTF room.
|
|
133
|
+
|
|
134
|
+
You are the senior operator. The student is shadowing you. You show your work, \
|
|
135
|
+
explain your thinking, and answer questions. You are NOT a quiz master — you're \
|
|
136
|
+
a mentor working alongside them.
|
|
137
|
+
|
|
138
|
+
Style:
|
|
139
|
+
- Direct, clear, no fluff — like a real hacker mentoring a junior
|
|
140
|
+
- Explain the WHY, not just the WHAT
|
|
141
|
+
- Keep responses to 2-4 sentences unless explaining a concept in depth
|
|
142
|
+
- Reference real tools and real-world context
|
|
143
|
+
- Celebrate when they notice something good
|
|
144
|
+
|
|
145
|
+
Room brief: {room_brief}
|
|
146
|
+
Phase: {phase_name} — {phase_objective}
|
|
147
|
+
Step: {step_id}
|
|
148
|
+
|
|
149
|
+
{step_context}
|
|
150
|
+
|
|
151
|
+
Rules:
|
|
152
|
+
- NEVER give flags directly — let them find flags in command output
|
|
153
|
+
- If they suggest a different approach, evaluate it honestly
|
|
154
|
+
- When analyzing output, focus on what matters for THIS room, not generic observations
|
|
155
|
+
- If output looks wrong/empty, help troubleshoot specifically"""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _handle_api_error(result: dict) -> None:
|
|
159
|
+
"""Log API errors for debugging."""
|
|
160
|
+
if result.get("error"):
|
|
161
|
+
logger.warning(f"API error ({result.get(status)}): {result.get(detail)}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def ask_claude(
|
|
165
|
+
student_input: str,
|
|
166
|
+
room_name: str = "",
|
|
167
|
+
room_brief: str = "",
|
|
168
|
+
room_slug: str = "",
|
|
169
|
+
phase_name: str = "",
|
|
170
|
+
phase_objective: str = "",
|
|
171
|
+
step_id: str = "",
|
|
172
|
+
step_instruction: str = "",
|
|
173
|
+
look_for: Optional[list[str]] = None,
|
|
174
|
+
key_takeaway: str = "",
|
|
175
|
+
if_fails: str = "",
|
|
176
|
+
skill_level: str = "beginner",
|
|
177
|
+
hints_used: int = 0,
|
|
178
|
+
conversation_history: Optional[list[dict]] = None,
|
|
179
|
+
) -> Optional[str]:
|
|
180
|
+
"""Legacy v1 interface — used for intro, debrief, non-step contexts."""
|
|
181
|
+
if not is_available():
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
step_parts = []
|
|
185
|
+
if look_for:
|
|
186
|
+
step_parts.append("What to look for in output:\n" + "\n".join(f"- {item}" for item in look_for))
|
|
187
|
+
if key_takeaway:
|
|
188
|
+
step_parts.append(f"Key takeaway for student: {key_takeaway}")
|
|
189
|
+
if if_fails:
|
|
190
|
+
step_parts.append(f"If this step fails: {if_fails}")
|
|
191
|
+
step_context = "\n\n".join(step_parts)
|
|
192
|
+
|
|
193
|
+
system = SYSTEM_PROMPT.format(
|
|
194
|
+
room_brief=room_brief or f"Room: {room_name}" if room_name else "Free mode",
|
|
195
|
+
phase_name=phase_name or "N/A",
|
|
196
|
+
phase_objective=phase_objective or "N/A",
|
|
197
|
+
step_id=step_id or "N/A",
|
|
198
|
+
step_context=step_context or "No additional context for this step.",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
messages = []
|
|
202
|
+
if conversation_history:
|
|
203
|
+
for entry in conversation_history[-20:]:
|
|
204
|
+
messages.append(entry)
|
|
205
|
+
messages.append({"role": "user", "content": student_input})
|
|
206
|
+
|
|
207
|
+
result = api.chat_sync(
|
|
208
|
+
messages=messages,
|
|
209
|
+
system=system,
|
|
210
|
+
room_slug=room_slug,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if result.get("error"):
|
|
214
|
+
_handle_api_error(result)
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
# Parse Anthropic response format
|
|
218
|
+
try:
|
|
219
|
+
content = result.get("content", [])
|
|
220
|
+
for block in content:
|
|
221
|
+
if block.get("type") == "text":
|
|
222
|
+
return block.get("text")
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.warning(f"Failed to parse response: {e}")
|
|
225
|
+
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def teach_step(
|
|
230
|
+
message: str,
|
|
231
|
+
step_knowledge: dict,
|
|
232
|
+
phase_name: str = "",
|
|
233
|
+
phase_objective: str = "",
|
|
234
|
+
room_name: str = "",
|
|
235
|
+
room_brief: str = "",
|
|
236
|
+
room_slug: str = "",
|
|
237
|
+
difficulty: str = "",
|
|
238
|
+
conversation_history: Optional[list[dict]] = None,
|
|
239
|
+
):
|
|
240
|
+
"""Conversation-driven Claude call with tool_use.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
tuple: (text_response: str, tool_calls: list[dict])
|
|
244
|
+
"""
|
|
245
|
+
if not is_available():
|
|
246
|
+
return None, []
|
|
247
|
+
|
|
248
|
+
# Build step knowledge text
|
|
249
|
+
parts = []
|
|
250
|
+
if step_knowledge.get("step_id"):
|
|
251
|
+
parts.append(f"Step: {step_knowledge[step_id]}")
|
|
252
|
+
if step_knowledge.get("narration"):
|
|
253
|
+
parts.append(f"What to teach: {step_knowledge[narration]}")
|
|
254
|
+
if step_knowledge.get("command"):
|
|
255
|
+
parts.append(f"Command to run: {step_knowledge[command]}")
|
|
256
|
+
if step_knowledge.get("expected_output"):
|
|
257
|
+
out = step_knowledge["expected_output"]
|
|
258
|
+
if len(out) > 1500:
|
|
259
|
+
out = out[:1500] + "..."
|
|
260
|
+
parts.append(f"Expected output:\n{out}")
|
|
261
|
+
if step_knowledge.get("explanation"):
|
|
262
|
+
parts.append(f"Technical explanation: {step_knowledge[explanation]}")
|
|
263
|
+
if step_knowledge.get("look_for"):
|
|
264
|
+
parts.append("Key observations:\n" + "\n".join(f" - {i}" for i in step_knowledge["look_for"]))
|
|
265
|
+
if step_knowledge.get("key_takeaway"):
|
|
266
|
+
parts.append(f"Key takeaway: {step_knowledge[key_takeaway]}")
|
|
267
|
+
if step_knowledge.get("if_fails"):
|
|
268
|
+
parts.append(f"If command fails: {step_knowledge[if_fails]}")
|
|
269
|
+
if step_knowledge.get("conversation"):
|
|
270
|
+
parts.append(f"Discussion prompt: {step_knowledge[conversation]}")
|
|
271
|
+
if step_knowledge.get("questions"):
|
|
272
|
+
for q in step_knowledge["questions"]:
|
|
273
|
+
parts.append(f"Question: {q.get(text, )} (id: {q.get(id, )}, answer: {q.get(answer, N/A)})")
|
|
274
|
+
|
|
275
|
+
system = INSTRUCTOR_SYSTEM.format(
|
|
276
|
+
room_name=room_name or "Unknown",
|
|
277
|
+
difficulty=difficulty or "unknown",
|
|
278
|
+
room_brief=room_brief or "No brief.",
|
|
279
|
+
phase_name=phase_name or "N/A",
|
|
280
|
+
phase_objective=phase_objective or "N/A",
|
|
281
|
+
step_knowledge="\n".join(parts) if parts else "No additional context.",
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
messages = []
|
|
285
|
+
if conversation_history:
|
|
286
|
+
messages.extend(conversation_history[-30:])
|
|
287
|
+
messages.append({"role": "user", "content": message})
|
|
288
|
+
|
|
289
|
+
result = api.chat_sync(
|
|
290
|
+
messages=messages,
|
|
291
|
+
system=system,
|
|
292
|
+
tools=INSTRUCTOR_TOOLS,
|
|
293
|
+
room_slug=room_slug,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if result.get("error"):
|
|
297
|
+
_handle_api_error(result)
|
|
298
|
+
return None, []
|
|
299
|
+
|
|
300
|
+
# Parse Anthropic response format
|
|
301
|
+
text_parts = []
|
|
302
|
+
tool_calls = []
|
|
303
|
+
try:
|
|
304
|
+
content = result.get("content", [])
|
|
305
|
+
for block in content:
|
|
306
|
+
if block.get("type") == "text":
|
|
307
|
+
text_parts.append(block.get("text", ""))
|
|
308
|
+
elif block.get("type") == "tool_use":
|
|
309
|
+
tool_calls.append({
|
|
310
|
+
"id": block.get("id"),
|
|
311
|
+
"name": block.get("name"),
|
|
312
|
+
"input": block.get("input", {}),
|
|
313
|
+
})
|
|
314
|
+
except Exception as e:
|
|
315
|
+
logger.warning(f"Failed to parse response: {e}")
|
|
316
|
+
|
|
317
|
+
return "\n".join(text_parts) if text_parts else None, tool_calls
|