added AI support for yaml/run
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
__version__ = "6.0.0"
|
||||
__version__ = "6.0.1"
|
||||
|
||||
+315
-2
@@ -114,6 +114,7 @@ class ai:
|
||||
self.confirm_handler = confirm_handler or self._local_confirm_handler
|
||||
self.trusted_session = trust # Trust mode for the entire session
|
||||
self.interrupted = False
|
||||
self.one_shot = kwargs.get("one_shot", False)
|
||||
|
||||
|
||||
# 1. Cargar configuración genérica con herencia/merge global
|
||||
@@ -285,10 +286,13 @@ class ai:
|
||||
@property
|
||||
def architect_system_prompt(self):
|
||||
"""Build architect system prompt with plugin extensions."""
|
||||
prompt = self._architect_base_prompt
|
||||
if getattr(self, "one_shot", False):
|
||||
prompt += "\n\nCRITICAL 1-SHOT DIAGNOSTICS DIRECTIVE:\nYou are running in a 1-shot offline diagnostics mode. There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately and directly to the user. Do not suggest or attempt to delegate/return control to the engineer."
|
||||
if self.architect_prompt_extensions:
|
||||
extensions = "\n".join(self.architect_prompt_extensions)
|
||||
return self._architect_base_prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return self._architect_base_prompt
|
||||
return prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return prompt
|
||||
|
||||
def register_ai_tool(self, tool_definition, handler, target="engineer", engineer_prompt=None, architect_prompt=None, status_formatter=None):
|
||||
"""Register an external tool for the AI system.
|
||||
@@ -880,6 +884,8 @@ class ai:
|
||||
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
||||
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
||||
]
|
||||
if getattr(self, "one_shot", False):
|
||||
base_tools = [t for t in base_tools if t["function"]["name"] not in ("delegate_to_engineer", "return_to_engineer")]
|
||||
|
||||
all_tools = base_tools + self.external_architect_tools
|
||||
seen_names = set()
|
||||
@@ -1624,3 +1630,310 @@ Node: {node_name}"""
|
||||
|
||||
@MethodHook
|
||||
def confirm(self, user_input): return True
|
||||
|
||||
|
||||
PLAYBOOK_BUILDER_SYSTEM_PROMPT = """
|
||||
You are a Connpy Playbook Builder Agent, a specialist in creating structured Connpy automation playbooks in YAML format.
|
||||
Your primary mission is to help the user build, refine, and validate playbooks.
|
||||
|
||||
You MUST follow the Connpy canonical playbook format strictly:
|
||||
The playbook MUST always use the `tasks[]` array structure as the root key, where each task is sequential and independent.
|
||||
|
||||
Connpy YAML Playbook Canonical Schema:
|
||||
---
|
||||
tasks:
|
||||
- name: "Task Description"
|
||||
action: 'run' # Can be 'run' or 'test'. Mandatory.
|
||||
nodes: # List of nodes filter or regular expressions to work on. Mandatory. Can be a string or array of strings. Supports regex (e.g. 'router.*@office' to match all routers in the 'office' folder).
|
||||
- 'router1@office'
|
||||
- 'router.*@office' # Regex filters are fully supported to match multiple nodes dynamically.
|
||||
- '@aws'
|
||||
commands: # List of CLI commands to execute. Mandatory.
|
||||
- 'show version'
|
||||
variables: # Key-value pairs for variables replacement in commands and expected. Optional.
|
||||
__global__: # Global variables fallback. Optional.
|
||||
key: value
|
||||
node_name@folder: # Node-specific variables. Optional.
|
||||
key: value
|
||||
output: stdout # Mandatory. Output configuration. Choices: 'stdout', 'null', or a folder path like '/path/to/folder'.
|
||||
options: # Execution options. Optional.
|
||||
prompt: 'regex_prompt' # Optional prompt to expect.
|
||||
parallel: 10 # Optional number of parallel threads. Default 10.
|
||||
timeout: 20 # Optional execution timeout in seconds. Default 20.
|
||||
|
||||
- name: "Verification Task"
|
||||
action: 'test'
|
||||
nodes:
|
||||
- 'router1@office'
|
||||
commands:
|
||||
- 'ping 10.100.100.1'
|
||||
expected: '!' # Expected text pattern to search in output. Mandatory ONLY for 'test' action.
|
||||
|
||||
Connpy Variable Templating & Usage:
|
||||
- Variables defined under the `variables` key (either globally under `__global__` or for specific nodes) are used in commands or expected output by surrounding the variable name with single curly braces: `{variable_name}`.
|
||||
- Example: If you define a variable `ip` with a value of `10.100.100.1`, you use it in commands as `'ping {ip}'`.
|
||||
- Recommendation (Important): Variables are not limited to simple words or values. You can define entire CLI commands as variables to abstract vendor-specific syntax! This is highly recommended when executing the same logical operation across different operating systems (OS) or vendors.
|
||||
- Example: You can define `show_interface_cmd` under a specific node's variables to be `'show ip interface brief'` for Cisco, and `'show interfaces terse'` for Juniper, and then write a single generic command under `commands`:
|
||||
`- '{show_interface_cmd}'`
|
||||
|
||||
Guidelines:
|
||||
1. When the user requests a playbook, you should guide them and output the YAML.
|
||||
2. IMPORTANT: You have access to the `list_nodes` tool. Proactively use it to inspect the user's real inventory. This allows you to discover correct node names, folders, or device tags, and construct precise regex filters for the `nodes` field based on real assets.
|
||||
3. IMPORTANT: Before presenting the playbook, you MUST call the `validate_playbook` tool with the YAML to let the backend check for syntax and schema correctness.
|
||||
4. If `validate_playbook` returns errors, fix them in your YAML and validate again before responding to the user.
|
||||
5. When the playbook is complete, validated, and the user approves it, you MUST call the `return_playbook` tool to return the final YAML.
|
||||
6. All text responses must be in the same language the user uses in their prompt.
|
||||
"""
|
||||
|
||||
PLAYBOOK_BUILDER_TOOLS = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "list_nodes",
|
||||
"description": "[Universal Platform] Lists available nodes in the inventory. Use this to discover device names, folders, or operating systems to build proper regex filters.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"filter_pattern": {
|
||||
"type": "STRING",
|
||||
"description": "Regex or pattern to filter nodes (e.g. '.*', 'border.*', '@office')."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "validate_playbook",
|
||||
"description": "Validates the Connpy YAML playbook structure, syntax, and schema correctness with the backend.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"playbook_yaml": {
|
||||
"type": "STRING",
|
||||
"description": "The YAML content of the playbook to validate."
|
||||
}
|
||||
},
|
||||
"required": ["playbook_yaml"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "return_playbook",
|
||||
"description": "Returns the final validated YAML playbook to the calling application when the user is satisfied.",
|
||||
"parameters": {
|
||||
"type": "OBJECT",
|
||||
"properties": {
|
||||
"playbook_yaml": {
|
||||
"type": "STRING",
|
||||
"description": "The final YAML content of the playbook."
|
||||
}
|
||||
},
|
||||
"required": ["playbook_yaml"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
class PlaybookBuilderAgent:
|
||||
"""Specialized AI agent for building, validating, and generating Connpy YAML playbooks."""
|
||||
|
||||
def __init__(self, config, console=None, confirm_handler=None, trust=False, **kwargs):
|
||||
self.config = config
|
||||
self.console = console or printer.console
|
||||
self.interrupted = False
|
||||
|
||||
# Load AI configuration
|
||||
if hasattr(self.config, "get_effective_setting"):
|
||||
aiconfig = self.config.get_effective_setting("ai", {})
|
||||
else:
|
||||
aiconfig = self.config.config.get("ai", {}) if hasattr(self.config, "config") else {}
|
||||
|
||||
# Default model for technical tasks
|
||||
self.model = kwargs.get("engineer_model") or aiconfig.get("engineer_model") or "gemini/gemini-3.1-flash-lite"
|
||||
self.key = kwargs.get("engineer_api_key") or aiconfig.get("engineer_api_key")
|
||||
self.auth = kwargs.get("engineer_auth") or aiconfig.get("engineer_auth") or {}
|
||||
if self.key and "api_key" not in self.auth:
|
||||
self.auth = self.auth.copy()
|
||||
self.auth["api_key"] = self.key
|
||||
|
||||
def validate_playbook(self, playbook_yaml: str) -> dict:
|
||||
"""Sintactical and schema validation of Connpy Playbook YAML."""
|
||||
import yaml
|
||||
try:
|
||||
# 1. Parse YAML
|
||||
data = yaml.load(playbook_yaml, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
return {"valid": False, "error": f"YAML Syntax Error: {e}"}
|
||||
|
||||
# 2. Check structure
|
||||
if not isinstance(data, dict):
|
||||
return {"valid": False, "error": "Playbook must be a YAML dictionary."}
|
||||
|
||||
if "tasks" not in data:
|
||||
return {"valid": False, "error": "Playbook missing mandatory root 'tasks' key."}
|
||||
|
||||
tasks = data["tasks"]
|
||||
if not isinstance(tasks, list):
|
||||
return {"valid": False, "error": "'tasks' must be a list of tasks."}
|
||||
|
||||
# 3. Check individual tasks
|
||||
for idx, task in enumerate(tasks):
|
||||
if not isinstance(task, dict):
|
||||
return {"valid": False, "error": f"Task index {idx} must be a dictionary."}
|
||||
|
||||
name = task.get("name", f"Task {idx}")
|
||||
|
||||
# Mandatory fields
|
||||
mandatory = ["name", "action", "nodes", "commands", "output"]
|
||||
missing = [field for field in mandatory if field not in task]
|
||||
if missing:
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) is missing mandatory fields: {missing}"}
|
||||
|
||||
# Validate nodes field type (supports string regexes or array of string regexes)
|
||||
nodes = task["nodes"]
|
||||
if not isinstance(nodes, (str, list)):
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) 'nodes' must be a string (regex) or a list of strings (regexes)."}
|
||||
|
||||
if isinstance(nodes, list):
|
||||
for n_idx, node_item in enumerate(nodes):
|
||||
if not isinstance(node_item, str):
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) 'nodes' list contains a non-string value at index {n_idx}: {node_item}"}
|
||||
|
||||
action = task["action"]
|
||||
if action not in ["run", "test"]:
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) has invalid action '{action}'. Choices are: 'run', 'test'."}
|
||||
|
||||
if action == "test" and "expected" not in task:
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) has action 'test' but is missing the mandatory 'expected' key."}
|
||||
|
||||
output = task["output"]
|
||||
if output not in [None, "stdout"] and not output.startswith("/"):
|
||||
return {"valid": False, "error": f"Task '{name}' (index {idx}) output '{output}' is invalid. Must be 'stdout', 'null' or an absolute path."}
|
||||
|
||||
return {"valid": True, "message": "Playbook schema and syntax is valid."}
|
||||
|
||||
def ask(self, user_input, chat_history=None, status=None, debug=False, chunk_callback=None):
|
||||
"""Standard conversation step with tool loop for PlaybookBuilderAgent."""
|
||||
if chat_history is None:
|
||||
chat_history = []
|
||||
|
||||
# System prompt and tool definition
|
||||
system_prompt = PLAYBOOK_BUILDER_SYSTEM_PROMPT
|
||||
tools = PLAYBOOK_BUILDER_TOOLS
|
||||
messages = [{"role": "system", "content": system_prompt}]
|
||||
|
||||
for msg in chat_history:
|
||||
m = msg if isinstance(msg, dict) else msg.copy()
|
||||
if m.get('role') == 'assistant' and m.get('tool_calls') and m.get('content') == "":
|
||||
m['content'] = None
|
||||
messages.append(m)
|
||||
|
||||
messages.append({"role": "user", "content": user_input})
|
||||
|
||||
final_playbook_yaml = None
|
||||
iteration = 0
|
||||
max_iterations = 10
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
|
||||
if status:
|
||||
status.update(f"Playbook Agent is thinking... (step {iteration})")
|
||||
|
||||
# Call LiteLLM completion
|
||||
from connpy.ai import completion
|
||||
try:
|
||||
response = completion(
|
||||
model=self.model,
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
num_retries=3,
|
||||
**self.auth
|
||||
)
|
||||
except Exception as e:
|
||||
return {"response": f"Playbook Agent failed: {str(e)}", "chat_history": messages[1:]}
|
||||
|
||||
resp_msg = response.choices[0].message
|
||||
msg_dict = resp_msg.model_dump(exclude_none=True)
|
||||
if msg_dict.get("tool_calls") and msg_dict.get("content") == "":
|
||||
msg_dict["content"] = None
|
||||
|
||||
messages.append(msg_dict)
|
||||
|
||||
# If the model sends content, stream or yield it
|
||||
if resp_msg.content:
|
||||
if chunk_callback:
|
||||
chunk_callback(resp_msg.content)
|
||||
elif not resp_msg.tool_calls:
|
||||
# In direct non-streaming output, print markdown
|
||||
self.console.print(Markdown(resp_msg.content))
|
||||
|
||||
if not resp_msg.tool_calls:
|
||||
break
|
||||
|
||||
for tc in resp_msg.tool_calls:
|
||||
fn = tc.function.name
|
||||
args = json.loads(tc.function.arguments)
|
||||
|
||||
if fn == "list_nodes":
|
||||
filter_pattern = args.get("filter_pattern", ".*")
|
||||
try:
|
||||
matched_names = self.config._getallnodes(filter_pattern)
|
||||
if not matched_names:
|
||||
obs = "No nodes found matching the filter."
|
||||
else:
|
||||
if len(matched_names) <= 5:
|
||||
matched_data = self.config.getitems(matched_names, extract=True)
|
||||
res = {}
|
||||
for name, data in matched_data.items():
|
||||
os_tag = "unknown"
|
||||
if isinstance(data, dict):
|
||||
ts = data.get("tags")
|
||||
if isinstance(ts, dict): os_tag = ts.get("os", "unknown")
|
||||
res[name] = {"os": os_tag}
|
||||
obs = json.dumps(res)
|
||||
else:
|
||||
obs = json.dumps({
|
||||
"matched_count": len(matched_names),
|
||||
"message": "Too many nodes matched. Showing names only.",
|
||||
"node_names": matched_names
|
||||
})
|
||||
except Exception as e:
|
||||
obs = f"Error listing nodes: {e}"
|
||||
messages.append({
|
||||
"tool_call_id": tc.id,
|
||||
"role": "tool",
|
||||
"name": fn,
|
||||
"content": obs
|
||||
})
|
||||
elif fn == "validate_playbook":
|
||||
playbook_yaml = args.get("playbook_yaml", "")
|
||||
validation_res = self.validate_playbook(playbook_yaml)
|
||||
messages.append({
|
||||
"tool_call_id": tc.id,
|
||||
"role": "tool",
|
||||
"name": fn,
|
||||
"content": json.dumps(validation_res)
|
||||
})
|
||||
elif fn == "return_playbook":
|
||||
final_playbook_yaml = args.get("playbook_yaml", "")
|
||||
messages.append({
|
||||
"tool_call_id": tc.id,
|
||||
"role": "tool",
|
||||
"name": fn,
|
||||
"content": json.dumps({"success": True, "message": "Playbook returned successfully."})
|
||||
})
|
||||
|
||||
# If return_playbook was called, we can terminate early
|
||||
if final_playbook_yaml is not None:
|
||||
break
|
||||
|
||||
return {
|
||||
"response": resp_msg.content or "",
|
||||
"chat_history": messages[1:],
|
||||
"playbook_yaml": final_playbook_yaml
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class AIHandler:
|
||||
|
||||
def single_question(self, args, session_id):
|
||||
query = " ".join(args.ask)
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...") as status:
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(query, status=status, debug=args.debug, session_id=session_id, trust=args.trust, **self.ai_overrides)
|
||||
|
||||
responder = result.get("responder", "engineer")
|
||||
@@ -131,7 +131,7 @@ class AIHandler:
|
||||
if not user_query.strip(): continue
|
||||
if user_query.lower() in ['exit', 'quit', 'bye', 'cancel']: break
|
||||
|
||||
with console.status("[ai_status]Agent is thinking...") as status:
|
||||
with console.status("[ai_status]Agent is thinking...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
|
||||
|
||||
new_history = result.get("chat_history")
|
||||
|
||||
+286
-2
@@ -15,7 +15,12 @@ class RunHandler:
|
||||
def dispatch(self, args):
|
||||
if len(args.data) > 1:
|
||||
args.action = "noderun"
|
||||
actions = {"noderun": self.node_run, "generate": self.yaml_generate, "run": self.yaml_run}
|
||||
actions = {
|
||||
"noderun": self.node_run,
|
||||
"generate": self.yaml_generate,
|
||||
"generate_ai": self.ai_generate,
|
||||
"run": self.yaml_run
|
||||
}
|
||||
return actions.get(args.action)(args)
|
||||
|
||||
def node_run(self, args):
|
||||
@@ -33,6 +38,41 @@ class RunHandler:
|
||||
|
||||
commands = [" ".join(args.data[1:])]
|
||||
|
||||
# Check for Preflight AI simulation
|
||||
if getattr(args, "preflight_ai", False):
|
||||
matched_node_names = [n.get("name") if isinstance(n, dict) else n for n in matched_nodes]
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
matched_node_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed: {e}")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
header_printed = False
|
||||
|
||||
@@ -70,6 +110,40 @@ class RunHandler:
|
||||
)
|
||||
printer.run_summary(results)
|
||||
|
||||
# Analyze execution results if requested
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else " ".join(args.data[1:])
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
@@ -90,8 +164,105 @@ class RunHandler:
|
||||
with open(path, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
# Check preflight first before any task runs
|
||||
if getattr(args, "preflight_ai", False):
|
||||
preflight_failed = False
|
||||
for task in playbook.get("tasks", []):
|
||||
name = task.get("name", "Task")
|
||||
nodelist = task.get("nodes", [])
|
||||
commands = task.get("commands", [])
|
||||
|
||||
# Resolve nodes to names
|
||||
try:
|
||||
if isinstance(nodelist, str):
|
||||
resolved_nodes = self.app.services.nodes.list_nodes(nodelist)
|
||||
elif isinstance(nodelist, list):
|
||||
resolved_nodes = []
|
||||
for item in nodelist:
|
||||
matches = self.app.services.nodes.list_nodes(item)
|
||||
for m in matches:
|
||||
if m not in resolved_nodes:
|
||||
resolved_nodes.append(m)
|
||||
else:
|
||||
resolved_nodes = []
|
||||
except Exception:
|
||||
resolved_nodes = []
|
||||
|
||||
resolved_names = [n.get("name") if isinstance(n, dict) else n for n in resolved_nodes]
|
||||
printer.console.print(f"\n[bold]Task: {name}[/bold] (Preflight for {len(resolved_names)} nodes)")
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
resolved_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed for task {name}: {e}")
|
||||
preflight_failed = True
|
||||
if preflight_failed:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
# Standard run
|
||||
results_all = {}
|
||||
for task in playbook.get("tasks", []):
|
||||
self.cli_run(task)
|
||||
task_res = self.cli_run(task)
|
||||
if task_res:
|
||||
results_all.update(task_res)
|
||||
|
||||
# If analyze is enabled, run analysis on accumulated results
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing playbook execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else f"Playbook: {path}"
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results_all,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to run playbook {path}: {e}")
|
||||
@@ -136,6 +307,7 @@ class RunHandler:
|
||||
|
||||
nodelist = resolved_nodes
|
||||
|
||||
results = {}
|
||||
try:
|
||||
header_printed = False
|
||||
if action == "run":
|
||||
@@ -195,6 +367,118 @@ class RunHandler:
|
||||
)
|
||||
# ALWAYS show the aggregate summary at the end
|
||||
printer.test_summary(results)
|
||||
|
||||
return results
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))
|
||||
return {}
|
||||
|
||||
def ai_generate(self, args):
|
||||
from rich.prompt import Prompt
|
||||
from rich.rule import Rule
|
||||
from rich.panel import Panel
|
||||
from rich.syntax import Syntax
|
||||
|
||||
dest_file = args.data[0]
|
||||
if os.path.exists(dest_file):
|
||||
printer.error(f"File '{dest_file}' already exists.")
|
||||
sys.exit(14)
|
||||
|
||||
chat_history = []
|
||||
|
||||
# Consistent layout opening matching global AI (engineer style)
|
||||
from rich.markdown import Markdown
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n"))
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Operation cancelled by user.")
|
||||
break
|
||||
|
||||
if user_prompt.strip().lower() in ["exit", "quit"]:
|
||||
printer.info("Exiting AI Assistant.")
|
||||
break
|
||||
|
||||
if not user_prompt.strip():
|
||||
continue
|
||||
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Agent is thinking...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Playbook Builder AI[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
res = self.app.services.ai.build_playbook_chat(
|
||||
user_prompt,
|
||||
chat_history=chat_history,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
renderer.flush()
|
||||
if not first_chunk:
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
# Update history
|
||||
if res and "chat_history" in res:
|
||||
chat_history = res["chat_history"]
|
||||
|
||||
# Check if the agent returned a validated playbook YAML
|
||||
if res and "playbook_yaml" in res and res["playbook_yaml"]:
|
||||
yaml_content = res["playbook_yaml"]
|
||||
printer.console.print()
|
||||
printer.success("Playbook YAML successfully generated and validated.")
|
||||
|
||||
# Show the YAML inside a beautiful panel matching AI style (with engineer borders)
|
||||
syntax = Syntax(yaml_content, "yaml", theme="ansi_dark", word_wrap=True, background_color="default")
|
||||
panel = Panel(syntax, title="[engineer][bold]Resulting Playbook[/bold][/engineer]", border_style="engineer", expand=False)
|
||||
printer.console.print(panel)
|
||||
|
||||
# Ask if the user wants to save it
|
||||
try:
|
||||
save_confirm = Prompt.ask(
|
||||
f"\nDo you want to save this playbook to '{dest_file}'?",
|
||||
choices=["y", "n", "run"],
|
||||
default="y"
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Saving skipped.")
|
||||
break
|
||||
|
||||
choice = save_confirm.strip().lower()
|
||||
if choice in ["y", "yes", "run"]:
|
||||
with open(dest_file, "w") as f:
|
||||
f.write(yaml_content)
|
||||
printer.success(f"Playbook saved successfully to '{dest_file}'")
|
||||
if choice == "run":
|
||||
printer.console.print()
|
||||
printer.info("Executing the saved playbook...")
|
||||
self.yaml_run(args)
|
||||
break
|
||||
else:
|
||||
printer.warning("Playbook not saved. You can continue describing changes or exit.")
|
||||
except Exception as e:
|
||||
printer.error(f"Error in AI chat: {e}")
|
||||
|
||||
@@ -169,12 +169,17 @@ def _build_tree(nodes, folders, profiles, plugins, configdir):
|
||||
run_after_node.update({
|
||||
"--test": {"*": run_after_node},
|
||||
"-t": {"*": run_after_node},
|
||||
"--analyze": {"*": run_after_node},
|
||||
"--preflight-ai": run_after_node,
|
||||
"*": run_after_node # Consume commands
|
||||
})
|
||||
|
||||
run_dict = {
|
||||
"--generate": {"__extra__": lambda w: get_cwd(w, "--generate")},
|
||||
"-g": {"__extra__": lambda w: get_cwd(w, "-g")},
|
||||
"--generate-ai": {"__extra__": lambda w: get_cwd(w, "--generate-ai")},
|
||||
"--analyze": {"*": run_after_node},
|
||||
"--preflight-ai": run_after_node,
|
||||
"--test": {"*": None},
|
||||
"-t": {"*": None},
|
||||
"--help": None,
|
||||
|
||||
@@ -303,6 +303,9 @@ class connapp:
|
||||
runparser.add_argument("run", nargs='+', action=self._store_type, help=get_help("run"), default="run").completer = nodes_completer
|
||||
runparser.add_argument("-t", "--test", dest="test_expected", nargs='+', help="Expected text(s) to validate in output. Converts the action from 'run' to 'test'")
|
||||
runparser.add_argument("-g","--generate", dest="action", action="store_const", help="Generate yaml file template", const="generate", default="run")
|
||||
runparser.add_argument("--generate-ai", dest="action", action="store_const", help="Generate a playbook interactively with AI assistance", const="generate_ai")
|
||||
runparser.add_argument("--analyze", nargs='?', const="", help="Analyze actual command execution results using AI")
|
||||
runparser.add_argument("--preflight-ai", action="store_true", help="Simulate and predict command execution on devices using AI preventively")
|
||||
runparser.set_defaults(func=self._run.dispatch)
|
||||
#APIPARSER
|
||||
apiparser = subparsers.add_parser("api", help="Start and stop connpy API", description="Start and stop connpy API", formatter_class=RichHelpFormatter)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1542,11 +1542,6 @@ class ExecutionServiceStub(object):
|
||||
request_serializer=connpy__pb2.ScriptRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.run_yaml_playbook = channel.unary_unary(
|
||||
'/connpy.ExecutionService/run_yaml_playbook',
|
||||
request_serializer=connpy__pb2.ScriptRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class ExecutionServiceServicer(object):
|
||||
@@ -1570,12 +1565,6 @@ class ExecutionServiceServicer(object):
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def run_yaml_playbook(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_ExecutionServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
@@ -1594,11 +1583,6 @@ def add_ExecutionServiceServicer_to_server(servicer, server):
|
||||
request_deserializer=connpy__pb2.ScriptRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
'run_yaml_playbook': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.run_yaml_playbook,
|
||||
request_deserializer=connpy__pb2.ScriptRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'connpy.ExecutionService', rpc_method_handlers)
|
||||
@@ -1691,33 +1675,6 @@ class ExecutionService(object):
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def run_yaml_playbook(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/connpy.ExecutionService/run_yaml_playbook',
|
||||
connpy__pb2.ScriptRequest.SerializeToString,
|
||||
connpy__pb2.StructResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class ImportExportServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
@@ -1931,6 +1888,21 @@ class AIServiceStub(object):
|
||||
request_serializer=connpy__pb2.StringRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.build_playbook_chat = channel.stream_stream(
|
||||
'/connpy.AIService/build_playbook_chat',
|
||||
request_serializer=connpy__pb2.AskRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.analyze_execution_results = channel.unary_stream(
|
||||
'/connpy.AIService/analyze_execution_results',
|
||||
request_serializer=connpy__pb2.AnalyzeRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.predict_execution_results = channel.unary_stream(
|
||||
'/connpy.AIService/predict_execution_results',
|
||||
request_serializer=connpy__pb2.PreflightRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class AIServiceServicer(object):
|
||||
@@ -1990,6 +1962,24 @@ class AIServiceServicer(object):
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def build_playbook_chat(self, request_iterator, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def analyze_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def predict_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
|
||||
def add_AIServiceServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
@@ -2038,6 +2028,21 @@ def add_AIServiceServicer_to_server(servicer, server):
|
||||
request_deserializer=connpy__pb2.StringRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
'build_playbook_chat': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.build_playbook_chat,
|
||||
request_deserializer=connpy__pb2.AskRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
'analyze_execution_results': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.analyze_execution_results,
|
||||
request_deserializer=connpy__pb2.AnalyzeRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
'predict_execution_results': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.predict_execution_results,
|
||||
request_deserializer=connpy__pb2.PreflightRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'connpy.AIService', rpc_method_handlers)
|
||||
@@ -2292,6 +2297,87 @@ class AIService(object):
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def build_playbook_chat(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(
|
||||
request_iterator,
|
||||
target,
|
||||
'/connpy.AIService/build_playbook_chat',
|
||||
connpy__pb2.AskRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def analyze_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/analyze_execution_results',
|
||||
connpy__pb2.AnalyzeRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def predict_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/predict_execution_results',
|
||||
connpy__pb2.PreflightRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
|
||||
class SystemServiceStub(object):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
+86
-21
@@ -791,11 +791,6 @@ class ExecutionServicer(connpy_pb2_grpc.ExecutionServiceServicer):
|
||||
res = self.service.run_cli_script(request.param1, request.param2, request.parallel)
|
||||
return connpy_pb2.StructResponse(data=to_struct(res))
|
||||
|
||||
@handle_errors
|
||||
def run_yaml_playbook(self, request, context):
|
||||
res = self.service.run_yaml_playbook(request.param1, request.parallel)
|
||||
return connpy_pb2.StructResponse(data=to_struct(res))
|
||||
|
||||
class ImportExportServicer(connpy_pb2_grpc.ImportExportServiceServicer):
|
||||
def __init__(self, provider, registry=None):
|
||||
if not hasattr(provider, "mode"):
|
||||
@@ -955,12 +950,10 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
||||
def service(self):
|
||||
return self._get_provider().ai
|
||||
|
||||
@handle_errors
|
||||
def ask(self, request_iterator, context):
|
||||
def _handle_chat_stream(self, request_iterator, context, service_method):
|
||||
import queue
|
||||
import threading
|
||||
|
||||
ai_service = self.service
|
||||
chunk_queue = queue.Queue()
|
||||
request_queue = queue.Queue()
|
||||
bridge = None
|
||||
@@ -978,21 +971,28 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
||||
nonlocal history, bridge, agent_instance
|
||||
try:
|
||||
# Run the AI interaction (this blocks this specific thread)
|
||||
res = ai_service.ask(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
session_id=session_id,
|
||||
debug=debug,
|
||||
status=bridge,
|
||||
console=bridge,
|
||||
confirm_handler=bridge.confirm,
|
||||
chunk_callback=callback,
|
||||
trust=trust,
|
||||
**overrides
|
||||
)
|
||||
if getattr(service_method, "__name__", None) == "build_playbook_chat":
|
||||
res = service_method(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
status=bridge,
|
||||
chunk_callback=callback
|
||||
)
|
||||
else:
|
||||
res = service_method(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
session_id=session_id,
|
||||
debug=debug,
|
||||
status=bridge,
|
||||
confirm_handler=bridge.confirm,
|
||||
chunk_callback=callback,
|
||||
trust=trust,
|
||||
**overrides
|
||||
)
|
||||
|
||||
# Update history for next message
|
||||
if "chat_history" in res:
|
||||
if res and "chat_history" in res:
|
||||
history = res["chat_history"]
|
||||
|
||||
# Send final chunk marker
|
||||
@@ -1086,6 +1086,71 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
||||
elif msg_type == "final_mark":
|
||||
yield connpy_pb2.AIResponse(is_final=True, full_result=to_struct(val))
|
||||
|
||||
def _handle_unary_stream(self, service_method, *args, **kwargs):
|
||||
import queue
|
||||
import threading
|
||||
|
||||
chunk_queue = queue.Queue()
|
||||
bridge = StatusBridge(chunk_queue, is_web=False)
|
||||
|
||||
def callback(chunk):
|
||||
chunk_queue.put(("text", chunk))
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
res = service_method(*args, chunk_callback=callback, status=bridge, **kwargs)
|
||||
chunk_queue.put(("final_mark", res))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"gRPC Unary Stream error: {e}")
|
||||
traceback.print_exc()
|
||||
chunk_queue.put(("status", f"Error: {str(e)}"))
|
||||
chunk_queue.put(("final_mark", {"response": f"Error: {str(e)}", "error": True}))
|
||||
finally:
|
||||
chunk_queue.put((None, None))
|
||||
|
||||
threading.Thread(target=_worker, daemon=True).start()
|
||||
|
||||
while True:
|
||||
item = chunk_queue.get()
|
||||
if item == (None, None):
|
||||
break
|
||||
|
||||
msg_type, val = item
|
||||
if msg_type == "text":
|
||||
yield connpy_pb2.AIResponse(text_chunk=val, is_final=False)
|
||||
elif msg_type == "status":
|
||||
clean_val = val.replace("[ai_status]", "").replace("[/ai_status]", "")
|
||||
yield connpy_pb2.AIResponse(status_update=clean_val, is_final=False)
|
||||
elif msg_type == "debug":
|
||||
yield connpy_pb2.AIResponse(debug_message=val, is_final=False)
|
||||
elif msg_type == "important":
|
||||
yield connpy_pb2.AIResponse(important_message=val, is_final=False)
|
||||
elif msg_type == "confirm":
|
||||
yield connpy_pb2.AIResponse(status_update=val, requires_confirmation=True, is_final=False)
|
||||
elif msg_type == "final_mark":
|
||||
yield connpy_pb2.AIResponse(is_final=True, full_result=to_struct(val))
|
||||
|
||||
@handle_errors
|
||||
def ask(self, request_iterator, context):
|
||||
yield from self._handle_chat_stream(request_iterator, context, self.service.ask)
|
||||
|
||||
@handle_errors
|
||||
def build_playbook_chat(self, request_iterator, context):
|
||||
yield from self._handle_chat_stream(request_iterator, context, self.service.build_playbook_chat)
|
||||
|
||||
@handle_errors
|
||||
def analyze_execution_results(self, request, context):
|
||||
results = from_struct(request.results)
|
||||
query = request.query if request.query else None
|
||||
yield from self._handle_unary_stream(self.service.analyze_execution_results, results, query=query)
|
||||
|
||||
@handle_errors
|
||||
def predict_execution_results(self, request, context):
|
||||
target_nodes = list(request.target_nodes)
|
||||
commands = list(request.commands)
|
||||
yield from self._handle_unary_stream(self.service.predict_execution_results, target_nodes, commands)
|
||||
|
||||
@handle_errors
|
||||
def confirm(self, request, context):
|
||||
res = self.service.confirm(request.value)
|
||||
|
||||
+122
-28
@@ -692,11 +692,6 @@ class ExecutionStub:
|
||||
req = connpy_pb2.ScriptRequest(param1=nodes_filter, param2=script_path, parallel=parallel)
|
||||
return from_struct(self.stub.run_cli_script(req).data)
|
||||
|
||||
@handle_errors
|
||||
def run_yaml_playbook(self, playbook_path, parallel=10):
|
||||
req = connpy_pb2.ScriptRequest(param1=playbook_path, parallel=parallel)
|
||||
return from_struct(self.stub.run_yaml_playbook(req).data)
|
||||
|
||||
class ImportExportStub:
|
||||
def __init__(self, channel, remote_host):
|
||||
self.stub = connpy_pb2_grpc.ImportExportServiceStub(channel)
|
||||
@@ -724,8 +719,7 @@ class AIStub:
|
||||
self.stub = connpy_pb2_grpc.AIServiceStub(channel)
|
||||
self.remote_host = remote_host
|
||||
|
||||
@handle_errors
|
||||
def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, **overrides):
|
||||
def _ai_chat_stream(self, stub_method, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, chunk_callback=None, **overrides):
|
||||
import queue
|
||||
from rich.prompt import Prompt
|
||||
from rich.text import Text
|
||||
@@ -760,7 +754,7 @@ class AIStub:
|
||||
if req is None: break
|
||||
yield req
|
||||
|
||||
responses = self.stub.ask(request_generator())
|
||||
responses = stub_method(request_generator())
|
||||
|
||||
full_content = ""
|
||||
header_printed = False
|
||||
@@ -859,26 +853,32 @@ class AIStub:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print header on first chunk
|
||||
alias = "architect" if current_responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
|
||||
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
|
||||
header_printed = True
|
||||
|
||||
# Initialize parser
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
if chunk_callback:
|
||||
header_printed = True
|
||||
else:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print header on first chunk
|
||||
alias = "architect" if current_responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
|
||||
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
|
||||
header_printed = True
|
||||
|
||||
# Initialize parser
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
|
||||
full_content += response.text_chunk
|
||||
md_parser.feed(response.text_chunk)
|
||||
if chunk_callback:
|
||||
chunk_callback(response.text_chunk)
|
||||
elif md_parser:
|
||||
md_parser.feed(response.text_chunk)
|
||||
continue
|
||||
|
||||
if response.is_final:
|
||||
if header_printed:
|
||||
if not chunk_callback and header_printed:
|
||||
from rich.rule import Rule
|
||||
md_parser.flush()
|
||||
|
||||
@@ -887,12 +887,8 @@ class AIStub:
|
||||
except: pass
|
||||
|
||||
final_result = from_struct(response.full_result)
|
||||
responder = final_result.get("responder", "engineer")
|
||||
alias = "architect" if responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
||||
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
||||
|
||||
if header_printed:
|
||||
if not chunk_callback and header_printed:
|
||||
from rich.console import Console as RichConsole
|
||||
from ..printer import connpy_theme, get_original_stdout
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
@@ -911,6 +907,104 @@ class AIStub:
|
||||
|
||||
return final_result
|
||||
|
||||
@handle_errors
|
||||
def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, **overrides):
|
||||
return self._ai_chat_stream(self.stub.ask, input_text, dryrun=dryrun, chat_history=chat_history, session_id=session_id, debug=debug, status=status, **overrides)
|
||||
|
||||
@handle_errors
|
||||
def build_playbook_chat(self, user_input, chat_history=None, status=None, chunk_callback=None):
|
||||
return self._ai_chat_stream(self.stub.build_playbook_chat, user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
def _process_unary_stream(self, responses, status=None, chunk_callback=None):
|
||||
full_content = ""
|
||||
header_printed = False
|
||||
final_result = {"response": "", "chat_history": []}
|
||||
md_parser = None
|
||||
|
||||
try:
|
||||
for response in responses:
|
||||
if response.status_update:
|
||||
if status:
|
||||
status.update(response.status_update)
|
||||
continue
|
||||
|
||||
if response.important_message:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
printer.console.print(Text.from_ansi(response.important_message))
|
||||
if status:
|
||||
try: status.start()
|
||||
except: pass
|
||||
continue
|
||||
|
||||
if not response.is_final:
|
||||
if response.text_chunk:
|
||||
if not header_printed:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
if chunk_callback:
|
||||
header_printed = True
|
||||
else:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print default header
|
||||
stable_console.print(Rule("[bold engineer]AI Analysis[/bold engineer]", style="engineer"))
|
||||
header_printed = True
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
|
||||
full_content += response.text_chunk
|
||||
if chunk_callback:
|
||||
chunk_callback(response.text_chunk)
|
||||
elif md_parser:
|
||||
md_parser.feed(response.text_chunk)
|
||||
continue
|
||||
|
||||
if response.is_final:
|
||||
if md_parser:
|
||||
md_parser.flush()
|
||||
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
final_result = from_struct(response.full_result)
|
||||
|
||||
if md_parser:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
stable_console.print(Rule(style="engineer"))
|
||||
break
|
||||
except Exception as e:
|
||||
if isinstance(e, grpc.RpcError):
|
||||
raise
|
||||
printer.warning(f"Stream interrupted: {e}")
|
||||
|
||||
if full_content:
|
||||
final_result["streamed"] = True
|
||||
|
||||
return final_result
|
||||
|
||||
@handle_errors
|
||||
def analyze_execution_results(self, results, query=None, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.AnalyzeRequest(query=query or "")
|
||||
req.results.CopyFrom(to_struct(results))
|
||||
responses = self.stub.analyze_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)
|
||||
|
||||
@handle_errors
|
||||
def predict_execution_results(self, target_nodes, commands, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.PreflightRequest(target_nodes=target_nodes, commands=commands)
|
||||
responses = self.stub.predict_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)
|
||||
|
||||
@handle_errors
|
||||
def confirm(self, input_text, console=None):
|
||||
return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value
|
||||
|
||||
+1
-1
@@ -573,7 +573,7 @@ class BlockMarkdownRenderer:
|
||||
if not block_text:
|
||||
return
|
||||
from rich.markdown import Markdown
|
||||
self._console.print(Markdown(block_text))
|
||||
self._console.print(Markdown(block_text, code_theme="ansi_dark"))
|
||||
|
||||
# Alias for backward compatibility
|
||||
IncrementalMarkdownParser = BlockMarkdownRenderer
|
||||
|
||||
@@ -53,7 +53,6 @@ service ExecutionService {
|
||||
rpc run_commands (RunRequest) returns (stream NodeRunResult) {}
|
||||
rpc test_commands (TestRequest) returns (stream NodeRunResult) {}
|
||||
rpc run_cli_script (ScriptRequest) returns (StructResponse) {}
|
||||
rpc run_yaml_playbook (ScriptRequest) returns (StructResponse) {}
|
||||
}
|
||||
|
||||
service ImportExportService {
|
||||
@@ -72,6 +71,9 @@ service AIService {
|
||||
rpc configure_mcp (MCPRequest) returns (google.protobuf.Empty) {}
|
||||
rpc list_mcp_servers (google.protobuf.Empty) returns (ValueResponse) {}
|
||||
rpc load_session_data (StringRequest) returns (StructResponse) {}
|
||||
rpc build_playbook_chat (stream AskRequest) returns (stream AIResponse) {}
|
||||
rpc analyze_execution_results (AnalyzeRequest) returns (stream AIResponse) {}
|
||||
rpc predict_execution_results (PreflightRequest) returns (stream AIResponse) {}
|
||||
}
|
||||
|
||||
service SystemService {
|
||||
@@ -317,3 +319,13 @@ message ChangePasswordRequest {
|
||||
string old_password = 1;
|
||||
string new_password = 2;
|
||||
}
|
||||
|
||||
message AnalyzeRequest {
|
||||
google.protobuf.Struct results = 1;
|
||||
string query = 2;
|
||||
}
|
||||
|
||||
message PreflightRequest {
|
||||
repeated string target_nodes = 1;
|
||||
repeated string commands = 2;
|
||||
}
|
||||
|
||||
@@ -319,3 +319,37 @@ class AIService(BaseService):
|
||||
agent = ai(self.config)
|
||||
return agent.load_session_data(session_id)
|
||||
|
||||
def build_playbook_chat(self, user_input: str, chat_history: list = None, status=None, chunk_callback=None):
|
||||
"""Interact with the specialized Playbook Builder Agent."""
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
agent = PlaybookBuilderAgent(self.config)
|
||||
return agent.ask(user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
def analyze_execution_results(self, results: dict, query: str = None, status=None, chunk_callback=None):
|
||||
"""Analyze actual command execution results using Network Architect 1-shot."""
|
||||
import json
|
||||
results_str = json.dumps(results, indent=2)
|
||||
|
||||
prompt = f"@architect: Please analyze the following actual execution results. Diagnose any issues, highlight successful actions, and suggest strategic remediation steps if needed."
|
||||
if query:
|
||||
prompt += f"\nSpecific user request: {query}"
|
||||
prompt += f"\n\nResults Data:\n{results_str}"
|
||||
prompt += "\n\nCRITICAL DIRECTIVE: You are running in a strictly 1-shot offline diagnostics mode (--analyze). There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately. DO NOT suggest, mention, or attempt to delegate the session back to the engineer."
|
||||
|
||||
# Delegate to self.ask, setting stream=True and forwarding callback/status.
|
||||
# This will invoke standard ai.ask with '@architect:' prefix, forcing 1-shot architect brain.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback, one_shot=True)
|
||||
|
||||
def predict_execution_results(self, target_nodes: list, commands: list, status=None, chunk_callback=None):
|
||||
"""Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot)."""
|
||||
nodes_str = ", ".join(target_nodes)
|
||||
commands_str = "\n".join(f"- {cmd}" for cmd in commands)
|
||||
|
||||
prompt = f"@engineer: Act as a Preflight Simulation Agent. Simulate and predict the expected outputs and behaviors of the following commands on the target nodes. Alert about potential safety or configuration risks based on node profiles."
|
||||
prompt += f"\n\nTarget Nodes: {nodes_str}"
|
||||
prompt += f"\nCommands to simulate:\n{commands_str}"
|
||||
prompt += "\n\nCRITICAL SCALABILITY DIRECTIVE: If there are many target nodes, DO NOT list predictions node-by-node. Instead, group them by Operating System, vendor, or platform, and provide a highly concise Executive Summary. Detail individual risks only for nodes that present specific anomalies or security concerns. Focus on overall impact."
|
||||
|
||||
# Delegate to self.ask, using the standard engineer brain but with the simulated preflight prompt.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import List, Dict, Any, Callable, Optional
|
||||
import os
|
||||
import yaml
|
||||
from .base import BaseService
|
||||
from connpy.core import nodes as Nodes
|
||||
from .exceptions import ConnpyError
|
||||
@@ -108,52 +107,3 @@ class ExecutionService(BaseService):
|
||||
|
||||
return self.run_commands(nodes_filter, commands, parallel=parallel)
|
||||
|
||||
def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
|
||||
"""Run a structured Connpy YAML automation playbook (from path or content)."""
|
||||
playbook = None
|
||||
if playbook_data.startswith("---YAML---\n"):
|
||||
try:
|
||||
content = playbook_data[len("---YAML---\n"):]
|
||||
playbook = yaml.load(content, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to parse YAML content: {e}")
|
||||
else:
|
||||
if not os.path.exists(playbook_data):
|
||||
raise ConnpyError(f"Playbook file not found: {playbook_data}")
|
||||
try:
|
||||
with open(playbook_data, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
|
||||
raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
|
||||
|
||||
action = playbook.get("action", "run")
|
||||
options = playbook.get("options", {})
|
||||
|
||||
# Extract all fields similar to RunHandler.cli_run
|
||||
exec_args = {
|
||||
"nodes_filter": playbook["nodes"],
|
||||
"commands": playbook["commands"],
|
||||
"variables": playbook.get("variables"),
|
||||
"parallel": options.get("parallel", parallel),
|
||||
"timeout": playbook.get("timeout", options.get("timeout", 20)),
|
||||
"prompt": options.get("prompt"),
|
||||
"name": playbook.get("name", "Task")
|
||||
}
|
||||
|
||||
# Map 'output' field to folder path if it's not stdout/null
|
||||
output_cfg = playbook.get("output")
|
||||
if output_cfg not in [None, "stdout"]:
|
||||
exec_args["folder"] = output_cfg
|
||||
|
||||
if action == "run":
|
||||
return self.run_commands(**exec_args)
|
||||
elif action == "test":
|
||||
exec_args["expected"] = playbook.get("expected", [])
|
||||
return self.test_commands(**exec_args)
|
||||
else:
|
||||
raise ConnpyError(f"Unsupported playbook action: {action}")
|
||||
|
||||
|
||||
@@ -480,6 +480,15 @@ class TestToolDefinitions:
|
||||
names = [t["function"]["name"] for t in tools]
|
||||
assert "arch_tool" in names
|
||||
|
||||
def test_architect_tools_one_shot(self, ai_config):
|
||||
from connpy.ai import ai
|
||||
one_shot_ai = ai(ai_config, one_shot=True)
|
||||
tools = one_shot_ai._get_architect_tools()
|
||||
names = [t["function"]["name"] for t in tools]
|
||||
assert "delegate_to_engineer" not in names
|
||||
assert "return_to_engineer" not in names
|
||||
assert "manage_memory_tool" in names
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# AI Session Management tests
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock, ANY
|
||||
from connpy.connapp import connapp
|
||||
import os
|
||||
|
||||
@pytest.fixture
|
||||
def app(populated_config):
|
||||
"""Returns an instance of connapp initialized with mock config."""
|
||||
return connapp(populated_config)
|
||||
|
||||
def test_run_generate_ai_dispatch(app):
|
||||
"""Test that connpy run --generate-ai parses and calls ai_generate."""
|
||||
with patch("connpy.cli.run_handler.RunHandler.ai_generate") as mock_ai_gen:
|
||||
app.start(["run", "--generate-ai", "new_playbook.yaml"])
|
||||
mock_ai_gen.assert_called_once()
|
||||
args = mock_ai_gen.call_args[0][0]
|
||||
assert args.data == ["new_playbook.yaml"]
|
||||
assert args.action == "generate_ai"
|
||||
|
||||
def test_run_preflight_ai_node(app):
|
||||
"""Test that connpy run --preflight-ai calls predict_execution_results and exits."""
|
||||
with patch("connpy.services.node_service.NodeService.list_nodes", return_value=["router1"]):
|
||||
with patch("connpy.services.ai_service.AIService.predict_execution_results") as mock_predict:
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
app.start(["run", "router1", "show version", "--preflight-ai"])
|
||||
|
||||
assert exc.value.code == 0
|
||||
mock_predict.assert_called_once_with(["router1"], ["show version"], chunk_callback=ANY)
|
||||
|
||||
def test_run_analyze_node(app):
|
||||
"""Test that connpy run --analyze calls analyze_execution_results after execution."""
|
||||
mock_run = MagicMock(return_value={"router1": {"status": 0, "output": "success"}})
|
||||
|
||||
with patch("connpy.services.node_service.NodeService.list_nodes", return_value=["router1"]):
|
||||
with patch("connpy.services.execution_service.ExecutionService.run_commands", mock_run):
|
||||
with patch("connpy.services.ai_service.AIService.analyze_execution_results") as mock_analyze:
|
||||
app.start(["run", "router1", "show version", "--analyze"])
|
||||
mock_run.assert_called_once()
|
||||
mock_analyze.assert_called_once_with(
|
||||
{"router1": {"status": 0, "output": "success"}},
|
||||
query="show version",
|
||||
chunk_callback=ANY
|
||||
)
|
||||
|
||||
def test_run_preflight_ai_playbook(app, tmp_path):
|
||||
"""Test that running a playbook with --preflight-ai predicts results per task."""
|
||||
playbook_path = tmp_path / "test_playbook.yaml"
|
||||
playbook_content = """
|
||||
tasks:
|
||||
- name: test-task
|
||||
action: run
|
||||
nodes: "router1"
|
||||
commands: ["show ip interface brief"]
|
||||
output: stdout
|
||||
"""
|
||||
playbook_path.write_text(playbook_content)
|
||||
|
||||
with patch("connpy.services.node_service.NodeService.list_nodes", return_value=["router1"]):
|
||||
with patch("connpy.services.ai_service.AIService.predict_execution_results") as mock_predict:
|
||||
with pytest.raises(SystemExit) as exc:
|
||||
app.start(["run", str(playbook_path), "--preflight-ai"])
|
||||
|
||||
assert exc.value.code == 0
|
||||
mock_predict.assert_called_once_with(["router1"], ["show ip interface brief"], chunk_callback=ANY)
|
||||
|
||||
def test_run_analyze_playbook(app, tmp_path):
|
||||
"""Test that running a playbook with --analyze triggers strategic analysis on all task outcomes."""
|
||||
playbook_path = tmp_path / "test_playbook.yaml"
|
||||
playbook_content = """
|
||||
tasks:
|
||||
- name: test-task
|
||||
action: run
|
||||
nodes: "router1"
|
||||
commands: ["show ip interface brief"]
|
||||
output: stdout
|
||||
"""
|
||||
playbook_path.write_text(playbook_content)
|
||||
|
||||
mock_run = MagicMock(return_value={"router1": {"status": 0, "output": "ok"}})
|
||||
|
||||
with patch("connpy.services.node_service.NodeService.list_nodes", return_value=["router1"]):
|
||||
with patch("connpy.services.execution_service.ExecutionService.run_commands", mock_run):
|
||||
with patch("connpy.services.ai_service.AIService.analyze_execution_results") as mock_analyze:
|
||||
app.start(["run", str(playbook_path), "--analyze"])
|
||||
mock_run.assert_called_once()
|
||||
mock_analyze.assert_called_once_with(
|
||||
{"router1": {"status": 0, "output": "ok"}},
|
||||
query=f"Playbook: {str(playbook_path)}",
|
||||
chunk_callback=ANY
|
||||
)
|
||||
|
||||
def test_ai_generate_wizard_save(app, tmp_path):
|
||||
"""Test that ai_generate wizard runs interactive chat loop, asks for validation and saves YAML."""
|
||||
dest_yaml = tmp_path / "playbook.yaml"
|
||||
|
||||
mock_chat = MagicMock(return_value={
|
||||
"response": "Here is your playbook.",
|
||||
"chat_history": [],
|
||||
"playbook_yaml": "tasks:\n - name: mytask"
|
||||
})
|
||||
app.services.ai.build_playbook_chat = mock_chat
|
||||
|
||||
# Mock rich.prompt.Prompt.ask to simulate User inputting prompt and then 'y' to save
|
||||
with patch("rich.prompt.Prompt.ask", side_effect=["create a basic task", "y"]):
|
||||
app.start(["run", "--generate-ai", str(dest_yaml)])
|
||||
|
||||
mock_chat.assert_called_once_with("create a basic task", chat_history=[], chunk_callback=ANY)
|
||||
assert os.path.exists(dest_yaml)
|
||||
with open(dest_yaml) as f:
|
||||
content = f.read()
|
||||
assert "tasks:" in content
|
||||
|
||||
def test_ai_generate_wizard_run(app, tmp_path):
|
||||
"""Test that ai_generate wizard runs, saves the playbook and executes it when choosing 'run'."""
|
||||
dest_yaml = tmp_path / "playbook_run.yaml"
|
||||
|
||||
mock_chat = MagicMock(return_value={
|
||||
"response": "Here is your playbook.",
|
||||
"chat_history": [],
|
||||
"playbook_yaml": "tasks:\n - name: mytask\n action: run\n nodes: '*'\n commands: ['show version']\n output: stdout"
|
||||
})
|
||||
app.services.ai.build_playbook_chat = mock_chat
|
||||
|
||||
with patch("rich.prompt.Prompt.ask", side_effect=["create task", "run"]):
|
||||
with patch("connpy.cli.run_handler.RunHandler.yaml_run") as mock_yaml_run:
|
||||
app.start(["run", "--generate-ai", str(dest_yaml)])
|
||||
|
||||
mock_chat.assert_called_once_with("create task", chat_history=[], chunk_callback=ANY)
|
||||
assert os.path.exists(dest_yaml)
|
||||
with open(dest_yaml) as f:
|
||||
content = f.read()
|
||||
assert "tasks:" in content
|
||||
|
||||
mock_yaml_run.assert_called_once()
|
||||
args = mock_yaml_run.call_args[0][0]
|
||||
assert args.data == [str(dest_yaml)]
|
||||
@@ -0,0 +1,296 @@
|
||||
import pytest
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
from connpy.services.ai_service import AIService
|
||||
|
||||
# =========================================================================
|
||||
# PlaybookBuilderAgent validation tests
|
||||
# =========================================================================
|
||||
|
||||
def test_validate_playbook_valid(ai_config):
|
||||
"""Verifies that a valid canonical tasks[] playbook passes validation."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
valid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply standard config"
|
||||
action: "run"
|
||||
nodes: "router1"
|
||||
commands:
|
||||
- "conf t"
|
||||
- "end"
|
||||
output: "stdout"
|
||||
- name: "Verify connectivity"
|
||||
action: "test"
|
||||
nodes: "router1"
|
||||
commands:
|
||||
- "ping 10.0.0.1"
|
||||
expected: "!"
|
||||
output: "stdout"
|
||||
"""
|
||||
|
||||
res = agent.validate_playbook(valid_yaml)
|
||||
assert res["valid"] is True
|
||||
assert "valid" in res["message"].lower()
|
||||
|
||||
def test_validate_playbook_invalid_yaml(ai_config):
|
||||
"""Verifies that syntax errors in YAML are caught and reported."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Broken task"
|
||||
action: "run
|
||||
nodes: "router1"
|
||||
"""
|
||||
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "syntax error" in res["error"].lower()
|
||||
|
||||
def test_validate_playbook_missing_tasks_key(ai_config):
|
||||
"""Verifies that a playbook without tasks root key is invalid."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
not_tasks:
|
||||
- name: "Apply standard config"
|
||||
action: "run"
|
||||
nodes: "router1"
|
||||
commands:
|
||||
- "conf t"
|
||||
output: "stdout"
|
||||
"""
|
||||
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "missing mandatory root 'tasks' key" in res["error"].lower()
|
||||
|
||||
def test_validate_playbook_missing_mandatory_fields(ai_config):
|
||||
"""Verifies that missing name, action, nodes, commands, or output triggers a validation failure."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
# Missing nodes
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply standard config"
|
||||
action: "run"
|
||||
commands:
|
||||
- "conf t"
|
||||
output: "stdout"
|
||||
"""
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "missing mandatory fields" in res["error"].lower()
|
||||
assert "nodes" in res["error"]
|
||||
|
||||
def test_validate_playbook_invalid_action(ai_config):
|
||||
"""Verifies that an unsupported action type is caught."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply standard config"
|
||||
action: "delete_everything"
|
||||
nodes: "router1"
|
||||
commands:
|
||||
- "conf t"
|
||||
output: "stdout"
|
||||
"""
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "invalid action" in res["error"].lower()
|
||||
|
||||
def test_validate_playbook_missing_expected_in_test(ai_config):
|
||||
"""Verifies that action 'test' requires the expected field."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply standard config"
|
||||
action: "test"
|
||||
nodes: "router1"
|
||||
commands:
|
||||
- "ping 10.0.0.1"
|
||||
output: "stdout"
|
||||
"""
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "missing the mandatory 'expected' key" in res["error"].lower()
|
||||
|
||||
def test_validate_playbook_invalid_nodes_type(ai_config):
|
||||
"""Verifies that nodes of invalid type (e.g. integer) is caught."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply config"
|
||||
action: "run"
|
||||
nodes: 12345
|
||||
commands:
|
||||
- "conf t"
|
||||
output: "stdout"
|
||||
"""
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "nodes' must be a string (regex) or a list of strings (regexes)" in res["error"]
|
||||
|
||||
def test_validate_playbook_invalid_nodes_list_item(ai_config):
|
||||
"""Verifies that nodes list containing non-string items is caught."""
|
||||
agent = PlaybookBuilderAgent(ai_config)
|
||||
|
||||
invalid_yaml = """
|
||||
tasks:
|
||||
- name: "Apply config"
|
||||
action: "run"
|
||||
nodes:
|
||||
- "router1"
|
||||
- 9999
|
||||
commands:
|
||||
- "conf t"
|
||||
output: "stdout"
|
||||
"""
|
||||
res = agent.validate_playbook(invalid_yaml)
|
||||
assert res["valid"] is False
|
||||
assert "list contains a non-string value" in res["error"]
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# AIService new methods delegation tests
|
||||
# =========================================================================
|
||||
|
||||
def test_build_playbook_chat_delegation(ai_config):
|
||||
"""Verifies that build_playbook_chat instantiates PlaybookBuilderAgent and delegates ask."""
|
||||
service = AIService(ai_config)
|
||||
|
||||
with patch("connpy.ai.PlaybookBuilderAgent") as MockAgentClass:
|
||||
mock_agent = MockAgentClass.return_value
|
||||
mock_agent.ask.return_value = {"response": "Mock response", "chat_history": []}
|
||||
|
||||
history = [{"role": "user", "content": "build playbook"}]
|
||||
res = service.build_playbook_chat("help me", chat_history=history)
|
||||
|
||||
MockAgentClass.assert_called_once_with(ai_config)
|
||||
mock_agent.ask.assert_called_once_with("help me", chat_history=history, status=None, chunk_callback=None)
|
||||
assert res["response"] == "Mock response"
|
||||
|
||||
def test_analyze_execution_results_delegation(ai_config):
|
||||
"""Verifies that analyze_execution_results formats prompt with @architect and delegates to self.ask."""
|
||||
service = AIService(ai_config)
|
||||
service.ask = MagicMock()
|
||||
|
||||
results = {"router1": {"output": "success", "status": 0}}
|
||||
service.analyze_execution_results(results, query="diagnose border")
|
||||
|
||||
service.ask.assert_called_once()
|
||||
args, kwargs = service.ask.call_args
|
||||
prompt = args[0]
|
||||
|
||||
assert prompt.startswith("@architect:")
|
||||
assert "diagnose border" in prompt
|
||||
assert "Results Data:" in prompt
|
||||
assert "router1" in prompt
|
||||
assert kwargs.get("one_shot") is True
|
||||
|
||||
def test_predict_execution_results_delegation(ai_config):
|
||||
"""Verifies that predict_execution_results formats prompt with @engineer and delegates to self.ask."""
|
||||
service = AIService(ai_config)
|
||||
service.ask = MagicMock()
|
||||
|
||||
nodes = ["router1", "router2"]
|
||||
commands = ["conf t", "interface lo0"]
|
||||
service.predict_execution_results(nodes, commands)
|
||||
|
||||
service.ask.assert_called_once()
|
||||
args, kwargs = service.ask.call_args
|
||||
prompt = args[0]
|
||||
|
||||
assert prompt.startswith("@engineer:")
|
||||
assert "Preflight Simulation Agent" in prompt
|
||||
assert "router1, router2" in prompt
|
||||
assert "conf t" in prompt
|
||||
assert "interface lo0" in prompt
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# gRPC Integration Tests for AIService
|
||||
# =========================================================================
|
||||
|
||||
import grpc
|
||||
from concurrent import futures
|
||||
from connpy.grpc_layer import server, connpy_pb2, connpy_pb2_grpc, stubs
|
||||
|
||||
class TestGRPCAIIntegration:
|
||||
@pytest.fixture
|
||||
def grpc_server(self, populated_config):
|
||||
"""Starts a local gRPC server for IA integration testing."""
|
||||
srv = grpc.server(futures.ThreadPoolExecutor(max_workers=5))
|
||||
connpy_pb2_grpc.add_AIServiceServicer_to_server(server.ServerServicer(populated_config).ai if hasattr(server, 'ServerServicer') else server.AIServicer(populated_config), srv)
|
||||
port = srv.add_insecure_port('127.0.0.1:0')
|
||||
srv.start()
|
||||
yield f"127.0.0.1:{port}"
|
||||
srv.stop(0)
|
||||
|
||||
@pytest.fixture
|
||||
def channel(self, grpc_server):
|
||||
with grpc.insecure_channel(grpc_server) as channel:
|
||||
yield channel
|
||||
|
||||
@pytest.fixture
|
||||
def ai_stub(self, channel):
|
||||
return stubs.AIStub(channel, "localhost")
|
||||
|
||||
def test_build_playbook_chat_grpc(self, ai_stub, populated_config):
|
||||
"""Verifies that build_playbook_chat gRPC stream functions correctly."""
|
||||
# Mock PlaybookBuilderAgent.ask to simulate agent response stream
|
||||
def mock_ask(user_input, chat_history=None, status=None, debug=False, chunk_callback=None):
|
||||
if chunk_callback:
|
||||
chunk_callback("Generated Tasks:\n- name: config")
|
||||
return {"response": "Done", "playbook_yaml": "tasks:\n- name: config"}
|
||||
|
||||
with patch("connpy.ai.PlaybookBuilderAgent.ask", side_effect=mock_ask):
|
||||
chunks = []
|
||||
def callback(chunk):
|
||||
chunks.append(chunk)
|
||||
|
||||
res = ai_stub.build_playbook_chat("make playbook", chunk_callback=callback)
|
||||
assert "tasks:" in res["playbook_yaml"]
|
||||
assert len(chunks) > 0
|
||||
assert "Generated Tasks:" in chunks[0]
|
||||
|
||||
def test_analyze_execution_results_grpc(self, ai_stub, populated_config):
|
||||
"""Verifies that analyze_execution_results gRPC stream functions correctly."""
|
||||
# Mock AIService.ask to simulate response stream
|
||||
def mock_ask(prompt, status=None, debug=False, chunk_callback=None, **kwargs):
|
||||
if chunk_callback:
|
||||
chunk_callback("Results are optimal.")
|
||||
return {"response": "Done"}
|
||||
|
||||
with patch.object(AIService, "ask", side_effect=mock_ask):
|
||||
chunks = []
|
||||
def callback(chunk):
|
||||
chunks.append(chunk)
|
||||
|
||||
res = ai_stub.analyze_execution_results({"r1": "ok"}, query="test query", chunk_callback=callback)
|
||||
assert res is not None
|
||||
assert len(chunks) > 0
|
||||
assert "optimal" in chunks[0]
|
||||
|
||||
def test_predict_execution_results_grpc(self, ai_stub, populated_config):
|
||||
"""Verifies that predict_execution_results gRPC stream functions correctly."""
|
||||
# Mock AIService.ask to simulate response stream
|
||||
def mock_ask(prompt, status=None, debug=False, chunk_callback=None, **kwargs):
|
||||
if chunk_callback:
|
||||
chunk_callback("Commands are safe.")
|
||||
return {"response": "Done"}
|
||||
|
||||
with patch.object(AIService, "ask", side_effect=mock_ask):
|
||||
chunks = []
|
||||
def callback(chunk):
|
||||
chunks.append(chunk)
|
||||
|
||||
res = ai_stub.predict_execution_results(["r1"], ["show version"], chunk_callback=callback)
|
||||
assert res is not None
|
||||
assert len(chunks) > 0
|
||||
assert "safe" in chunks[0]
|
||||
@@ -140,7 +140,7 @@ el.replaceWith(d);
|
||||
|
||||
def single_question(self, args, session_id):
|
||||
query = " ".join(args.ask)
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...") as status:
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(query, status=status, debug=args.debug, session_id=session_id, trust=args.trust, **self.ai_overrides)
|
||||
|
||||
responder = result.get("responder", "engineer")
|
||||
@@ -177,7 +177,7 @@ el.replaceWith(d);
|
||||
if not user_query.strip(): continue
|
||||
if user_query.lower() in ['exit', 'quit', 'bye', 'cancel']: break
|
||||
|
||||
with console.status("[ai_status]Agent is thinking...") as status:
|
||||
with console.status("[ai_status]Agent is thinking...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
|
||||
|
||||
new_history = result.get("chat_history")
|
||||
@@ -583,7 +583,7 @@ el.replaceWith(d);
|
||||
if not user_query.strip(): continue
|
||||
if user_query.lower() in ['exit', 'quit', 'bye', 'cancel']: break
|
||||
|
||||
with console.status("[ai_status]Agent is thinking...") as status:
|
||||
with console.status("[ai_status]Agent is thinking...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
|
||||
|
||||
new_history = result.get("chat_history")
|
||||
@@ -618,7 +618,7 @@ el.replaceWith(d);
|
||||
</summary>
|
||||
<pre><code class="python">def single_question(self, args, session_id):
|
||||
query = " ".join(args.ask)
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...") as status:
|
||||
with console.status("[ai_status]Agent is thinking and analyzing...[/ai_status]") as status:
|
||||
result = self.app.myai.ask(query, status=status, debug=args.debug, session_id=session_id, trust=args.trust, **self.ai_overrides)
|
||||
|
||||
responder = result.get("responder", "engineer")
|
||||
|
||||
@@ -63,7 +63,12 @@ el.replaceWith(d);
|
||||
def dispatch(self, args):
|
||||
if len(args.data) > 1:
|
||||
args.action = "noderun"
|
||||
actions = {"noderun": self.node_run, "generate": self.yaml_generate, "run": self.yaml_run}
|
||||
actions = {
|
||||
"noderun": self.node_run,
|
||||
"generate": self.yaml_generate,
|
||||
"generate_ai": self.ai_generate,
|
||||
"run": self.yaml_run
|
||||
}
|
||||
return actions.get(args.action)(args)
|
||||
|
||||
def node_run(self, args):
|
||||
@@ -81,6 +86,41 @@ el.replaceWith(d);
|
||||
|
||||
commands = [" ".join(args.data[1:])]
|
||||
|
||||
# Check for Preflight AI simulation
|
||||
if getattr(args, "preflight_ai", False):
|
||||
matched_node_names = [n.get("name") if isinstance(n, dict) else n for n in matched_nodes]
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
matched_node_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed: {e}")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
header_printed = False
|
||||
|
||||
@@ -118,6 +158,40 @@ el.replaceWith(d);
|
||||
)
|
||||
printer.run_summary(results)
|
||||
|
||||
# Analyze execution results if requested
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else " ".join(args.data[1:])
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)
|
||||
@@ -138,8 +212,105 @@ el.replaceWith(d);
|
||||
with open(path, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
# Check preflight first before any task runs
|
||||
if getattr(args, "preflight_ai", False):
|
||||
preflight_failed = False
|
||||
for task in playbook.get("tasks", []):
|
||||
name = task.get("name", "Task")
|
||||
nodelist = task.get("nodes", [])
|
||||
commands = task.get("commands", [])
|
||||
|
||||
# Resolve nodes to names
|
||||
try:
|
||||
if isinstance(nodelist, str):
|
||||
resolved_nodes = self.app.services.nodes.list_nodes(nodelist)
|
||||
elif isinstance(nodelist, list):
|
||||
resolved_nodes = []
|
||||
for item in nodelist:
|
||||
matches = self.app.services.nodes.list_nodes(item)
|
||||
for m in matches:
|
||||
if m not in resolved_nodes:
|
||||
resolved_nodes.append(m)
|
||||
else:
|
||||
resolved_nodes = []
|
||||
except Exception:
|
||||
resolved_nodes = []
|
||||
|
||||
resolved_names = [n.get("name") if isinstance(n, dict) else n for n in resolved_nodes]
|
||||
printer.console.print(f"\n[bold]Task: {name}[/bold] (Preflight for {len(resolved_names)} nodes)")
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
resolved_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed for task {name}: {e}")
|
||||
preflight_failed = True
|
||||
if preflight_failed:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
# Standard run
|
||||
results_all = {}
|
||||
for task in playbook.get("tasks", []):
|
||||
self.cli_run(task)
|
||||
task_res = self.cli_run(task)
|
||||
if task_res:
|
||||
results_all.update(task_res)
|
||||
|
||||
# If analyze is enabled, run analysis on accumulated results
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing playbook execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else f"Playbook: {path}"
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results_all,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to run playbook {path}: {e}")
|
||||
@@ -184,6 +355,7 @@ el.replaceWith(d);
|
||||
|
||||
nodelist = resolved_nodes
|
||||
|
||||
results = {}
|
||||
try:
|
||||
header_printed = False
|
||||
if action == "run":
|
||||
@@ -243,13 +415,244 @@ el.replaceWith(d);
|
||||
)
|
||||
# ALWAYS show the aggregate summary at the end
|
||||
printer.test_summary(results)
|
||||
|
||||
return results
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))</code></pre>
|
||||
printer.error(str(e))
|
||||
return {}
|
||||
|
||||
def ai_generate(self, args):
|
||||
from rich.prompt import Prompt
|
||||
from rich.rule import Rule
|
||||
from rich.panel import Panel
|
||||
from rich.syntax import Syntax
|
||||
|
||||
dest_file = args.data[0]
|
||||
if os.path.exists(dest_file):
|
||||
printer.error(f"File '{dest_file}' already exists.")
|
||||
sys.exit(14)
|
||||
|
||||
chat_history = []
|
||||
|
||||
# Consistent layout opening matching global AI (engineer style)
|
||||
from rich.markdown import Markdown
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n"))
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Operation cancelled by user.")
|
||||
break
|
||||
|
||||
if user_prompt.strip().lower() in ["exit", "quit"]:
|
||||
printer.info("Exiting AI Assistant.")
|
||||
break
|
||||
|
||||
if not user_prompt.strip():
|
||||
continue
|
||||
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Agent is thinking...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Playbook Builder AI[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
res = self.app.services.ai.build_playbook_chat(
|
||||
user_prompt,
|
||||
chat_history=chat_history,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
renderer.flush()
|
||||
if not first_chunk:
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
# Update history
|
||||
if res and "chat_history" in res:
|
||||
chat_history = res["chat_history"]
|
||||
|
||||
# Check if the agent returned a validated playbook YAML
|
||||
if res and "playbook_yaml" in res and res["playbook_yaml"]:
|
||||
yaml_content = res["playbook_yaml"]
|
||||
printer.console.print()
|
||||
printer.success("Playbook YAML successfully generated and validated.")
|
||||
|
||||
# Show the YAML inside a beautiful panel matching AI style (with engineer borders)
|
||||
syntax = Syntax(yaml_content, "yaml", theme="ansi_dark", word_wrap=True, background_color="default")
|
||||
panel = Panel(syntax, title="[engineer][bold]Resulting Playbook[/bold][/engineer]", border_style="engineer", expand=False)
|
||||
printer.console.print(panel)
|
||||
|
||||
# Ask if the user wants to save it
|
||||
try:
|
||||
save_confirm = Prompt.ask(
|
||||
f"\nDo you want to save this playbook to '{dest_file}'?",
|
||||
choices=["y", "n", "run"],
|
||||
default="y"
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Saving skipped.")
|
||||
break
|
||||
|
||||
choice = save_confirm.strip().lower()
|
||||
if choice in ["y", "yes", "run"]:
|
||||
with open(dest_file, "w") as f:
|
||||
f.write(yaml_content)
|
||||
printer.success(f"Playbook saved successfully to '{dest_file}'")
|
||||
if choice == "run":
|
||||
printer.console.print()
|
||||
printer.info("Executing the saved playbook...")
|
||||
self.yaml_run(args)
|
||||
break
|
||||
else:
|
||||
printer.warning("Playbook not saved. You can continue describing changes or exit.")
|
||||
except Exception as e:
|
||||
printer.error(f"Error in AI chat: {e}")</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
<dt id="connpy.cli.run_handler.RunHandler.ai_generate"><code class="name flex">
|
||||
<span>def <span class="ident">ai_generate</span></span>(<span>self, args)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def ai_generate(self, args):
|
||||
from rich.prompt import Prompt
|
||||
from rich.rule import Rule
|
||||
from rich.panel import Panel
|
||||
from rich.syntax import Syntax
|
||||
|
||||
dest_file = args.data[0]
|
||||
if os.path.exists(dest_file):
|
||||
printer.error(f"File '{dest_file}' already exists.")
|
||||
sys.exit(14)
|
||||
|
||||
chat_history = []
|
||||
|
||||
# Consistent layout opening matching global AI (engineer style)
|
||||
from rich.markdown import Markdown
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
printer.console.print(Markdown("**Playbook Builder AI**: Welcome! Describe the automation workflow you want to design.\nType **exit** to quit.\n"))
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
while True:
|
||||
try:
|
||||
user_prompt = Prompt.ask("[user_prompt]User[/user_prompt]")
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Operation cancelled by user.")
|
||||
break
|
||||
|
||||
if user_prompt.strip().lower() in ["exit", "quit"]:
|
||||
printer.info("Exiting AI Assistant.")
|
||||
break
|
||||
|
||||
if not user_prompt.strip():
|
||||
continue
|
||||
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Agent is thinking...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Playbook Builder AI[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
res = self.app.services.ai.build_playbook_chat(
|
||||
user_prompt,
|
||||
chat_history=chat_history,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try:
|
||||
status_context.stop()
|
||||
except:
|
||||
pass
|
||||
renderer.flush()
|
||||
if not first_chunk:
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
|
||||
# Update history
|
||||
if res and "chat_history" in res:
|
||||
chat_history = res["chat_history"]
|
||||
|
||||
# Check if the agent returned a validated playbook YAML
|
||||
if res and "playbook_yaml" in res and res["playbook_yaml"]:
|
||||
yaml_content = res["playbook_yaml"]
|
||||
printer.console.print()
|
||||
printer.success("Playbook YAML successfully generated and validated.")
|
||||
|
||||
# Show the YAML inside a beautiful panel matching AI style (with engineer borders)
|
||||
syntax = Syntax(yaml_content, "yaml", theme="ansi_dark", word_wrap=True, background_color="default")
|
||||
panel = Panel(syntax, title="[engineer][bold]Resulting Playbook[/bold][/engineer]", border_style="engineer", expand=False)
|
||||
printer.console.print(panel)
|
||||
|
||||
# Ask if the user wants to save it
|
||||
try:
|
||||
save_confirm = Prompt.ask(
|
||||
f"\nDo you want to save this playbook to '{dest_file}'?",
|
||||
choices=["y", "n", "run"],
|
||||
default="y"
|
||||
)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
printer.console.print()
|
||||
printer.warning("Saving skipped.")
|
||||
break
|
||||
|
||||
choice = save_confirm.strip().lower()
|
||||
if choice in ["y", "yes", "run"]:
|
||||
with open(dest_file, "w") as f:
|
||||
f.write(yaml_content)
|
||||
printer.success(f"Playbook saved successfully to '{dest_file}'")
|
||||
if choice == "run":
|
||||
printer.console.print()
|
||||
printer.info("Executing the saved playbook...")
|
||||
self.yaml_run(args)
|
||||
break
|
||||
else:
|
||||
printer.warning("Playbook not saved. You can continue describing changes or exit.")
|
||||
except Exception as e:
|
||||
printer.error(f"Error in AI chat: {e}")</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.cli.run_handler.RunHandler.cli_run"><code class="name flex">
|
||||
<span>def <span class="ident">cli_run</span></span>(<span>self, script)</span>
|
||||
</code></dt>
|
||||
@@ -297,6 +700,7 @@ el.replaceWith(d);
|
||||
|
||||
nodelist = resolved_nodes
|
||||
|
||||
results = {}
|
||||
try:
|
||||
header_printed = False
|
||||
if action == "run":
|
||||
@@ -356,9 +760,12 @@ el.replaceWith(d);
|
||||
)
|
||||
# ALWAYS show the aggregate summary at the end
|
||||
printer.test_summary(results)
|
||||
|
||||
return results
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))</code></pre>
|
||||
printer.error(str(e))
|
||||
return {}</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
@@ -373,7 +780,12 @@ el.replaceWith(d);
|
||||
<pre><code class="python">def dispatch(self, args):
|
||||
if len(args.data) > 1:
|
||||
args.action = "noderun"
|
||||
actions = {"noderun": self.node_run, "generate": self.yaml_generate, "run": self.yaml_run}
|
||||
actions = {
|
||||
"noderun": self.node_run,
|
||||
"generate": self.yaml_generate,
|
||||
"generate_ai": self.ai_generate,
|
||||
"run": self.yaml_run
|
||||
}
|
||||
return actions.get(args.action)(args)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
@@ -401,6 +813,41 @@ el.replaceWith(d);
|
||||
|
||||
commands = [" ".join(args.data[1:])]
|
||||
|
||||
# Check for Preflight AI simulation
|
||||
if getattr(args, "preflight_ai", False):
|
||||
matched_node_names = [n.get("name") if isinstance(n, dict) else n for n in matched_nodes]
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
matched_node_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[engineer][bold]Preflight AI Simulation[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed: {e}")
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
header_printed = False
|
||||
|
||||
@@ -438,6 +885,40 @@ el.replaceWith(d);
|
||||
)
|
||||
printer.run_summary(results)
|
||||
|
||||
# Analyze execution results if requested
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else " ".join(args.data[1:])
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except ConnpyError as e:
|
||||
printer.error(str(e))
|
||||
sys.exit(1)</code></pre>
|
||||
@@ -478,8 +959,105 @@ el.replaceWith(d);
|
||||
with open(path, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
|
||||
# Check preflight first before any task runs
|
||||
if getattr(args, "preflight_ai", False):
|
||||
preflight_failed = False
|
||||
for task in playbook.get("tasks", []):
|
||||
name = task.get("name", "Task")
|
||||
nodelist = task.get("nodes", [])
|
||||
commands = task.get("commands", [])
|
||||
|
||||
# Resolve nodes to names
|
||||
try:
|
||||
if isinstance(nodelist, str):
|
||||
resolved_nodes = self.app.services.nodes.list_nodes(nodelist)
|
||||
elif isinstance(nodelist, list):
|
||||
resolved_nodes = []
|
||||
for item in nodelist:
|
||||
matches = self.app.services.nodes.list_nodes(item)
|
||||
for m in matches:
|
||||
if m not in resolved_nodes:
|
||||
resolved_nodes.append(m)
|
||||
else:
|
||||
resolved_nodes = []
|
||||
except Exception:
|
||||
resolved_nodes = []
|
||||
|
||||
resolved_names = [n.get("name") if isinstance(n, dict) else n for n in resolved_nodes]
|
||||
printer.console.print(f"\n[bold]Task: {name}[/bold] (Preflight for {len(resolved_names)} nodes)")
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Simulating execution...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.predict_execution_results(
|
||||
resolved_names,
|
||||
commands,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title=f"[engineer][bold]Preflight AI Simulation: {name}[/bold][/engineer]", style="engineer"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="engineer"))
|
||||
except Exception as e:
|
||||
printer.error(f"Preflight AI simulation failed for task {name}: {e}")
|
||||
preflight_failed = True
|
||||
if preflight_failed:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
# Standard run
|
||||
results_all = {}
|
||||
for task in playbook.get("tasks", []):
|
||||
self.cli_run(task)
|
||||
task_res = self.cli_run(task)
|
||||
if task_res:
|
||||
results_all.update(task_res)
|
||||
|
||||
# If analyze is enabled, run analysis on accumulated results
|
||||
if getattr(args, "analyze", None) is not None:
|
||||
printer.console.print()
|
||||
|
||||
renderer = printer.BlockMarkdownRenderer()
|
||||
first_chunk = True
|
||||
status_context = printer.console.status("[ai_status]Analyzing playbook execution results...[/ai_status]")
|
||||
|
||||
def callback(chunk):
|
||||
nonlocal first_chunk
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
first_chunk = False
|
||||
renderer.feed(chunk)
|
||||
|
||||
query = args.analyze if args.analyze else f"Playbook: {path}"
|
||||
try:
|
||||
status_context.start()
|
||||
self.app.services.ai.analyze_execution_results(
|
||||
results_all,
|
||||
query=query,
|
||||
chunk_callback=callback
|
||||
)
|
||||
if first_chunk:
|
||||
try: status_context.stop()
|
||||
except: pass
|
||||
printer.console.print(Rule(title="[architect][bold]Network Architect AI Playbook Analysis[/bold][/architect]", style="architect"))
|
||||
renderer.flush()
|
||||
printer.console.print(Rule(style="architect"))
|
||||
except Exception as e:
|
||||
printer.error(f"AI Analysis failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
printer.error(f"Failed to run playbook {path}: {e}")
|
||||
@@ -506,7 +1084,8 @@ el.replaceWith(d);
|
||||
<ul>
|
||||
<li>
|
||||
<h4><code><a title="connpy.cli.run_handler.RunHandler" href="#connpy.cli.run_handler.RunHandler">RunHandler</a></code></h4>
|
||||
<ul class="">
|
||||
<ul class="two-column">
|
||||
<li><code><a title="connpy.cli.run_handler.RunHandler.ai_generate" href="#connpy.cli.run_handler.RunHandler.ai_generate">ai_generate</a></code></li>
|
||||
<li><code><a title="connpy.cli.run_handler.RunHandler.cli_run" href="#connpy.cli.run_handler.RunHandler.cli_run">cli_run</a></code></li>
|
||||
<li><code><a title="connpy.cli.run_handler.RunHandler.dispatch" href="#connpy.cli.run_handler.RunHandler.dispatch">dispatch</a></code></li>
|
||||
<li><code><a title="connpy.cli.run_handler.RunHandler.node_run" href="#connpy.cli.run_handler.RunHandler.node_run">node_run</a></code></li>
|
||||
|
||||
@@ -100,6 +100,21 @@ el.replaceWith(d);
|
||||
request_deserializer=connpy__pb2.StringRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
'build_playbook_chat': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.build_playbook_chat,
|
||||
request_deserializer=connpy__pb2.AskRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
'analyze_execution_results': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.analyze_execution_results,
|
||||
request_deserializer=connpy__pb2.AnalyzeRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
'predict_execution_results': grpc.unary_stream_rpc_method_handler(
|
||||
servicer.predict_execution_results,
|
||||
request_deserializer=connpy__pb2.PreflightRequest.FromString,
|
||||
response_serializer=connpy__pb2.AIResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'connpy.AIService', rpc_method_handlers)
|
||||
@@ -209,11 +224,6 @@ el.replaceWith(d);
|
||||
request_deserializer=connpy__pb2.ScriptRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
'run_yaml_playbook': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.run_yaml_playbook,
|
||||
request_deserializer=connpy__pb2.ScriptRequest.FromString,
|
||||
response_serializer=connpy__pb2.StructResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'connpy.ExecutionService', rpc_method_handlers)
|
||||
@@ -739,11 +749,129 @@ el.replaceWith(d);
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def build_playbook_chat(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(
|
||||
request_iterator,
|
||||
target,
|
||||
'/connpy.AIService/build_playbook_chat',
|
||||
connpy__pb2.AskRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def analyze_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/analyze_execution_results',
|
||||
connpy__pb2.AnalyzeRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def predict_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/predict_execution_results',
|
||||
connpy__pb2.PreflightRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
<h3>Static methods</h3>
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIService.analyze_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">analyze_execution_results</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@staticmethod
|
||||
def analyze_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/analyze_execution_results',
|
||||
connpy__pb2.AnalyzeRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIService.ask"><code class="name flex">
|
||||
<span>def <span class="ident">ask</span></span>(<span>request_iterator,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
@@ -818,6 +946,43 @@ def ask_copilot(request,
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIService.build_playbook_chat"><code class="name flex">
|
||||
<span>def <span class="ident">build_playbook_chat</span></span>(<span>request_iterator,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@staticmethod
|
||||
def build_playbook_chat(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.stream_stream(
|
||||
request_iterator,
|
||||
target,
|
||||
'/connpy.AIService/build_playbook_chat',
|
||||
connpy__pb2.AskRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIService.configure_mcp"><code class="name flex">
|
||||
<span>def <span class="ident">configure_mcp</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
@@ -1077,6 +1242,43 @@ def load_session_data(request,
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIService.predict_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">predict_execution_results</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@staticmethod
|
||||
def predict_execution_results(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_stream(
|
||||
request,
|
||||
target,
|
||||
'/connpy.AIService/predict_execution_results',
|
||||
connpy__pb2.PreflightRequest.SerializeToString,
|
||||
connpy__pb2.AIResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer"><code class="flex name class">
|
||||
@@ -1139,6 +1341,24 @@ def load_session_data(request,
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def load_session_data(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def build_playbook_chat(self, request_iterator, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def analyze_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def predict_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
@@ -1151,6 +1371,22 @@ def load_session_data(request,
|
||||
</ul>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.analyze_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">analyze_execution_results</span></span>(<span>self, request, context)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def analyze_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask"><code class="name flex">
|
||||
<span>def <span class="ident">ask</span></span>(<span>self, request_iterator, context)</span>
|
||||
</code></dt>
|
||||
@@ -1183,6 +1419,22 @@ def load_session_data(request,
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.build_playbook_chat"><code class="name flex">
|
||||
<span>def <span class="ident">build_playbook_chat</span></span>(<span>self, request_iterator, context)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def build_playbook_chat(self, request_iterator, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_mcp"><code class="name flex">
|
||||
<span>def <span class="ident">configure_mcp</span></span>(<span>self, request, context)</span>
|
||||
</code></dt>
|
||||
@@ -1295,6 +1547,22 @@ def load_session_data(request,
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.predict_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">predict_execution_results</span></span>(<span>self, request, context)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def predict_execution_results(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.AIServiceStub"><code class="flex name class">
|
||||
@@ -1359,6 +1627,21 @@ def load_session_data(request,
|
||||
'/connpy.AIService/load_session_data',
|
||||
request_serializer=connpy__pb2.StringRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.build_playbook_chat = channel.stream_stream(
|
||||
'/connpy.AIService/build_playbook_chat',
|
||||
request_serializer=connpy__pb2.AskRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.analyze_execution_results = channel.unary_stream(
|
||||
'/connpy.AIService/analyze_execution_results',
|
||||
request_serializer=connpy__pb2.AnalyzeRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.predict_execution_results = channel.unary_stream(
|
||||
'/connpy.AIService/predict_execution_results',
|
||||
request_serializer=connpy__pb2.PreflightRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.AIResponse.FromString,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p>
|
||||
@@ -2313,33 +2596,6 @@ def update_setting(request,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
|
||||
@staticmethod
|
||||
def run_yaml_playbook(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/connpy.ExecutionService/run_yaml_playbook',
|
||||
connpy__pb2.ScriptRequest.SerializeToString,
|
||||
connpy__pb2.StructResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
@@ -2419,43 +2675,6 @@ def run_commands(request,
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_yaml_playbook"><code class="name flex">
|
||||
<span>def <span class="ident">run_yaml_playbook</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@staticmethod
|
||||
def run_yaml_playbook(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/connpy.ExecutionService/run_yaml_playbook',
|
||||
connpy__pb2.ScriptRequest.SerializeToString,
|
||||
connpy__pb2.StructResponse.FromString,
|
||||
options,
|
||||
channel_credentials,
|
||||
insecure,
|
||||
call_credentials,
|
||||
compression,
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.test_commands"><code class="name flex">
|
||||
<span>def <span class="ident">test_commands</span></span>(<span>request,<br>target,<br>options=(),<br>channel_credentials=None,<br>call_credentials=None,<br>insecure=False,<br>compression=None,<br>wait_for_ready=None,<br>timeout=None,<br>metadata=None)</span>
|
||||
</code></dt>
|
||||
@@ -2519,12 +2738,6 @@ def test_commands(request,
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def run_cli_script(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
|
||||
def run_yaml_playbook(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
@@ -2569,22 +2782,6 @@ def test_commands(request,
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_yaml_playbook"><code class="name flex">
|
||||
<span>def <span class="ident">run_yaml_playbook</span></span>(<span>self, request, context)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def run_yaml_playbook(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.test_commands"><code class="name flex">
|
||||
<span>def <span class="ident">test_commands</span></span>(<span>self, request, context)</span>
|
||||
</code></dt>
|
||||
@@ -2635,11 +2832,6 @@ def test_commands(request,
|
||||
'/connpy.ExecutionService/run_cli_script',
|
||||
request_serializer=connpy__pb2.ScriptRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)
|
||||
self.run_yaml_playbook = channel.unary_unary(
|
||||
'/connpy.ExecutionService/run_yaml_playbook',
|
||||
request_serializer=connpy__pb2.ScriptRequest.SerializeToString,
|
||||
response_deserializer=connpy__pb2.StructResponse.FromString,
|
||||
_registered_method=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p>
|
||||
@@ -6089,9 +6281,11 @@ def stop_api(request,
|
||||
<ul>
|
||||
<li>
|
||||
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService">AIService</a></code></h4>
|
||||
<ul class="two-column">
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.analyze_execution_results" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.ask" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.ask_copilot" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.ask_copilot">ask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.build_playbook_chat" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.configure_mcp" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.configure_provider" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.confirm" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.confirm">confirm</a></code></li>
|
||||
@@ -6099,13 +6293,16 @@ def stop_api(request,
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.list_mcp_servers" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.list_sessions" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.load_session_data" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIService.predict_execution_results" href="#connpy.grpc_layer.connpy_pb2_grpc.AIService.predict_execution_results">predict_execution_results</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer">AIServiceServicer</a></code></h4>
|
||||
<ul class="two-column">
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.analyze_execution_results" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask_copilot" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask_copilot">ask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.build_playbook_chat" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_mcp" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm">confirm</a></code></li>
|
||||
@@ -6113,6 +6310,7 @@ def stop_api(request,
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.predict_execution_results" href="#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.predict_execution_results">predict_execution_results</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
@@ -6165,7 +6363,6 @@ def stop_api(request,
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_cli_script" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_commands" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_yaml_playbook" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.test_commands" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionService.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -6174,7 +6371,6 @@ def stop_api(request,
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_cli_script" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_commands" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_yaml_playbook" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.test_commands" href="#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -174,12 +174,10 @@ el.replaceWith(d);
|
||||
def service(self):
|
||||
return self._get_provider().ai
|
||||
|
||||
@handle_errors
|
||||
def ask(self, request_iterator, context):
|
||||
def _handle_chat_stream(self, request_iterator, context, service_method):
|
||||
import queue
|
||||
import threading
|
||||
|
||||
ai_service = self.service
|
||||
chunk_queue = queue.Queue()
|
||||
request_queue = queue.Queue()
|
||||
bridge = None
|
||||
@@ -197,21 +195,28 @@ el.replaceWith(d);
|
||||
nonlocal history, bridge, agent_instance
|
||||
try:
|
||||
# Run the AI interaction (this blocks this specific thread)
|
||||
res = ai_service.ask(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
session_id=session_id,
|
||||
debug=debug,
|
||||
status=bridge,
|
||||
console=bridge,
|
||||
confirm_handler=bridge.confirm,
|
||||
chunk_callback=callback,
|
||||
trust=trust,
|
||||
**overrides
|
||||
)
|
||||
if getattr(service_method, "__name__", None) == "build_playbook_chat":
|
||||
res = service_method(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
status=bridge,
|
||||
chunk_callback=callback
|
||||
)
|
||||
else:
|
||||
res = service_method(
|
||||
input_text,
|
||||
chat_history=history if history else None,
|
||||
session_id=session_id,
|
||||
debug=debug,
|
||||
status=bridge,
|
||||
confirm_handler=bridge.confirm,
|
||||
chunk_callback=callback,
|
||||
trust=trust,
|
||||
**overrides
|
||||
)
|
||||
|
||||
# Update history for next message
|
||||
if "chat_history" in res:
|
||||
if res and "chat_history" in res:
|
||||
history = res["chat_history"]
|
||||
|
||||
# Send final chunk marker
|
||||
@@ -305,6 +310,71 @@ el.replaceWith(d);
|
||||
elif msg_type == "final_mark":
|
||||
yield connpy_pb2.AIResponse(is_final=True, full_result=to_struct(val))
|
||||
|
||||
def _handle_unary_stream(self, service_method, *args, **kwargs):
|
||||
import queue
|
||||
import threading
|
||||
|
||||
chunk_queue = queue.Queue()
|
||||
bridge = StatusBridge(chunk_queue, is_web=False)
|
||||
|
||||
def callback(chunk):
|
||||
chunk_queue.put(("text", chunk))
|
||||
|
||||
def _worker():
|
||||
try:
|
||||
res = service_method(*args, chunk_callback=callback, status=bridge, **kwargs)
|
||||
chunk_queue.put(("final_mark", res))
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"gRPC Unary Stream error: {e}")
|
||||
traceback.print_exc()
|
||||
chunk_queue.put(("status", f"Error: {str(e)}"))
|
||||
chunk_queue.put(("final_mark", {"response": f"Error: {str(e)}", "error": True}))
|
||||
finally:
|
||||
chunk_queue.put((None, None))
|
||||
|
||||
threading.Thread(target=_worker, daemon=True).start()
|
||||
|
||||
while True:
|
||||
item = chunk_queue.get()
|
||||
if item == (None, None):
|
||||
break
|
||||
|
||||
msg_type, val = item
|
||||
if msg_type == "text":
|
||||
yield connpy_pb2.AIResponse(text_chunk=val, is_final=False)
|
||||
elif msg_type == "status":
|
||||
clean_val = val.replace("[ai_status]", "").replace("[/ai_status]", "")
|
||||
yield connpy_pb2.AIResponse(status_update=clean_val, is_final=False)
|
||||
elif msg_type == "debug":
|
||||
yield connpy_pb2.AIResponse(debug_message=val, is_final=False)
|
||||
elif msg_type == "important":
|
||||
yield connpy_pb2.AIResponse(important_message=val, is_final=False)
|
||||
elif msg_type == "confirm":
|
||||
yield connpy_pb2.AIResponse(status_update=val, requires_confirmation=True, is_final=False)
|
||||
elif msg_type == "final_mark":
|
||||
yield connpy_pb2.AIResponse(is_final=True, full_result=to_struct(val))
|
||||
|
||||
@handle_errors
|
||||
def ask(self, request_iterator, context):
|
||||
yield from self._handle_chat_stream(request_iterator, context, self.service.ask)
|
||||
|
||||
@handle_errors
|
||||
def build_playbook_chat(self, request_iterator, context):
|
||||
yield from self._handle_chat_stream(request_iterator, context, self.service.build_playbook_chat)
|
||||
|
||||
@handle_errors
|
||||
def analyze_execution_results(self, request, context):
|
||||
results = from_struct(request.results)
|
||||
query = request.query if request.query else None
|
||||
yield from self._handle_unary_stream(self.service.analyze_execution_results, results, query=query)
|
||||
|
||||
@handle_errors
|
||||
def predict_execution_results(self, request, context):
|
||||
target_nodes = list(request.target_nodes)
|
||||
commands = list(request.commands)
|
||||
yield from self._handle_unary_stream(self.service.predict_execution_results, target_nodes, commands)
|
||||
|
||||
@handle_errors
|
||||
def confirm(self, request, context):
|
||||
res = self.service.confirm(request.value)
|
||||
@@ -386,8 +456,10 @@ def service(self):
|
||||
<ul class="hlist">
|
||||
<li><code><b><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer">AIServiceServicer</a></b></code>:
|
||||
<ul class="hlist">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.analyze_execution_results" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask_copilot" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.ask_copilot">ask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.build_playbook_chat" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_mcp" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm">confirm</a></code></li>
|
||||
@@ -395,6 +467,7 @@ def service(self):
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.predict_execution_results" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.predict_execution_results">predict_execution_results</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -855,11 +928,6 @@ def service(self):
|
||||
@handle_errors
|
||||
def run_cli_script(self, request, context):
|
||||
res = self.service.run_cli_script(request.param1, request.param2, request.parallel)
|
||||
return connpy_pb2.StructResponse(data=to_struct(res))
|
||||
|
||||
@handle_errors
|
||||
def run_yaml_playbook(self, request, context):
|
||||
res = self.service.run_yaml_playbook(request.param1, request.parallel)
|
||||
return connpy_pb2.StructResponse(data=to_struct(res))</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Missing associated documentation comment in .proto file.</p></div>
|
||||
@@ -888,7 +956,6 @@ def service(self):
|
||||
<ul class="hlist">
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_cli_script" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_commands" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_yaml_playbook" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.test_commands" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.ExecutionServiceServicer.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
+175
-230
@@ -99,8 +99,7 @@ el.replaceWith(d);
|
||||
self.stub = connpy_pb2_grpc.AIServiceStub(channel)
|
||||
self.remote_host = remote_host
|
||||
|
||||
@handle_errors
|
||||
def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, **overrides):
|
||||
def _ai_chat_stream(self, stub_method, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, chunk_callback=None, **overrides):
|
||||
import queue
|
||||
from rich.prompt import Prompt
|
||||
from rich.text import Text
|
||||
@@ -135,7 +134,7 @@ el.replaceWith(d);
|
||||
if req is None: break
|
||||
yield req
|
||||
|
||||
responses = self.stub.ask(request_generator())
|
||||
responses = stub_method(request_generator())
|
||||
|
||||
full_content = ""
|
||||
header_printed = False
|
||||
@@ -234,26 +233,32 @@ el.replaceWith(d);
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print header on first chunk
|
||||
alias = "architect" if current_responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
|
||||
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
|
||||
header_printed = True
|
||||
|
||||
# Initialize parser
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
if chunk_callback:
|
||||
header_printed = True
|
||||
else:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print header on first chunk
|
||||
alias = "architect" if current_responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
|
||||
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
|
||||
header_printed = True
|
||||
|
||||
# Initialize parser
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
|
||||
full_content += response.text_chunk
|
||||
md_parser.feed(response.text_chunk)
|
||||
if chunk_callback:
|
||||
chunk_callback(response.text_chunk)
|
||||
elif md_parser:
|
||||
md_parser.feed(response.text_chunk)
|
||||
continue
|
||||
|
||||
if response.is_final:
|
||||
if header_printed:
|
||||
if not chunk_callback and header_printed:
|
||||
from rich.rule import Rule
|
||||
md_parser.flush()
|
||||
|
||||
@@ -262,12 +267,8 @@ el.replaceWith(d);
|
||||
except: pass
|
||||
|
||||
final_result = from_struct(response.full_result)
|
||||
responder = final_result.get("responder", "engineer")
|
||||
alias = "architect" if responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
||||
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
||||
|
||||
if header_printed:
|
||||
if not chunk_callback and header_printed:
|
||||
from rich.console import Console as RichConsole
|
||||
from ..printer import connpy_theme, get_original_stdout
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
@@ -286,6 +287,104 @@ el.replaceWith(d);
|
||||
|
||||
return final_result
|
||||
|
||||
@handle_errors
|
||||
def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, **overrides):
|
||||
return self._ai_chat_stream(self.stub.ask, input_text, dryrun=dryrun, chat_history=chat_history, session_id=session_id, debug=debug, status=status, **overrides)
|
||||
|
||||
@handle_errors
|
||||
def build_playbook_chat(self, user_input, chat_history=None, status=None, chunk_callback=None):
|
||||
return self._ai_chat_stream(self.stub.build_playbook_chat, user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
def _process_unary_stream(self, responses, status=None, chunk_callback=None):
|
||||
full_content = ""
|
||||
header_printed = False
|
||||
final_result = {"response": "", "chat_history": []}
|
||||
md_parser = None
|
||||
|
||||
try:
|
||||
for response in responses:
|
||||
if response.status_update:
|
||||
if status:
|
||||
status.update(response.status_update)
|
||||
continue
|
||||
|
||||
if response.important_message:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
printer.console.print(Text.from_ansi(response.important_message))
|
||||
if status:
|
||||
try: status.start()
|
||||
except: pass
|
||||
continue
|
||||
|
||||
if not response.is_final:
|
||||
if response.text_chunk:
|
||||
if not header_printed:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
if chunk_callback:
|
||||
header_printed = True
|
||||
else:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print default header
|
||||
stable_console.print(Rule("[bold engineer]AI Analysis[/bold engineer]", style="engineer"))
|
||||
header_printed = True
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
|
||||
full_content += response.text_chunk
|
||||
if chunk_callback:
|
||||
chunk_callback(response.text_chunk)
|
||||
elif md_parser:
|
||||
md_parser.feed(response.text_chunk)
|
||||
continue
|
||||
|
||||
if response.is_final:
|
||||
if md_parser:
|
||||
md_parser.flush()
|
||||
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
final_result = from_struct(response.full_result)
|
||||
|
||||
if md_parser:
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
stable_console.print(Rule(style="engineer"))
|
||||
break
|
||||
except Exception as e:
|
||||
if isinstance(e, grpc.RpcError):
|
||||
raise
|
||||
printer.warning(f"Stream interrupted: {e}")
|
||||
|
||||
if full_content:
|
||||
final_result["streamed"] = True
|
||||
|
||||
return final_result
|
||||
|
||||
@handle_errors
|
||||
def analyze_execution_results(self, results, query=None, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.AnalyzeRequest(query=query or "")
|
||||
req.results.CopyFrom(to_struct(results))
|
||||
responses = self.stub.analyze_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)
|
||||
|
||||
@handle_errors
|
||||
def predict_execution_results(self, target_nodes, commands, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.PreflightRequest(target_nodes=target_nodes, commands=commands)
|
||||
responses = self.stub.predict_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)
|
||||
|
||||
@handle_errors
|
||||
def confirm(self, input_text, console=None):
|
||||
return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value
|
||||
@@ -333,6 +432,23 @@ el.replaceWith(d);
|
||||
<div class="desc"></div>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
<dt id="connpy.grpc_layer.stubs.AIStub.analyze_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">analyze_execution_results</span></span>(<span>self, results, query=None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@handle_errors
|
||||
def analyze_execution_results(self, results, query=None, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.AnalyzeRequest(query=query or "")
|
||||
req.results.CopyFrom(to_struct(results))
|
||||
responses = self.stub.analyze_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.AIStub.ask"><code class="name flex">
|
||||
<span>def <span class="ident">ask</span></span>(<span>self,<br>input_text,<br>dryrun=False,<br>chat_history=None,<br>session_id=None,<br>debug=False,<br>status=None,<br>**overrides)</span>
|
||||
</code></dt>
|
||||
@@ -343,190 +459,21 @@ el.replaceWith(d);
|
||||
</summary>
|
||||
<pre><code class="python">@handle_errors
|
||||
def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debug=False, status=None, **overrides):
|
||||
import queue
|
||||
from rich.prompt import Prompt
|
||||
from rich.text import Text
|
||||
from rich.panel import Panel
|
||||
from rich.markdown import Markdown
|
||||
|
||||
req_queue = queue.Queue()
|
||||
|
||||
initial_req = connpy_pb2.AskRequest(
|
||||
input_text=input_text,
|
||||
dryrun=dryrun,
|
||||
session_id=session_id or "",
|
||||
debug=debug,
|
||||
engineer_model=overrides.get("engineer_model", ""),
|
||||
engineer_api_key=overrides.get("engineer_api_key", ""),
|
||||
architect_model=overrides.get("architect_model", ""),
|
||||
architect_api_key=overrides.get("architect_api_key", ""),
|
||||
trust=overrides.get("trust", False)
|
||||
)
|
||||
if chat_history is not None:
|
||||
initial_req.chat_history.CopyFrom(to_value(chat_history))
|
||||
if "engineer_auth" in overrides and overrides["engineer_auth"]:
|
||||
initial_req.engineer_auth.CopyFrom(to_struct(overrides["engineer_auth"]))
|
||||
if "architect_auth" in overrides and overrides["architect_auth"]:
|
||||
initial_req.architect_auth.CopyFrom(to_struct(overrides["architect_auth"]))
|
||||
|
||||
req_queue.put(initial_req)
|
||||
|
||||
def request_generator():
|
||||
while True:
|
||||
req = req_queue.get()
|
||||
if req is None: break
|
||||
yield req
|
||||
|
||||
responses = self.stub.ask(request_generator())
|
||||
|
||||
full_content = ""
|
||||
header_printed = False
|
||||
current_responder = "engineer"
|
||||
final_result = {"response": "", "chat_history": []}
|
||||
|
||||
# Background thread to pull responses from gRPC into a local queue
|
||||
# This prevents KeyboardInterrupt from corrupting the gRPC iterator state
|
||||
response_queue = queue.Queue()
|
||||
|
||||
def pull_responses():
|
||||
try:
|
||||
for response in responses:
|
||||
response_queue.put(("data", response))
|
||||
except Exception as e:
|
||||
response_queue.put(("error", e))
|
||||
finally:
|
||||
response_queue.put((None, None))
|
||||
|
||||
threading.Thread(target=pull_responses, daemon=True).start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
# BLOCKING GET from local queue (interruptible by signal)
|
||||
msg_type, response = response_queue.get()
|
||||
except KeyboardInterrupt:
|
||||
# Signal interruption to the server
|
||||
if status:
|
||||
status.update("[error]Interrupted! Closing pending tasks...")
|
||||
|
||||
# Send the interrupt signal to the server
|
||||
req_queue.put(connpy_pb2.AskRequest(interrupt=True))
|
||||
|
||||
# CONTINUE the loop to receive remaining data and summary from the queue
|
||||
continue
|
||||
|
||||
if msg_type is None: # Sentinel
|
||||
break
|
||||
|
||||
if msg_type == "error":
|
||||
# Re-raise or handle gRPC error from background thread
|
||||
if isinstance(response, grpc.RpcError):
|
||||
raise response
|
||||
printer.warning(f"Stream interrupted: {response}")
|
||||
break
|
||||
|
||||
if response.status_update:
|
||||
if response.status_update.startswith("__RESPONDER__:"):
|
||||
current_responder = response.status_update.split(":")[1].lower()
|
||||
continue
|
||||
|
||||
if response.requires_confirmation:
|
||||
if status: status.stop()
|
||||
|
||||
# Show prompt and wait for answer
|
||||
prompt_text = Text.from_ansi(response.status_update)
|
||||
ans = Prompt.ask(prompt_text)
|
||||
|
||||
if status:
|
||||
status.update("[ai_status]Agent: Resuming...")
|
||||
status.start()
|
||||
|
||||
req_queue.put(connpy_pb2.AskRequest(confirmation_answer=ans))
|
||||
continue
|
||||
|
||||
if status:
|
||||
status.update(response.status_update)
|
||||
continue
|
||||
|
||||
if response.debug_message:
|
||||
if debug:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
printer.console.print(Text.from_ansi(response.debug_message))
|
||||
if status:
|
||||
try: status.start()
|
||||
except: pass
|
||||
continue
|
||||
|
||||
if response.important_message:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
printer.console.print(Text.from_ansi(response.important_message))
|
||||
if status:
|
||||
try: status.start()
|
||||
except: pass
|
||||
continue
|
||||
|
||||
if not response.is_final:
|
||||
if response.text_chunk:
|
||||
if not header_printed:
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
from rich.console import Console as RichConsole
|
||||
from rich.rule import Rule
|
||||
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
|
||||
# Print header on first chunk
|
||||
alias = "architect" if current_responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
|
||||
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
|
||||
header_printed = True
|
||||
|
||||
# Initialize parser
|
||||
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||
|
||||
full_content += response.text_chunk
|
||||
md_parser.feed(response.text_chunk)
|
||||
continue
|
||||
|
||||
if response.is_final:
|
||||
if header_printed:
|
||||
from rich.rule import Rule
|
||||
md_parser.flush()
|
||||
|
||||
if status:
|
||||
try: status.stop()
|
||||
except: pass
|
||||
|
||||
final_result = from_struct(response.full_result)
|
||||
responder = final_result.get("responder", "engineer")
|
||||
alias = "architect" if responder == "architect" else "engineer"
|
||||
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
||||
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
||||
|
||||
if header_printed:
|
||||
from rich.console import Console as RichConsole
|
||||
from ..printer import connpy_theme, get_original_stdout
|
||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||
stable_console.print(Rule(style=alias))
|
||||
break
|
||||
except Exception as e:
|
||||
# Check if it was a gRPC error that we should let handle_errors catch
|
||||
if isinstance(e, grpc.RpcError):
|
||||
raise
|
||||
printer.warning(f"Stream interrupted: {e}")
|
||||
finally:
|
||||
req_queue.put(None)
|
||||
|
||||
if full_content:
|
||||
final_result["streamed"] = True
|
||||
|
||||
return final_result</code></pre>
|
||||
return self._ai_chat_stream(self.stub.ask, input_text, dryrun=dryrun, chat_history=chat_history, session_id=session_id, debug=debug, status=status, **overrides)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.AIStub.build_playbook_chat"><code class="name flex">
|
||||
<span>def <span class="ident">build_playbook_chat</span></span>(<span>self, user_input, chat_history=None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@handle_errors
|
||||
def build_playbook_chat(self, user_input, chat_history=None, status=None, chunk_callback=None):
|
||||
return self._ai_chat_stream(self.stub.build_playbook_chat, user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
@@ -644,6 +591,22 @@ def load_session_data(self, session_id):
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.AIStub.predict_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">predict_execution_results</span></span>(<span>self, target_nodes, commands, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@handle_errors
|
||||
def predict_execution_results(self, target_nodes, commands, status=None, chunk_callback=None):
|
||||
req = connpy_pb2.PreflightRequest(target_nodes=target_nodes, commands=commands)
|
||||
responses = self.stub.predict_execution_results(req)
|
||||
return self._process_unary_stream(responses, status, chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.AuthClientInterceptor"><code class="flex name class">
|
||||
@@ -1124,12 +1087,7 @@ def update_setting(self, key, value):
|
||||
@handle_errors
|
||||
def run_cli_script(self, nodes_filter, script_path, parallel=10):
|
||||
req = connpy_pb2.ScriptRequest(param1=nodes_filter, param2=script_path, parallel=parallel)
|
||||
return from_struct(self.stub.run_cli_script(req).data)
|
||||
|
||||
@handle_errors
|
||||
def run_yaml_playbook(self, playbook_path, parallel=10):
|
||||
req = connpy_pb2.ScriptRequest(param1=playbook_path, parallel=parallel)
|
||||
return from_struct(self.stub.run_yaml_playbook(req).data)</code></pre>
|
||||
return from_struct(self.stub.run_cli_script(req).data)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
<h3>Methods</h3>
|
||||
@@ -1187,21 +1145,6 @@ def run_commands(self, nodes_filter, commands, variables=None, parallel=10, time
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.ExecutionStub.run_yaml_playbook"><code class="name flex">
|
||||
<span>def <span class="ident">run_yaml_playbook</span></span>(<span>self, playbook_path, parallel=10)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">@handle_errors
|
||||
def run_yaml_playbook(self, playbook_path, parallel=10):
|
||||
req = connpy_pb2.ScriptRequest(param1=playbook_path, parallel=parallel)
|
||||
return from_struct(self.stub.run_yaml_playbook(req).data)</code></pre>
|
||||
</details>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
<dt id="connpy.grpc_layer.stubs.ExecutionStub.test_commands"><code class="name flex">
|
||||
<span>def <span class="ident">test_commands</span></span>(<span>self,<br>nodes_filter,<br>commands,<br>expected,<br>variables=None,<br>parallel=10,<br>timeout=10,<br>prompt=None,<br>**kwargs)</span>
|
||||
</code></dt>
|
||||
@@ -2815,8 +2758,10 @@ def stop_api(self):
|
||||
<ul>
|
||||
<li>
|
||||
<h4><code><a title="connpy.grpc_layer.stubs.AIStub" href="#connpy.grpc_layer.stubs.AIStub">AIStub</a></code></h4>
|
||||
<ul class="two-column">
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.analyze_execution_results" href="#connpy.grpc_layer.stubs.AIStub.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.ask" href="#connpy.grpc_layer.stubs.AIStub.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.build_playbook_chat" href="#connpy.grpc_layer.stubs.AIStub.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.configure_mcp" href="#connpy.grpc_layer.stubs.AIStub.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.configure_provider" href="#connpy.grpc_layer.stubs.AIStub.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.confirm" href="#connpy.grpc_layer.stubs.AIStub.confirm">confirm</a></code></li>
|
||||
@@ -2824,6 +2769,7 @@ def stop_api(self):
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.list_mcp_servers" href="#connpy.grpc_layer.stubs.AIStub.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.list_sessions" href="#connpy.grpc_layer.stubs.AIStub.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.load_session_data" href="#connpy.grpc_layer.stubs.AIStub.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.AIStub.predict_execution_results" href="#connpy.grpc_layer.stubs.AIStub.predict_execution_results">predict_execution_results</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
@@ -2857,7 +2803,6 @@ def stop_api(self):
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.grpc_layer.stubs.ExecutionStub.run_cli_script" href="#connpy.grpc_layer.stubs.ExecutionStub.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.ExecutionStub.run_commands" href="#connpy.grpc_layer.stubs.ExecutionStub.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.ExecutionStub.run_yaml_playbook" href="#connpy.grpc_layer.stubs.ExecutionStub.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.grpc_layer.stubs.ExecutionStub.test_commands" href="#connpy.grpc_layer.stubs.ExecutionStub.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
+68
-46
@@ -185,6 +185,8 @@ response = myai.ask("What is the status of the BGP neighbors in the office?
|
||||
</code></pre>
|
||||
<hr>
|
||||
<p><em>For detailed developer notes and plugin hooks documentation, see the <a href="https://fluzzi.github.io/connpy/">Documentation</a>.</em></p>
|
||||
<h2 id="license">📜 License</h2>
|
||||
<p><a href="LICENSE">PolyForm Noncommercial 1.0.0</a></p>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="section-title" id="header-submodules">Sub-modules</h2>
|
||||
@@ -644,6 +646,7 @@ class ai:
|
||||
self.confirm_handler = confirm_handler or self._local_confirm_handler
|
||||
self.trusted_session = trust # Trust mode for the entire session
|
||||
self.interrupted = False
|
||||
self.one_shot = kwargs.get("one_shot", False)
|
||||
|
||||
|
||||
# 1. Cargar configuración genérica con herencia/merge global
|
||||
@@ -815,10 +818,13 @@ class ai:
|
||||
@property
|
||||
def architect_system_prompt(self):
|
||||
"""Build architect system prompt with plugin extensions."""
|
||||
prompt = self._architect_base_prompt
|
||||
if getattr(self, "one_shot", False):
|
||||
prompt += "\n\nCRITICAL 1-SHOT DIAGNOSTICS DIRECTIVE:\nYou are running in a 1-shot offline diagnostics mode. There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately and directly to the user. Do not suggest or attempt to delegate/return control to the engineer."
|
||||
if self.architect_prompt_extensions:
|
||||
extensions = "\n".join(self.architect_prompt_extensions)
|
||||
return self._architect_base_prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return self._architect_base_prompt
|
||||
return prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return prompt
|
||||
|
||||
def register_ai_tool(self, tool_definition, handler, target="engineer", engineer_prompt=None, architect_prompt=None, status_formatter=None):
|
||||
"""Register an external tool for the AI system.
|
||||
@@ -1295,13 +1301,11 @@ class ai:
|
||||
if self.interrupted:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
# Soft limit warning
|
||||
if iteration == self.soft_limit_iterations and not soft_limit_warned:
|
||||
self.console.print(f"[warning]⚠ Engineer has performed {iteration} steps. This is taking longer than expected.[/warning]")
|
||||
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]")
|
||||
soft_limit_warned = True
|
||||
|
||||
if status and not chat_history: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
|
||||
if status and not chat_history:
|
||||
status_text = f"[ai_status]Engineer: Analyzing mission... (step {iteration})"
|
||||
if iteration >= self.soft_limit_iterations:
|
||||
status_text += " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]"
|
||||
status.update(status_text)
|
||||
|
||||
try:
|
||||
safe_messages = self._sanitize_messages(messages)
|
||||
@@ -1326,17 +1330,23 @@ class ai:
|
||||
|
||||
# Notificación en tiempo real de la tarea técnica (Only if not in Architect loop)
|
||||
if status and not chat_history:
|
||||
if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}")
|
||||
s_text = ""
|
||||
if fn == "list_nodes": s_text = f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}"
|
||||
elif fn == "run_commands":
|
||||
cmds = args.get('commands', [])
|
||||
cmd_str = cmds[0] if cmds else ""
|
||||
status.update(f"[ai_status]Engineer: [CMD] {cmd_str}")
|
||||
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}")
|
||||
s_text = f"[ai_status]Engineer: [CMD] {cmd_str}"
|
||||
elif fn == "get_node_info": s_text = f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}"
|
||||
elif fn.startswith("mcp_"):
|
||||
server = fn.split("__")[0].replace("mcp_", "")
|
||||
tool = fn.split("__")[1] if "__" in fn else fn
|
||||
status.update(f"[ai_status]Engineer: [MCP:{server}] {tool}")
|
||||
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args))
|
||||
s_text = f"[ai_status]Engineer: [MCP:{server}] {tool}"
|
||||
elif fn in self.tool_status_formatters: s_text = self.tool_status_formatters[fn](args)
|
||||
|
||||
if s_text:
|
||||
if iteration >= self.soft_limit_iterations:
|
||||
s_text += " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]"
|
||||
status.update(s_text)
|
||||
|
||||
if debug:
|
||||
self._print_debug_observation(f"Decision: {fn}", args, status=status)
|
||||
@@ -1406,6 +1416,8 @@ class ai:
|
||||
{"type": "function", "function": {"name": "return_to_engineer", "description": "Return control to the Engineer. Use this when your strategic analysis is complete and the Engineer should handle the rest of the conversation.", "parameters": {"type": "object", "properties": {"summary": {"type": "string", "description": "Brief summary of your analysis to hand over to the Engineer."}}, "required": ["summary"]}}},
|
||||
{"type": "function", "function": {"name": "manage_memory_tool", "description": "Saves information to long-term memory. MANDATORY: Only use this if the user explicitly asks to remember or save something.", "parameters": {"type": "object", "properties": {"content": {"type": "string"}, "action": {"type": "string", "enum": ["append", "replace"]}}, "required": ["content"]}}}
|
||||
]
|
||||
if getattr(self, "one_shot", False):
|
||||
base_tools = [t for t in base_tools if t["function"]["name"] not in ("delegate_to_engineer", "return_to_engineer")]
|
||||
|
||||
all_tools = base_tools + self.external_architect_tools
|
||||
seen_names = set()
|
||||
@@ -1541,11 +1553,18 @@ class ai:
|
||||
|
||||
@MethodHook
|
||||
def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None):
|
||||
soft_limit_warned = False
|
||||
is_engineer_keyless = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower()
|
||||
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
|
||||
raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth <auth>' to set it.")
|
||||
|
||||
def update_status(text):
|
||||
if not status:
|
||||
return
|
||||
if iteration >= self.soft_limit_iterations:
|
||||
warning_suffix = " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]"
|
||||
if warning_suffix not in text:
|
||||
text += warning_suffix
|
||||
status.update(text)
|
||||
|
||||
if chat_history is None: chat_history = []
|
||||
|
||||
@@ -1636,18 +1655,14 @@ class ai:
|
||||
if self.interrupted:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
# Soft limit warning
|
||||
if iteration == self.soft_limit_iterations and not soft_limit_warned:
|
||||
self.console.print(f"[warning]⚠ Agent has performed {iteration} steps. This is taking longer than expected.[/warning]")
|
||||
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary of progress.[/warning]")
|
||||
soft_limit_warned = True
|
||||
# Soft limit warning - handled inline within update_status
|
||||
|
||||
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
||||
if status:
|
||||
# Notify responder identity for web/remote clients
|
||||
if getattr(status, "is_web", False) or getattr(status, "is_remote", False):
|
||||
status.update(f"__RESPONDER__:{current_brain}")
|
||||
status.update(f"{label} is thinking... (step {iteration})")
|
||||
update_status(f"{label} is thinking... (step {iteration})")
|
||||
|
||||
streamed_response = False
|
||||
try:
|
||||
@@ -1662,7 +1677,7 @@ class ai:
|
||||
response = completion(model=model, messages=safe_messages, tools=tools, num_retries=3, **current_auth)
|
||||
except Exception as e:
|
||||
if current_brain == "architect":
|
||||
if status: status.update("[unavailable]Architect unavailable! Falling back to Engineer...")
|
||||
if status: update_status("[unavailable]Architect unavailable! Falling back to Engineer...")
|
||||
# Preserve context when falling back - use clean_input directly
|
||||
current_brain = "engineer"
|
||||
model = self.engineer_model
|
||||
@@ -1719,8 +1734,8 @@ class ai:
|
||||
continue
|
||||
|
||||
if status:
|
||||
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
||||
if fn == "delegate_to_engineer": update_status(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||
elif fn == "manage_memory_tool": update_status(f"[architect]Architect: [UPDATING MEMORY]")
|
||||
|
||||
if debug:
|
||||
self._print_debug_observation(f"Decision: {fn}", args, status=status)
|
||||
@@ -1729,7 +1744,7 @@ class ai:
|
||||
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
||||
usage["input"] += eng_usage["input"]; usage["output"] += eng_usage["output"]; usage["total"] += eng_usage["total"]
|
||||
elif fn == "consult_architect":
|
||||
if status: status.update("[architect]Engineer consulting Architect...")
|
||||
if status: update_status("[architect]Engineer consulting Architect...")
|
||||
try:
|
||||
# Consultation only - Engineer stays in control
|
||||
claude_resp = completion(
|
||||
@@ -1751,11 +1766,11 @@ class ai:
|
||||
try: status.start()
|
||||
except: pass
|
||||
except Exception as e:
|
||||
if status: status.update("[unavailable]Architect unavailable! Engineer continuing alone...")
|
||||
if status: update_status("[unavailable]Architect unavailable! Engineer continuing alone...")
|
||||
obs = f"Architect unavailable ({str(e)}). Proceeding with your best technical judgment."
|
||||
|
||||
elif fn == "escalate_to_architect":
|
||||
if status: status.update("[architect]Transferring control to Architect...")
|
||||
if status: update_status("[architect]Transferring control to Architect...")
|
||||
# Full escalation - Architect takes over
|
||||
current_brain = "architect"
|
||||
model = self.architect_model
|
||||
@@ -1777,7 +1792,7 @@ class ai:
|
||||
except: pass
|
||||
|
||||
elif fn == "return_to_engineer":
|
||||
if status: status.update("[engineer]Transferring control back to Engineer...")
|
||||
if status: update_status("[engineer]Transferring control back to Engineer...")
|
||||
# Architect returns control to Engineer
|
||||
current_brain = "engineer"
|
||||
model = self.engineer_model
|
||||
@@ -1830,7 +1845,7 @@ class ai:
|
||||
messages.append(resp_msg.model_dump(exclude_none=True))
|
||||
except Exception as e:
|
||||
if status:
|
||||
status.update(f"[error]Error fetching summary: {e}[/error]")
|
||||
update_status(f"[error]Error fetching summary: {e}[/error]")
|
||||
printer.warning(f"Failed to fetch final summary from LLM: {e}")
|
||||
except KeyboardInterrupt:
|
||||
if status: status.update("[error]Interrupted! Closing pending tasks...")
|
||||
@@ -2167,10 +2182,13 @@ Node: {node_name}"""
|
||||
<pre><code class="python">@property
|
||||
def architect_system_prompt(self):
|
||||
"""Build architect system prompt with plugin extensions."""
|
||||
prompt = self._architect_base_prompt
|
||||
if getattr(self, "one_shot", False):
|
||||
prompt += "\n\nCRITICAL 1-SHOT DIAGNOSTICS DIRECTIVE:\nYou are running in a 1-shot offline diagnostics mode. There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately and directly to the user. Do not suggest or attempt to delegate/return control to the engineer."
|
||||
if self.architect_prompt_extensions:
|
||||
extensions = "\n".join(self.architect_prompt_extensions)
|
||||
return self._architect_base_prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return self._architect_base_prompt</code></pre>
|
||||
return prompt + f"\n\nPlugin Capabilities:\n{extensions}"
|
||||
return prompt</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Build architect system prompt with plugin extensions.</p></div>
|
||||
</dd>
|
||||
@@ -2488,11 +2506,18 @@ Node: {node_name}"""
|
||||
</summary>
|
||||
<pre><code class="python">@MethodHook
|
||||
def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None):
|
||||
soft_limit_warned = False
|
||||
is_engineer_keyless = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower()
|
||||
if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
|
||||
raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth <auth>' to set it.")
|
||||
|
||||
def update_status(text):
|
||||
if not status:
|
||||
return
|
||||
if iteration >= self.soft_limit_iterations:
|
||||
warning_suffix = " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]"
|
||||
if warning_suffix not in text:
|
||||
text += warning_suffix
|
||||
status.update(text)
|
||||
|
||||
if chat_history is None: chat_history = []
|
||||
|
||||
@@ -2583,18 +2608,14 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
if self.interrupted:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
# Soft limit warning
|
||||
if iteration == self.soft_limit_iterations and not soft_limit_warned:
|
||||
self.console.print(f"[warning]⚠ Agent has performed {iteration} steps. This is taking longer than expected.[/warning]")
|
||||
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary of progress.[/warning]")
|
||||
soft_limit_warned = True
|
||||
# Soft limit warning - handled inline within update_status
|
||||
|
||||
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
|
||||
if status:
|
||||
# Notify responder identity for web/remote clients
|
||||
if getattr(status, "is_web", False) or getattr(status, "is_remote", False):
|
||||
status.update(f"__RESPONDER__:{current_brain}")
|
||||
status.update(f"{label} is thinking... (step {iteration})")
|
||||
update_status(f"{label} is thinking... (step {iteration})")
|
||||
|
||||
streamed_response = False
|
||||
try:
|
||||
@@ -2609,7 +2630,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
response = completion(model=model, messages=safe_messages, tools=tools, num_retries=3, **current_auth)
|
||||
except Exception as e:
|
||||
if current_brain == "architect":
|
||||
if status: status.update("[unavailable]Architect unavailable! Falling back to Engineer...")
|
||||
if status: update_status("[unavailable]Architect unavailable! Falling back to Engineer...")
|
||||
# Preserve context when falling back - use clean_input directly
|
||||
current_brain = "engineer"
|
||||
model = self.engineer_model
|
||||
@@ -2666,8 +2687,8 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
continue
|
||||
|
||||
if status:
|
||||
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]")
|
||||
if fn == "delegate_to_engineer": update_status(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
|
||||
elif fn == "manage_memory_tool": update_status(f"[architect]Architect: [UPDATING MEMORY]")
|
||||
|
||||
if debug:
|
||||
self._print_debug_observation(f"Decision: {fn}", args, status=status)
|
||||
@@ -2676,7 +2697,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1])
|
||||
usage["input"] += eng_usage["input"]; usage["output"] += eng_usage["output"]; usage["total"] += eng_usage["total"]
|
||||
elif fn == "consult_architect":
|
||||
if status: status.update("[architect]Engineer consulting Architect...")
|
||||
if status: update_status("[architect]Engineer consulting Architect...")
|
||||
try:
|
||||
# Consultation only - Engineer stays in control
|
||||
claude_resp = completion(
|
||||
@@ -2698,11 +2719,11 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
try: status.start()
|
||||
except: pass
|
||||
except Exception as e:
|
||||
if status: status.update("[unavailable]Architect unavailable! Engineer continuing alone...")
|
||||
if status: update_status("[unavailable]Architect unavailable! Engineer continuing alone...")
|
||||
obs = f"Architect unavailable ({str(e)}). Proceeding with your best technical judgment."
|
||||
|
||||
elif fn == "escalate_to_architect":
|
||||
if status: status.update("[architect]Transferring control to Architect...")
|
||||
if status: update_status("[architect]Transferring control to Architect...")
|
||||
# Full escalation - Architect takes over
|
||||
current_brain = "architect"
|
||||
model = self.architect_model
|
||||
@@ -2724,7 +2745,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
except: pass
|
||||
|
||||
elif fn == "return_to_engineer":
|
||||
if status: status.update("[engineer]Transferring control back to Engineer...")
|
||||
if status: update_status("[engineer]Transferring control back to Engineer...")
|
||||
# Architect returns control to Engineer
|
||||
current_brain = "engineer"
|
||||
model = self.engineer_model
|
||||
@@ -2777,7 +2798,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
|
||||
messages.append(resp_msg.model_dump(exclude_none=True))
|
||||
except Exception as e:
|
||||
if status:
|
||||
status.update(f"[error]Error fetching summary: {e}[/error]")
|
||||
update_status(f"[error]Error fetching summary: {e}[/error]")
|
||||
printer.warning(f"Failed to fetch final summary from LLM: {e}")
|
||||
except KeyboardInterrupt:
|
||||
if status: status.update("[error]Interrupted! Closing pending tasks...")
|
||||
@@ -6384,6 +6405,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
|
||||
<li><a href="#ai-programmatic-use">AI Programmatic Use</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#license">📜 License</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -369,7 +369,41 @@ el.replaceWith(d);
|
||||
"""Load a session's raw data by ID."""
|
||||
from connpy.ai import ai
|
||||
agent = ai(self.config)
|
||||
return agent.load_session_data(session_id)</code></pre>
|
||||
return agent.load_session_data(session_id)
|
||||
|
||||
def build_playbook_chat(self, user_input: str, chat_history: list = None, status=None, chunk_callback=None):
|
||||
"""Interact with the specialized Playbook Builder Agent."""
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
agent = PlaybookBuilderAgent(self.config)
|
||||
return agent.ask(user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
def analyze_execution_results(self, results: dict, query: str = None, status=None, chunk_callback=None):
|
||||
"""Analyze actual command execution results using Network Architect 1-shot."""
|
||||
import json
|
||||
results_str = json.dumps(results, indent=2)
|
||||
|
||||
prompt = f"@architect: Please analyze the following actual execution results. Diagnose any issues, highlight successful actions, and suggest strategic remediation steps if needed."
|
||||
if query:
|
||||
prompt += f"\nSpecific user request: {query}"
|
||||
prompt += f"\n\nResults Data:\n{results_str}"
|
||||
prompt += "\n\nCRITICAL DIRECTIVE: You are running in a strictly 1-shot offline diagnostics mode (--analyze). There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately. DO NOT suggest, mention, or attempt to delegate the session back to the engineer."
|
||||
|
||||
# Delegate to self.ask, setting stream=True and forwarding callback/status.
|
||||
# This will invoke standard ai.ask with '@architect:' prefix, forcing 1-shot architect brain.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback, one_shot=True)
|
||||
|
||||
def predict_execution_results(self, target_nodes: list, commands: list, status=None, chunk_callback=None):
|
||||
"""Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot)."""
|
||||
nodes_str = ", ".join(target_nodes)
|
||||
commands_str = "\n".join(f"- {cmd}" for cmd in commands)
|
||||
|
||||
prompt = f"@engineer: Act as a Preflight Simulation Agent. Simulate and predict the expected outputs and behaviors of the following commands on the target nodes. Alert about potential safety or configuration risks based on node profiles."
|
||||
prompt += f"\n\nTarget Nodes: {nodes_str}"
|
||||
prompt += f"\nCommands to simulate:\n{commands_str}"
|
||||
prompt += "\n\nCRITICAL SCALABILITY DIRECTIVE: If there are many target nodes, DO NOT list predictions node-by-node. Instead, group them by Operating System, vendor, or platform, and provide a highly concise Executive Summary. Detail individual risks only for nodes that present specific anomalies or security concerns. Focus on overall impact."
|
||||
|
||||
# Delegate to self.ask, using the standard engineer brain but with the simulated preflight prompt.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Business logic for interacting with AI agents and LLM configurations.</p>
|
||||
<p>Initialize the service.</p>
|
||||
@@ -402,6 +436,31 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Ask the AI copilot for terminal assistance asynchronously.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.analyze_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">analyze_execution_results</span></span>(<span>self, results: dict, query: str = None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def analyze_execution_results(self, results: dict, query: str = None, status=None, chunk_callback=None):
|
||||
"""Analyze actual command execution results using Network Architect 1-shot."""
|
||||
import json
|
||||
results_str = json.dumps(results, indent=2)
|
||||
|
||||
prompt = f"@architect: Please analyze the following actual execution results. Diagnose any issues, highlight successful actions, and suggest strategic remediation steps if needed."
|
||||
if query:
|
||||
prompt += f"\nSpecific user request: {query}"
|
||||
prompt += f"\n\nResults Data:\n{results_str}"
|
||||
prompt += "\n\nCRITICAL DIRECTIVE: You are running in a strictly 1-shot offline diagnostics mode (--analyze). There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately. DO NOT suggest, mention, or attempt to delegate the session back to the engineer."
|
||||
|
||||
# Delegate to self.ask, setting stream=True and forwarding callback/status.
|
||||
# This will invoke standard ai.ask with '@architect:' prefix, forcing 1-shot architect brain.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback, one_shot=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Analyze actual command execution results using Network Architect 1-shot.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.ask"><code class="name flex">
|
||||
<span>def <span class="ident">ask</span></span>(<span>self,<br>input_text,<br>dryrun=False,<br>chat_history=None,<br>status=None,<br>debug=False,<br>session_id=None,<br>console=None,<br>chunk_callback=None,<br>confirm_handler=None,<br>trust=False,<br>**overrides)</span>
|
||||
</code></dt>
|
||||
@@ -559,6 +618,22 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.build_playbook_chat"><code class="name flex">
|
||||
<span>def <span class="ident">build_playbook_chat</span></span>(<span>self, user_input: str, chat_history: list = None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def build_playbook_chat(self, user_input: str, chat_history: list = None, status=None, chunk_callback=None):
|
||||
"""Interact with the specialized Playbook Builder Agent."""
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
agent = PlaybookBuilderAgent(self.config)
|
||||
return agent.ask(user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Interact with the specialized Playbook Builder Agent.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.configure_mcp"><code class="name flex">
|
||||
<span>def <span class="ident">configure_mcp</span></span>(<span>self, name, url=None, enabled=None, auto_load_on_os=None, remove=False)</span>
|
||||
</code></dt>
|
||||
@@ -715,6 +790,29 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Load a session's raw data by ID.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.predict_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">predict_execution_results</span></span>(<span>self, target_nodes: list, commands: list, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def predict_execution_results(self, target_nodes: list, commands: list, status=None, chunk_callback=None):
|
||||
"""Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot)."""
|
||||
nodes_str = ", ".join(target_nodes)
|
||||
commands_str = "\n".join(f"- {cmd}" for cmd in commands)
|
||||
|
||||
prompt = f"@engineer: Act as a Preflight Simulation Agent. Simulate and predict the expected outputs and behaviors of the following commands on the target nodes. Alert about potential safety or configuration risks based on node profiles."
|
||||
prompt += f"\n\nTarget Nodes: {nodes_str}"
|
||||
prompt += f"\nCommands to simulate:\n{commands_str}"
|
||||
prompt += "\n\nCRITICAL SCALABILITY DIRECTIVE: If there are many target nodes, DO NOT list predictions node-by-node. Instead, group them by Operating System, vendor, or platform, and provide a highly concise Executive Summary. Detail individual risks only for nodes that present specific anomalies or security concerns. Focus on overall impact."
|
||||
|
||||
# Delegate to self.ask, using the standard engineer brain but with the simulated preflight prompt.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot).</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ai_service.AIService.process_copilot_input"><code class="name flex">
|
||||
<span>def <span class="ident">process_copilot_input</span></span>(<span>self, input_text: str, session_state: dict) ‑> dict</span>
|
||||
</code></dt>
|
||||
@@ -813,9 +911,11 @@ el.replaceWith(d);
|
||||
<h4><code><a title="connpy.services.ai_service.AIService" href="#connpy.services.ai_service.AIService">AIService</a></code></h4>
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.services.ai_service.AIService.aask_copilot" href="#connpy.services.ai_service.AIService.aask_copilot">aask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.analyze_execution_results" href="#connpy.services.ai_service.AIService.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.ask" href="#connpy.services.ai_service.AIService.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.ask_copilot" href="#connpy.services.ai_service.AIService.ask_copilot">ask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.build_context_blocks" href="#connpy.services.ai_service.AIService.build_context_blocks">build_context_blocks</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.build_playbook_chat" href="#connpy.services.ai_service.AIService.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.configure_mcp" href="#connpy.services.ai_service.AIService.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.configure_provider" href="#connpy.services.ai_service.AIService.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.confirm" href="#connpy.services.ai_service.AIService.confirm">confirm</a></code></li>
|
||||
@@ -823,6 +923,7 @@ el.replaceWith(d);
|
||||
<li><code><a title="connpy.services.ai_service.AIService.list_mcp_servers" href="#connpy.services.ai_service.AIService.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.list_sessions" href="#connpy.services.ai_service.AIService.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.load_session_data" href="#connpy.services.ai_service.AIService.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.predict_execution_results" href="#connpy.services.ai_service.AIService.predict_execution_results">predict_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.services.ai_service.AIService.process_copilot_input" href="#connpy.services.ai_service.AIService.process_copilot_input">process_copilot_input</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -156,56 +156,7 @@ el.replaceWith(d);
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to read script {script_path}: {e}")
|
||||
|
||||
return self.run_commands(nodes_filter, commands, parallel=parallel)
|
||||
|
||||
def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
|
||||
"""Run a structured Connpy YAML automation playbook (from path or content)."""
|
||||
playbook = None
|
||||
if playbook_data.startswith("---YAML---\n"):
|
||||
try:
|
||||
content = playbook_data[len("---YAML---\n"):]
|
||||
playbook = yaml.load(content, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to parse YAML content: {e}")
|
||||
else:
|
||||
if not os.path.exists(playbook_data):
|
||||
raise ConnpyError(f"Playbook file not found: {playbook_data}")
|
||||
try:
|
||||
with open(playbook_data, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
|
||||
raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
|
||||
|
||||
action = playbook.get("action", "run")
|
||||
options = playbook.get("options", {})
|
||||
|
||||
# Extract all fields similar to RunHandler.cli_run
|
||||
exec_args = {
|
||||
"nodes_filter": playbook["nodes"],
|
||||
"commands": playbook["commands"],
|
||||
"variables": playbook.get("variables"),
|
||||
"parallel": options.get("parallel", parallel),
|
||||
"timeout": playbook.get("timeout", options.get("timeout", 20)),
|
||||
"prompt": options.get("prompt"),
|
||||
"name": playbook.get("name", "Task")
|
||||
}
|
||||
|
||||
# Map 'output' field to folder path if it's not stdout/null
|
||||
output_cfg = playbook.get("output")
|
||||
if output_cfg not in [None, "stdout"]:
|
||||
exec_args["folder"] = output_cfg
|
||||
|
||||
if action == "run":
|
||||
return self.run_commands(**exec_args)
|
||||
elif action == "test":
|
||||
exec_args["expected"] = playbook.get("expected", [])
|
||||
return self.test_commands(**exec_args)
|
||||
else:
|
||||
raise ConnpyError(f"Unsupported playbook action: {action}")</code></pre>
|
||||
return self.run_commands(nodes_filter, commands, parallel=parallel)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Business logic for executing commands on nodes and running automation scripts.</p>
|
||||
<p>Initialize the service.</p>
|
||||
@@ -300,65 +251,6 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Execute commands on a set of nodes.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.execution_service.ExecutionService.run_yaml_playbook"><code class="name flex">
|
||||
<span>def <span class="ident">run_yaml_playbook</span></span>(<span>self, playbook_data: str, parallel: int = 10) ‑> Dict[str, Any]</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
|
||||
"""Run a structured Connpy YAML automation playbook (from path or content)."""
|
||||
playbook = None
|
||||
if playbook_data.startswith("---YAML---\n"):
|
||||
try:
|
||||
content = playbook_data[len("---YAML---\n"):]
|
||||
playbook = yaml.load(content, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to parse YAML content: {e}")
|
||||
else:
|
||||
if not os.path.exists(playbook_data):
|
||||
raise ConnpyError(f"Playbook file not found: {playbook_data}")
|
||||
try:
|
||||
with open(playbook_data, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
|
||||
raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
|
||||
|
||||
action = playbook.get("action", "run")
|
||||
options = playbook.get("options", {})
|
||||
|
||||
# Extract all fields similar to RunHandler.cli_run
|
||||
exec_args = {
|
||||
"nodes_filter": playbook["nodes"],
|
||||
"commands": playbook["commands"],
|
||||
"variables": playbook.get("variables"),
|
||||
"parallel": options.get("parallel", parallel),
|
||||
"timeout": playbook.get("timeout", options.get("timeout", 20)),
|
||||
"prompt": options.get("prompt"),
|
||||
"name": playbook.get("name", "Task")
|
||||
}
|
||||
|
||||
# Map 'output' field to folder path if it's not stdout/null
|
||||
output_cfg = playbook.get("output")
|
||||
if output_cfg not in [None, "stdout"]:
|
||||
exec_args["folder"] = output_cfg
|
||||
|
||||
if action == "run":
|
||||
return self.run_commands(**exec_args)
|
||||
elif action == "test":
|
||||
exec_args["expected"] = playbook.get("expected", [])
|
||||
return self.test_commands(**exec_args)
|
||||
else:
|
||||
raise ConnpyError(f"Unsupported playbook action: {action}")</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Run a structured Connpy YAML automation playbook (from path or content).</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.execution_service.ExecutionService.test_commands"><code class="name flex">
|
||||
<span>def <span class="ident">test_commands</span></span>(<span>self,<br>nodes_filter: str,<br>commands: List[str],<br>expected: List[str],<br>variables: Dict[str, Any] | None = None,<br>parallel: int = 10,<br>timeout: int = 20,<br>folder: str | None = None,<br>prompt: str | None = None,<br>on_node_complete: Callable | None = None,<br>logger: Callable | None = None,<br>name: str | None = None) ‑> Dict[str, Dict[str, bool]]</span>
|
||||
</code></dt>
|
||||
@@ -439,7 +331,6 @@ el.replaceWith(d);
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.services.execution_service.ExecutionService.run_cli_script" href="#connpy.services.execution_service.ExecutionService.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.services.execution_service.ExecutionService.run_commands" href="#connpy.services.execution_service.ExecutionService.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.services.execution_service.ExecutionService.run_yaml_playbook" href="#connpy.services.execution_service.ExecutionService.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.services.execution_service.ExecutionService.test_commands" href="#connpy.services.execution_service.ExecutionService.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
+103
-111
@@ -428,7 +428,41 @@ el.replaceWith(d);
|
||||
"""Load a session's raw data by ID."""
|
||||
from connpy.ai import ai
|
||||
agent = ai(self.config)
|
||||
return agent.load_session_data(session_id)</code></pre>
|
||||
return agent.load_session_data(session_id)
|
||||
|
||||
def build_playbook_chat(self, user_input: str, chat_history: list = None, status=None, chunk_callback=None):
|
||||
"""Interact with the specialized Playbook Builder Agent."""
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
agent = PlaybookBuilderAgent(self.config)
|
||||
return agent.ask(user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)
|
||||
|
||||
def analyze_execution_results(self, results: dict, query: str = None, status=None, chunk_callback=None):
|
||||
"""Analyze actual command execution results using Network Architect 1-shot."""
|
||||
import json
|
||||
results_str = json.dumps(results, indent=2)
|
||||
|
||||
prompt = f"@architect: Please analyze the following actual execution results. Diagnose any issues, highlight successful actions, and suggest strategic remediation steps if needed."
|
||||
if query:
|
||||
prompt += f"\nSpecific user request: {query}"
|
||||
prompt += f"\n\nResults Data:\n{results_str}"
|
||||
prompt += "\n\nCRITICAL DIRECTIVE: You are running in a strictly 1-shot offline diagnostics mode (--analyze). There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately. DO NOT suggest, mention, or attempt to delegate the session back to the engineer."
|
||||
|
||||
# Delegate to self.ask, setting stream=True and forwarding callback/status.
|
||||
# This will invoke standard ai.ask with '@architect:' prefix, forcing 1-shot architect brain.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback, one_shot=True)
|
||||
|
||||
def predict_execution_results(self, target_nodes: list, commands: list, status=None, chunk_callback=None):
|
||||
"""Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot)."""
|
||||
nodes_str = ", ".join(target_nodes)
|
||||
commands_str = "\n".join(f"- {cmd}" for cmd in commands)
|
||||
|
||||
prompt = f"@engineer: Act as a Preflight Simulation Agent. Simulate and predict the expected outputs and behaviors of the following commands on the target nodes. Alert about potential safety or configuration risks based on node profiles."
|
||||
prompt += f"\n\nTarget Nodes: {nodes_str}"
|
||||
prompt += f"\nCommands to simulate:\n{commands_str}"
|
||||
prompt += "\n\nCRITICAL SCALABILITY DIRECTIVE: If there are many target nodes, DO NOT list predictions node-by-node. Instead, group them by Operating System, vendor, or platform, and provide a highly concise Executive Summary. Detail individual risks only for nodes that present specific anomalies or security concerns. Focus on overall impact."
|
||||
|
||||
# Delegate to self.ask, using the standard engineer brain but with the simulated preflight prompt.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Business logic for interacting with AI agents and LLM configurations.</p>
|
||||
<p>Initialize the service.</p>
|
||||
@@ -461,6 +495,31 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Ask the AI copilot for terminal assistance asynchronously.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.analyze_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">analyze_execution_results</span></span>(<span>self, results: dict, query: str = None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def analyze_execution_results(self, results: dict, query: str = None, status=None, chunk_callback=None):
|
||||
"""Analyze actual command execution results using Network Architect 1-shot."""
|
||||
import json
|
||||
results_str = json.dumps(results, indent=2)
|
||||
|
||||
prompt = f"@architect: Please analyze the following actual execution results. Diagnose any issues, highlight successful actions, and suggest strategic remediation steps if needed."
|
||||
if query:
|
||||
prompt += f"\nSpecific user request: {query}"
|
||||
prompt += f"\n\nResults Data:\n{results_str}"
|
||||
prompt += "\n\nCRITICAL DIRECTIVE: You are running in a strictly 1-shot offline diagnostics mode (--analyze). There is no active conversation loop, and you are NOT conversing with a Network Engineer. You MUST deliver your complete strategic analysis immediately. DO NOT suggest, mention, or attempt to delegate the session back to the engineer."
|
||||
|
||||
# Delegate to self.ask, setting stream=True and forwarding callback/status.
|
||||
# This will invoke standard ai.ask with '@architect:' prefix, forcing 1-shot architect brain.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback, one_shot=True)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Analyze actual command execution results using Network Architect 1-shot.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.ask"><code class="name flex">
|
||||
<span>def <span class="ident">ask</span></span>(<span>self,<br>input_text,<br>dryrun=False,<br>chat_history=None,<br>status=None,<br>debug=False,<br>session_id=None,<br>console=None,<br>chunk_callback=None,<br>confirm_handler=None,<br>trust=False,<br>**overrides)</span>
|
||||
</code></dt>
|
||||
@@ -618,6 +677,22 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.build_playbook_chat"><code class="name flex">
|
||||
<span>def <span class="ident">build_playbook_chat</span></span>(<span>self, user_input: str, chat_history: list = None, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def build_playbook_chat(self, user_input: str, chat_history: list = None, status=None, chunk_callback=None):
|
||||
"""Interact with the specialized Playbook Builder Agent."""
|
||||
from connpy.ai import PlaybookBuilderAgent
|
||||
agent = PlaybookBuilderAgent(self.config)
|
||||
return agent.ask(user_input, chat_history=chat_history, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Interact with the specialized Playbook Builder Agent.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.configure_mcp"><code class="name flex">
|
||||
<span>def <span class="ident">configure_mcp</span></span>(<span>self, name, url=None, enabled=None, auto_load_on_os=None, remove=False)</span>
|
||||
</code></dt>
|
||||
@@ -774,6 +849,29 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Load a session's raw data by ID.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.predict_execution_results"><code class="name flex">
|
||||
<span>def <span class="ident">predict_execution_results</span></span>(<span>self, target_nodes: list, commands: list, status=None, chunk_callback=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def predict_execution_results(self, target_nodes: list, commands: list, status=None, chunk_callback=None):
|
||||
"""Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot)."""
|
||||
nodes_str = ", ".join(target_nodes)
|
||||
commands_str = "\n".join(f"- {cmd}" for cmd in commands)
|
||||
|
||||
prompt = f"@engineer: Act as a Preflight Simulation Agent. Simulate and predict the expected outputs and behaviors of the following commands on the target nodes. Alert about potential safety or configuration risks based on node profiles."
|
||||
prompt += f"\n\nTarget Nodes: {nodes_str}"
|
||||
prompt += f"\nCommands to simulate:\n{commands_str}"
|
||||
prompt += "\n\nCRITICAL SCALABILITY DIRECTIVE: If there are many target nodes, DO NOT list predictions node-by-node. Instead, group them by Operating System, vendor, or platform, and provide a highly concise Executive Summary. Detail individual risks only for nodes that present specific anomalies or security concerns. Focus on overall impact."
|
||||
|
||||
# Delegate to self.ask, using the standard engineer brain but with the simulated preflight prompt.
|
||||
return self.ask(prompt, status=status, chunk_callback=chunk_callback)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Predict and simulate execution results preventively using the Preflight Simulation Agent (1-shot).</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.AIService.process_copilot_input"><code class="name flex">
|
||||
<span>def <span class="ident">process_copilot_input</span></span>(<span>self, input_text: str, session_state: dict) ‑> dict</span>
|
||||
</code></dt>
|
||||
@@ -1255,56 +1353,7 @@ el.replaceWith(d);
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to read script {script_path}: {e}")
|
||||
|
||||
return self.run_commands(nodes_filter, commands, parallel=parallel)
|
||||
|
||||
def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
|
||||
"""Run a structured Connpy YAML automation playbook (from path or content)."""
|
||||
playbook = None
|
||||
if playbook_data.startswith("---YAML---\n"):
|
||||
try:
|
||||
content = playbook_data[len("---YAML---\n"):]
|
||||
playbook = yaml.load(content, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to parse YAML content: {e}")
|
||||
else:
|
||||
if not os.path.exists(playbook_data):
|
||||
raise ConnpyError(f"Playbook file not found: {playbook_data}")
|
||||
try:
|
||||
with open(playbook_data, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
|
||||
raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
|
||||
|
||||
action = playbook.get("action", "run")
|
||||
options = playbook.get("options", {})
|
||||
|
||||
# Extract all fields similar to RunHandler.cli_run
|
||||
exec_args = {
|
||||
"nodes_filter": playbook["nodes"],
|
||||
"commands": playbook["commands"],
|
||||
"variables": playbook.get("variables"),
|
||||
"parallel": options.get("parallel", parallel),
|
||||
"timeout": playbook.get("timeout", options.get("timeout", 20)),
|
||||
"prompt": options.get("prompt"),
|
||||
"name": playbook.get("name", "Task")
|
||||
}
|
||||
|
||||
# Map 'output' field to folder path if it's not stdout/null
|
||||
output_cfg = playbook.get("output")
|
||||
if output_cfg not in [None, "stdout"]:
|
||||
exec_args["folder"] = output_cfg
|
||||
|
||||
if action == "run":
|
||||
return self.run_commands(**exec_args)
|
||||
elif action == "test":
|
||||
exec_args["expected"] = playbook.get("expected", [])
|
||||
return self.test_commands(**exec_args)
|
||||
else:
|
||||
raise ConnpyError(f"Unsupported playbook action: {action}")</code></pre>
|
||||
return self.run_commands(nodes_filter, commands, parallel=parallel)</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Business logic for executing commands on nodes and running automation scripts.</p>
|
||||
<p>Initialize the service.</p>
|
||||
@@ -1399,65 +1448,6 @@ el.replaceWith(d);
|
||||
</details>
|
||||
<div class="desc"><p>Execute commands on a set of nodes.</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ExecutionService.run_yaml_playbook"><code class="name flex">
|
||||
<span>def <span class="ident">run_yaml_playbook</span></span>(<span>self, playbook_data: str, parallel: int = 10) ‑> Dict[str, Any]</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def run_yaml_playbook(self, playbook_data: str, parallel: int = 10) -> Dict[str, Any]:
|
||||
"""Run a structured Connpy YAML automation playbook (from path or content)."""
|
||||
playbook = None
|
||||
if playbook_data.startswith("---YAML---\n"):
|
||||
try:
|
||||
content = playbook_data[len("---YAML---\n"):]
|
||||
playbook = yaml.load(content, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to parse YAML content: {e}")
|
||||
else:
|
||||
if not os.path.exists(playbook_data):
|
||||
raise ConnpyError(f"Playbook file not found: {playbook_data}")
|
||||
try:
|
||||
with open(playbook_data, "r") as f:
|
||||
playbook = yaml.load(f, Loader=yaml.FullLoader)
|
||||
except Exception as e:
|
||||
raise ConnpyError(f"Failed to load playbook {playbook_data}: {e}")
|
||||
|
||||
# Basic validation
|
||||
if not isinstance(playbook, dict) or "nodes" not in playbook or "commands" not in playbook:
|
||||
raise ConnpyError("Invalid playbook format: missing 'nodes' or 'commands' keys.")
|
||||
|
||||
action = playbook.get("action", "run")
|
||||
options = playbook.get("options", {})
|
||||
|
||||
# Extract all fields similar to RunHandler.cli_run
|
||||
exec_args = {
|
||||
"nodes_filter": playbook["nodes"],
|
||||
"commands": playbook["commands"],
|
||||
"variables": playbook.get("variables"),
|
||||
"parallel": options.get("parallel", parallel),
|
||||
"timeout": playbook.get("timeout", options.get("timeout", 20)),
|
||||
"prompt": options.get("prompt"),
|
||||
"name": playbook.get("name", "Task")
|
||||
}
|
||||
|
||||
# Map 'output' field to folder path if it's not stdout/null
|
||||
output_cfg = playbook.get("output")
|
||||
if output_cfg not in [None, "stdout"]:
|
||||
exec_args["folder"] = output_cfg
|
||||
|
||||
if action == "run":
|
||||
return self.run_commands(**exec_args)
|
||||
elif action == "test":
|
||||
exec_args["expected"] = playbook.get("expected", [])
|
||||
return self.test_commands(**exec_args)
|
||||
else:
|
||||
raise ConnpyError(f"Unsupported playbook action: {action}")</code></pre>
|
||||
</details>
|
||||
<div class="desc"><p>Run a structured Connpy YAML automation playbook (from path or content).</p></div>
|
||||
</dd>
|
||||
<dt id="connpy.services.ExecutionService.test_commands"><code class="name flex">
|
||||
<span>def <span class="ident">test_commands</span></span>(<span>self,<br>nodes_filter: str,<br>commands: List[str],<br>expected: List[str],<br>variables: Dict[str, Any] | None = None,<br>parallel: int = 10,<br>timeout: int = 20,<br>folder: str | None = None,<br>prompt: str | None = None,<br>on_node_complete: Callable | None = None,<br>logger: Callable | None = None,<br>name: str | None = None) ‑> Dict[str, Dict[str, bool]]</span>
|
||||
</code></dt>
|
||||
@@ -4006,9 +3996,11 @@ el.replaceWith(d);
|
||||
<h4><code><a title="connpy.services.AIService" href="#connpy.services.AIService">AIService</a></code></h4>
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.services.AIService.aask_copilot" href="#connpy.services.AIService.aask_copilot">aask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.analyze_execution_results" href="#connpy.services.AIService.analyze_execution_results">analyze_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.ask" href="#connpy.services.AIService.ask">ask</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.ask_copilot" href="#connpy.services.AIService.ask_copilot">ask_copilot</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.build_context_blocks" href="#connpy.services.AIService.build_context_blocks">build_context_blocks</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.build_playbook_chat" href="#connpy.services.AIService.build_playbook_chat">build_playbook_chat</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.configure_mcp" href="#connpy.services.AIService.configure_mcp">configure_mcp</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.configure_provider" href="#connpy.services.AIService.configure_provider">configure_provider</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.confirm" href="#connpy.services.AIService.confirm">confirm</a></code></li>
|
||||
@@ -4016,6 +4008,7 @@ el.replaceWith(d);
|
||||
<li><code><a title="connpy.services.AIService.list_mcp_servers" href="#connpy.services.AIService.list_mcp_servers">list_mcp_servers</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.list_sessions" href="#connpy.services.AIService.list_sessions">list_sessions</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.load_session_data" href="#connpy.services.AIService.load_session_data">load_session_data</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.predict_execution_results" href="#connpy.services.AIService.predict_execution_results">predict_execution_results</a></code></li>
|
||||
<li><code><a title="connpy.services.AIService.process_copilot_input" href="#connpy.services.AIService.process_copilot_input">process_copilot_input</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
@@ -4041,7 +4034,6 @@ el.replaceWith(d);
|
||||
<ul class="">
|
||||
<li><code><a title="connpy.services.ExecutionService.run_cli_script" href="#connpy.services.ExecutionService.run_cli_script">run_cli_script</a></code></li>
|
||||
<li><code><a title="connpy.services.ExecutionService.run_commands" href="#connpy.services.ExecutionService.run_commands">run_commands</a></code></li>
|
||||
<li><code><a title="connpy.services.ExecutionService.run_yaml_playbook" href="#connpy.services.ExecutionService.run_yaml_playbook">run_yaml_playbook</a></code></li>
|
||||
<li><code><a title="connpy.services.ExecutionService.test_commands" href="#connpy.services.ExecutionService.test_commands">test_commands</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user