|
| 1 | +# Copyright (c) Microsoft. All rights reserved. |
| 2 | + |
| 3 | +""" |
| 4 | +OSOP Workflow Reader for Semantic Kernel |
| 5 | +
|
| 6 | +Reads an OSOP (.osop.yaml) workflow definition and creates |
| 7 | +Semantic Kernel ChatCompletionAgents from the agent nodes. |
| 8 | +Demonstrates how portable OSOP workflow definitions can drive |
| 9 | +SK agent orchestration. |
| 10 | +
|
| 11 | +Usage: |
| 12 | + python osop_workflow_reader.py |
| 13 | +""" |
| 14 | + |
| 15 | +import asyncio |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | +import yaml |
| 19 | + |
| 20 | +from semantic_kernel import Kernel |
| 21 | +from semantic_kernel.agents import ChatCompletionAgent, ChatHistoryAgentThread |
| 22 | +from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion |
| 23 | + |
| 24 | +# OSOP node type → SK description |
| 25 | +NODE_TYPE_DESCRIPTIONS = { |
| 26 | + "agent": "AI agent (ChatCompletionAgent)", |
| 27 | + "api": "API call (native function)", |
| 28 | + "cli": "CLI command execution", |
| 29 | + "human": "Human review step", |
| 30 | + "system": "System/infrastructure", |
| 31 | + "db": "Database operation", |
| 32 | +} |
| 33 | + |
| 34 | + |
| 35 | +def load_osop_workflow(file_path: str) -> dict: |
| 36 | + """Load and parse an OSOP workflow YAML file.""" |
| 37 | + content = Path(file_path).read_text(encoding="utf-8") |
| 38 | + data = yaml.safe_load(content) |
| 39 | + if not isinstance(data, dict) or "nodes" not in data: |
| 40 | + raise ValueError("Invalid OSOP workflow: missing 'nodes'") |
| 41 | + return data |
| 42 | + |
| 43 | + |
| 44 | +def describe_workflow(workflow: dict) -> str: |
| 45 | + """Generate a human-readable description of an OSOP workflow.""" |
| 46 | + name = workflow.get("name", workflow.get("id", "Untitled")) |
| 47 | + nodes = workflow.get("nodes", []) |
| 48 | + edges = workflow.get("edges", []) |
| 49 | + lines = [f"Workflow: {name}", f"Nodes: {len(nodes)}, Edges: {len(edges)}", ""] |
| 50 | + for node in nodes: |
| 51 | + ntype = node.get("type", "system") |
| 52 | + nname = node.get("name", node.get("id", "?")) |
| 53 | + desc = NODE_TYPE_DESCRIPTIONS.get(ntype, ntype) |
| 54 | + lines.append(f" [{ntype}] {nname} — {desc}") |
| 55 | + return "\n".join(lines) |
| 56 | + |
| 57 | + |
| 58 | +async def create_agents_from_osop(workflow: dict, kernel: Kernel) -> list[ChatCompletionAgent]: |
| 59 | + """Create SK ChatCompletionAgents from OSOP agent nodes.""" |
| 60 | + agents = [] |
| 61 | + for node in workflow.get("nodes", []): |
| 62 | + if node.get("type") != "agent": |
| 63 | + continue |
| 64 | + name = node.get("name", node.get("id", "Agent")) |
| 65 | + purpose = node.get("purpose", node.get("description", "")) |
| 66 | + config = node.get("config", {}) |
| 67 | + system_prompt = config.get("system_prompt", f"You are {name}. {purpose}") |
| 68 | + |
| 69 | + agent = ChatCompletionAgent( |
| 70 | + kernel=kernel, |
| 71 | + name=name.replace(" ", "_"), |
| 72 | + instructions=system_prompt, |
| 73 | + ) |
| 74 | + agents.append(agent) |
| 75 | + print(f" Created agent: {name}") |
| 76 | + return agents |
| 77 | + |
| 78 | + |
| 79 | +async def main(): |
| 80 | + # Load the OSOP workflow |
| 81 | + osop_path = Path(__file__).parent / "code-review-pipeline.osop.yaml" |
| 82 | + if not osop_path.exists(): |
| 83 | + print(f"OSOP file not found: {osop_path}") |
| 84 | + return |
| 85 | + |
| 86 | + workflow = load_osop_workflow(str(osop_path)) |
| 87 | + print(describe_workflow(workflow)) |
| 88 | + print() |
| 89 | + |
| 90 | + # Create a kernel with OpenAI |
| 91 | + kernel = Kernel() |
| 92 | + kernel.add_service(OpenAIChatCompletion(service_id="default")) |
| 93 | + |
| 94 | + # Create agents from OSOP definition |
| 95 | + print("Creating agents from OSOP workflow:") |
| 96 | + agents = await create_agents_from_osop(workflow, kernel) |
| 97 | + print(f"\nCreated {len(agents)} agents from {len(workflow.get('nodes', []))} OSOP nodes") |
| 98 | + |
| 99 | + # Demonstrate: use the first agent |
| 100 | + if agents: |
| 101 | + thread = ChatHistoryAgentThread() |
| 102 | + print(f"\nInvoking agent: {agents[0].name}") |
| 103 | + response = await agents[0].get_response( |
| 104 | + messages="Describe a simple Python function that calculates factorial.", |
| 105 | + thread=thread, |
| 106 | + ) |
| 107 | + print(f"Response: {response.content[:200]}...") |
| 108 | + |
| 109 | + |
| 110 | +if __name__ == "__main__": |
| 111 | + asyncio.run(main()) |
0 commit comments