diff --git a/.github/agents/dev.agent.md b/.github/agents/dev.agent.md index 6cf2dd9e..3fd93188 100644 --- a/.github/agents/dev.agent.md +++ b/.github/agents/dev.agent.md @@ -1,6 +1,6 @@ --- description: Acts as a development stakeholder, being able to break down features into technical tasks and manage project guidelines and standards. -tools: ['runCommands', 'runTasks', 'edit', 'runNotebooks', 'search', 'new', 'context7/*', 'deepwiki/*', 'microsoft.docs.mcp/*', 'Azure MCP/azd', 'Azure MCP/cloudarchitect', 'Azure MCP/documentation', 'Azure MCP/extension_azqr', 'Azure MCP/extension_cli_generate', 'Azure MCP/extension_cli_install', 'Azure MCP/get_bestpractices', 'Azure MCP/search', 'copilotkit/*', 'extensions', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes', 'ms-azuretools.vscode-azure-github-copilot/azure_query_azure_resource_graph', 'ms-azuretools.vscode-azure-github-copilot/azure_get_auth_context', 'ms-azuretools.vscode-azure-github-copilot/azure_set_auth_context', 'ms-azuretools.vscode-azure-github-copilot/azure_get_dotnet_template_tags', 'ms-azuretools.vscode-azure-github-copilot/azure_get_dotnet_templates_for_tag', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_agent_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_ai_model_guidance', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_agent_model_code_sample', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_tracing_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_evaluation_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_evaluation_agent_runner_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_evaluation_planner', 'todos', 'runSubagent', 'runTests'] +tools: ['runCommands', 'runTasks', 'context7/*', 'deepwiki/*', 'github/*', 'microsoft.docs.mcp/*', 'Azure MCP/azd', 'Azure MCP/cloudarchitect', 'Azure MCP/documentation', 'Azure MCP/extension_azqr', 'Azure MCP/extension_cli_generate', 'Azure MCP/extension_cli_install', 'Azure MCP/get_bestpractices', 'edit', 'runNotebooks', 'search', 'new', 'extensions', 'ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes', 'ms-azuretools.vscode-azure-github-copilot/azure_query_azure_resource_graph', 'ms-azuretools.vscode-azure-github-copilot/azure_get_auth_context', 'ms-azuretools.vscode-azure-github-copilot/azure_set_auth_context', 'ms-azuretools.vscode-azure-github-copilot/azure_get_dotnet_template_tags', 'ms-azuretools.vscode-azure-github-copilot/azure_get_dotnet_templates_for_tag', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_agent_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_ai_model_guidance', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_agent_model_code_sample', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_tracing_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_get_evaluation_code_gen_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_evaluation_agent_runner_best_practices', 'ms-windows-ai-studio.windows-ai-studio/aitk_evaluation_planner', 'ms-windows-ai-studio.windows-ai-studio/aitk_open_tracing_page', 'todos', 'runTests', 'runSubagent', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo'] model: Claude Sonnet 4.5 (copilot) handoffs: - label: Implement Code for technical tasks (/implement) diff --git a/.github/agents/spec2cloud.agent.md b/.github/agents/spec2cloud.agent.md new file mode 100644 index 00000000..5ff64b35 --- /dev/null +++ b/.github/agents/spec2cloud.agent.md @@ -0,0 +1,578 @@ +--- +description: Main orchestration agent that analyzes user intent and delegates tasks to specialized agents for product management, architecture, planning, development, and Azure deployment. +tools: ['runSubagent', 'edit', 'search', 'new', 'runCommands', 'runTasks', 'usages', 'problems', 'changes', 'fetch', 'todos'] +model: Claude Sonnet 4.5 (copilot) +name: spec2cloud +--- + +# Orchestrator Agent Instructions + +You are the **Orchestrator Agent** - the primary point of contact for all user requests in this multi-agent development system. Your role is to understand user intent, determine the appropriate workflow, and delegate tasks to specialized agents using the `runSubagent` tool. + +## Core Responsibilities + +1. **Intent Analysis**: Understand what the user wants to accomplish +2. **Workflow Selection**: Determine the appropriate workflow and agent(s) to involve +3. **Task Delegation**: Delegate tasks to specialized agents via `runSubagent` +4. **Context Management**: Ensure agents have the necessary context and instructions +5. **Coordination**: Orchestrate multi-agent workflows when tasks span multiple domains +6. **Progress Reporting**: Keep users informed about which agents are working on their requests +7. **Result Synthesis**: Combine outputs from multiple agents into coherent responses + +## Available Specialized Agents + +### 1. **pm** (Product Manager) +**When to use**: +- Creating or updating Product Requirements Documents (PRD) +- Breaking down PRDs into Feature Requirements Documents (FRDs) +- Defining business requirements, user personas, success metrics +- Clarifying stakeholder needs and acceptance criteria + +**Capabilities**: +- Creates PRD in `specs/prd.md` +- Creates FRDs in `specs/features/*.md` +- Focuses on WHAT to build, not HOW to build it +- Defines success criteria and business goals + +**Intent keywords**: "requirements", "PRD", "feature spec", "business needs", "user story", "acceptance criteria", "product definition" + +### 2. **devlead** (Developer Lead) +**When to use**: +- Reviewing PRDs/FRDs for technical feasibility +- Identifying missing technical requirements +- Validating requirement completeness +- Ensuring alignment with technical standards + +**Capabilities**: +- Reviews and enhances PRDs/FRDs with technical requirements +- Validates feasibility against technology stack +- Identifies gaps in requirements +- Advocates for simplicity-first approach + +**Intent keywords**: "review requirements", "technical feasibility", "missing requirements", "validate PRD", "technical completeness" + +### 3. **architect** (Architect) +**When to use**: +- Creating Architecture Decision Records (ADRs) +- Making key architectural decisions +- Researching technology options +- Maintaining architecture guidelines and standards +- Generating AGENTS.md documentation + +**Capabilities**: +- Creates ADRs in `specs/adr/` +- Documents architectural decisions and rationale +- Synthesizes project guidelines +- Maintains architecture standards + +**Intent keywords**: "architecture decision", "ADR", "technology choice", "design decision", "architecture guidelines", "standards", "AGENTS.md" + +### 4. **planner** (Planner) +**When to use**: +- Creating comprehensive implementation plans +- Breaking down features into technical tasks +- Designing system architecture diagrams +- Planning without implementation + +**Capabilities**: +- Creates multi-level Mermaid diagrams (L0-L3) +- Breaks down work into steps and tasks +- Identifies dependencies and risks +- **DOES NOT implement** - planning only + +**Intent keywords**: "plan", "implementation plan", "task breakdown", "architecture diagram", "roadmap", "strategy" + +### 5. **dev** (Developer) +**When to use**: +- Implementing features and code changes +- Writing actual application code +- Managing project guidelines in `/standards/` +- Breaking down features into technical tasks + +**Capabilities**: +- Writes and edits code across the codebase +- Implements features based on specs and plans +- Maintains development standards +- Can delegate to other developers + +**Intent keywords**: "implement", "code", "build", "create feature", "fix bug", "write code", "develop" + +### 6. **azure** (Azure Deployment Specialist) +**When to use**: +- Deploying applications to Azure +- Creating infrastructure as code (Bicep) +- Setting up CI/CD pipelines +- Configuring Azure services + +**Capabilities**: +- Uses Azure Dev CLI (azd) for deployment +- Creates Bicep templates for infrastructure +- Generates GitHub Actions workflows +- Follows Azure best practices + +**Intent keywords**: "deploy", "Azure", "infrastructure", "Bicep", "CI/CD", "cloud", "provision" + +### 7. **tech-analyst** (Reverse Engineering Analyst) +**When to use**: +- Analyzing existing codebases +- Reverse engineering specifications from code +- Documenting existing systems +- Extracting feature documentation + +**Capabilities**: +- Analyzes code structure and architecture +- Creates feature documentation from existing code +- Generates technical documentation +- Identifies technology stack and dependencies + +**Intent keywords**: "analyze", "reverse engineer", "document existing", "understand codebase", "extract specs", "analyze code" + +### 8. **modernizer** (Modernization Strategist) +**When to use**: +- Creating modernization strategies for legacy systems +- Identifying technical debt and security issues +- Planning architecture improvements +- Generating modernization tasks + +**Capabilities**: +- Analyzes legacy systems for improvement opportunities +- Creates comprehensive modernization roadmaps +- Identifies security vulnerabilities and technical debt +- Generates actionable modernization tasks + +**Intent keywords**: "modernize", "upgrade", "refactor", "improve architecture", "technical debt", "migration", "legacy" + +## Orchestration Workflow + +### Step 1: Analyze User Intent +When a user makes a request, analyze: +- **What** they want to accomplish +- **Which domain** it falls into (product, architecture, planning, development, deployment, analysis) +- **Which agent(s)** are best suited to handle the request +- **What context** the agent(s) will need + +### Step 2: Determine Workflow Pattern + +#### A. Single-Agent Delegation (Simple Requests) +For straightforward requests that fit one agent's scope: +``` +User Request → Orchestrator analyzes → Delegates to appropriate agent → Returns result +``` + +**Examples**: +- "Create a PRD for a task management app" → Delegate to **pm** +- "Deploy this app to Azure" → Delegate to **azure** +- "Implement user authentication" → Delegate to **dev** + +#### B. Sequential Multi-Agent Workflow (Complex Requests) +For requests requiring multiple agents in sequence: +``` +User Request → Agent 1 (foundational work) → Agent 2 (builds on Agent 1) → Agent 3 (final step) +``` + +**Common Sequences**: +1. **New Feature Development**: + - pm → devlead → architect → planner → dev + +2. **Deployment Pipeline**: + - architect → azure → dev (validation) + +3. **Legacy System Modernization**: + - tech-analyst → modernizer → planner → dev + +#### C. Parallel Multi-Agent Workflow (Independent Tasks) +For requests with independent sub-tasks: +``` +User Request → [Agent A + Agent B + Agent C] (parallel) → Combine results +``` + +**Examples**: +- Documentation generation across multiple domains +- Simultaneous infrastructure and code updates + +### Step 3: Delegate with Clear Instructions +When delegating to an agent via `runSubagent`: + +1. **Provide complete context**: Include relevant information from the user's request +2. **Be specific**: Give clear, actionable instructions +3. **Set expectations**: Explain what output you need back +4. **Include constraints**: Mention any limitations or requirements + +**Good delegation example**: +``` +description: "Create PRD for task management app" +prompt: "Create a Product Requirements Document for a task management application. The user wants to build a web app where teams can create, assign, and track tasks. Focus on core task management features: task creation, assignment, status tracking, and basic collaboration. Save the PRD in specs/prd.md." +``` + +**Bad delegation example**: +``` +description: "Help with tasks" +prompt: "Do something about tasks" +``` + +### Step 4: Handle Agent Responses +- **Review the output** from the agent +- **Extract key information** relevant to the user +- **Determine if additional agents** are needed +- **Synthesize the results** into a coherent response for the user + +### Step 5: Report Back to User +Provide a clear summary: +- **What was done**: Which agent(s) worked on the request +- **What was created**: Files, documents, or changes made +- **Next steps**: What should happen next (if applicable) +- **Options**: Present choices if multiple paths are available + +## Intent Classification Examples + +### Product & Requirements Intent +**User says**: "I want to build an e-commerce app with shopping cart and checkout" +**Classification**: Product requirements definition +**Delegate to**: `pm` agent +**Instruction**: "Create a PRD for an e-commerce application with shopping cart and checkout features. Define user personas, success metrics, and acceptance criteria." + +### Architecture Intent +**User says**: "Should we use Cosmos DB or SQL Database for this project?" +**Classification**: Architecture decision +**Delegate to**: `architect` agent +**Instruction**: "Create an ADR comparing Cosmos DB and Azure SQL Database for [specific project context]. Research both options, evaluate trade-offs, and provide a recommendation." + +### Planning Intent +**User says**: "Create an implementation plan for the authentication feature" +**Classification**: Implementation planning +**Delegate to**: `planner` agent +**Instruction**: "Create a comprehensive implementation plan for the authentication feature defined in specs/features/authentication.md. Include Mermaid diagrams (L0-L3) and a task breakdown." + +### Development Intent +**User says**: "Implement the login page using Next.js" +**Classification**: Code implementation +**Delegate to**: `dev` agent +**Instruction**: "Implement a login page using Next.js based on the authentication FRD in specs/features/authentication.md. Follow the project's frontend guidelines in AGENTS.md." + +### Deployment Intent +**User says**: "Deploy this application to Azure using best practices" +**Classification**: Azure deployment +**Delegate to**: `azure` agent +**Instruction**: "Analyze the codebase and deploy it to Azure following best practices. Use Azure Dev CLI (azd) and create Bicep templates for infrastructure as code." + +### Analysis Intent +**User says**: "Analyze this existing codebase and document the features" +**Classification**: Reverse engineering +**Delegate to**: `tech-analyst` agent +**Instruction**: "Analyze the existing codebase and create feature documentation in specs/features/. Extract the architecture, technology stack, and business logic." + +### Modernization Intent +**User says**: "How can we modernize this legacy application?" +**Classification**: Modernization strategy +**Delegate to**: `modernizer` agent +**Instruction**: "Analyze the legacy application and create a comprehensive modernization strategy. Identify technical debt, security issues, and architecture improvements. Create actionable tasks in specs/tasks/." + +### Browse/List Intent +**User says**: "Show me available agents" or "What prompts are available?" +**Classification**: Resource catalog browsing +**Action**: Display the resource catalog tables, then ask which items to fetch +**Response**: Show numbered list of agents/prompts with descriptions, prompt user to select by number + +### Fetch Intent +**User says**: "Fetch all agents from the spec2cloud repo" +**Classification**: Agent/prompt synchronization +**Action**: Use `fetch` tool to download from `https://raw.githubusercontent.com/EmeaAppGbb/spec2cloud/main/.github/agents/` and save to local `.github/agents/` +**Response**: "I've fetched 9 agent files from the spec2cloud repository to .github/agents/" + +## Multi-Agent Orchestration Patterns + +### Pattern 1: New Feature End-to-End +**User Request**: "Build a user authentication feature" + +**Orchestration**: +1. Delegate to **pm**: "Create an FRD for user authentication feature with email/password login, registration, and password reset." +2. Delegate to **devlead**: "Review the authentication FRD for technical completeness and feasibility." +3. Delegate to **architect**: "Create an ADR for authentication approach (consider OAuth, session management, token strategy)." +4. Delegate to **planner**: "Create an implementation plan for authentication based on the FRD and ADRs." +5. Delegate to **dev**: "Implement the authentication feature according to the plan." + +**Report to user**: "I've orchestrated the complete authentication feature workflow across 5 specialized agents: PM created the requirements, Dev Lead validated them, Architect made key decisions, Planner created the implementation plan, and Developer implemented the code. The feature is now ready." + +### Pattern 2: Azure Deployment +**User Request**: "Deploy to Azure with CI/CD" + +**Orchestration**: +1. Delegate to **architect**: "Review the deployment architecture and create an ADR for Azure service selection." +2. Delegate to **azure**: "Deploy the application to Azure using Azure Dev CLI. Create Bicep templates and GitHub Actions workflows." +3. Delegate to **dev**: "Verify the deployment and ensure the application works correctly in Azure." + +### Pattern 3: Legacy Modernization +**User Request**: "Modernize this old application" + +**Orchestration**: +1. Delegate to **tech-analyst**: "Analyze the existing codebase and document all features, architecture, and technology stack." +2. Delegate to **modernizer**: "Create a modernization strategy based on the analysis. Identify technical debt, security issues, and improvement opportunities." +3. Delegate to **planner**: "Create a phased implementation plan for the modernization." +4. Delegate to **dev**: "Begin implementing the highest priority modernization tasks." + +## Decision Tree for Agent Selection + +``` +User Request + │ + ├─ Mentions "list agents", "show agents", "browse", "what's available", "catalog"? + │ └─ YES → Show resource catalog, let user pick items to fetch + │ + ├─ Mentions "fetch agents", "fetch prompts", "sync agents", "download agents"? + │ └─ YES → Execute fetch workflow (see "Spec2Cloud Resource Catalog" section) + │ + ├─ Mentions "requirements", "PRD", "feature spec"? + │ └─ YES → pm agent + │ + ├─ Mentions "review", "feasibility", "complete"? + │ └─ YES → devlead agent + │ + ├─ Mentions "architecture", "ADR", "technology choice"? + │ └─ YES → architect agent + │ + ├─ Mentions "plan", "roadmap", "diagram"? + │ └─ YES → planner agent + │ + ├─ Mentions "implement", "code", "build", "create"? + │ └─ YES → dev agent + │ + ├─ Mentions "deploy", "Azure", "infrastructure", "CI/CD"? + │ └─ YES → azure agent + │ + ├─ Mentions "analyze", "document existing", "reverse engineer"? + │ └─ YES → tech-analyst agent + │ + ├─ Mentions "modernize", "upgrade", "migrate", "refactor"? + │ └─ YES → modernizer agent + │ + └─ Ambiguous or complex? + └─ Ask clarifying questions OR make best judgment +``` + +## Important Rules + +### DO: +✅ Always analyze user intent before delegating +✅ Provide clear, specific instructions to agents +✅ Include necessary context in delegation prompts +✅ Coordinate multi-agent workflows when needed +✅ Synthesize results from multiple agents +✅ Keep users informed about progress +✅ Ask clarifying questions when intent is unclear +✅ Use the most appropriate agent for each task + +### DON'T: +❌ Implement code yourself - delegate to **dev** agent +❌ Create infrastructure yourself - delegate to **azure** agent +❌ Write PRDs yourself - delegate to **pm** agent +❌ Skip analysis - always understand intent first +❌ Delegate without clear instructions +❌ Assume user intent - ask questions if unclear +❌ Use multiple agents when one will suffice +❌ Forget to report back to the user + +## Example Interactions + +### Example 1: Simple Delegation +**User**: "Create a PRD for a blogging platform" + +**Orchestrator Response**: +"I'll delegate this to the Product Manager agent to create a comprehensive PRD. + +[Delegates to pm agent with instructions] + +The PM agent has created a PRD for the blogging platform in `specs/prd.md`. The document includes user personas, core features (post creation, editing, publishing, comments), success metrics, and acceptance criteria. Would you like me to have the Dev Lead review it for technical completeness?" + +### Example 2: Multi-Agent Workflow +**User**: "Build a payment processing feature for my e-commerce app" + +**Orchestrator Response**: +"I'll coordinate a multi-agent workflow for this payment feature: + +1. First, I'll have the PM define the requirements... +[Delegates to pm] + +2. Next, the Dev Lead will review for technical completeness... +[Delegates to devlead] + +3. The Architect will make key technical decisions about payment providers and security... +[Delegates to architect] + +4. The Planner will create an implementation roadmap... +[Delegates to planner] + +5. Finally, the Developer will implement the feature... +[Delegates to dev] + +The payment processing feature has been fully implemented with PCI compliance considerations, secure payment gateway integration, and comprehensive error handling. All documentation is in `specs/features/payment-processing.md` and the code is ready for testing." + +### Example 3: Clarification Needed +**User**: "Make it better" + +**Orchestrator Response**: +"I'd be happy to help improve the project! To route your request to the right specialist, could you clarify: + +- Are you looking to improve the **requirements/specifications** (PM agent)? +- Do you want to improve the **architecture or technical decisions** (Architect agent)? +- Are you interested in **code quality or implementation** (Developer agent)? +- Would you like to improve the **Azure infrastructure or deployment** (Azure agent)? +- Or are you looking to **modernize/refactor existing code** (Modernizer agent)? + +Please provide more details about what aspect you'd like to improve." + +## Spec2Cloud Resource Catalog + +You can browse and fetch Copilot agents and prompts from the spec2cloud repository at `https://github.com/EmeaAppGbb/spec2cloud`. + +### Intent Keywords +- **List/Browse**: "list agents", "show agents", "show prompts", "list available", "what agents are available", "browse spec2cloud", "show catalog" +- **Fetch**: "fetch agents", "fetch prompts", "download agents", "sync agents", "get spec2cloud agents", "install agents" + +--- + +### Available Resources Catalog + +When a user asks to **list**, **show**, or **browse** available resources, display this catalog: + +#### 📦 AGENTS (`.github/agents/`) + +| # | Agent | File | Description | +|---|-------|------|-------------| +| 1 | **architect** | `architect.agent.md` | Creates Architecture Decision Records (ADRs), makes technology choices, maintains architecture guidelines | +| 2 | **azure** | `azure.agent.md` | Azure deployment specialist - uses Azure Dev CLI, creates Bicep templates, sets up CI/CD pipelines | +| 3 | **dev** | `dev.agent.md` | Developer agent for implementing features, writing code, managing project standards | +| 4 | **devlead** | `devlead.agent.md` | Reviews PRDs/FRDs for technical feasibility, validates completeness, identifies missing requirements | +| 5 | **modernizer** | `modernizer.agent.md` | Analyzes legacy systems, creates modernization strategies, identifies technical debt and security issues | +| 6 | **planner** | `planner.agent.md` | Creates implementation plans with Mermaid diagrams (L0-L3), breaks down work into tasks | +| 7 | **pm** | `pm.agent.md` | Product Manager - creates PRDs and FRDs, defines requirements, user personas, success metrics | +| 8 | **spec2cloud** | `spec2cloud.agent.md` | Main orchestrator agent that coordinates all other agents | +| 9 | **tech-analyst** | `tech-analyst.agent.md` | Reverse engineers existing codebases, extracts specifications, creates technical documentation | + +#### 📝 PROMPTS (`.github/prompts/`) + +| # | Prompt | File | Description | +|---|--------|------|-------------| +| 1 | **adr** | `adr.prompt.md` | Template for creating Architecture Decision Records | +| 2 | **delegate** | `delegate.prompt.md` | Template for delegating tasks between agents | +| 3 | **deploy** | `deploy.prompt.md` | Deployment workflow and Azure deployment guidance | +| 4 | **frd** | `frd.prompt.md` | Feature Requirements Document template | +| 5 | **generate-agents** | `generate-agents.prompt.md` | Template for generating new agent definitions | +| 6 | **implement** | `implement.prompt.md` | Implementation guidance and coding standards | +| 7 | **modernize** | `modernize.prompt.md` | Comprehensive modernization strategy template | +| 8 | **plan** | `plan.prompt.md` | Planning template with diagram guidance | +| 9 | **prd** | `prd.prompt.md` | Product Requirements Document template | +| 10 | **rev-eng** | `rev-eng.prompt.md` | Reverse engineering and code analysis template | + +--- + +### Interactive Selection Workflow + +When user asks to list or browse resources: + +**Step 1: Show the catalog** (display the tables above) + +**Step 2: Ask user to select** +Present options like: +- "Which agents would you like to fetch? (Enter numbers like 1,3,5 or 'all' or 'none')" +- "Which prompts would you like to fetch? (Enter numbers like 1,4,7 or 'all' or 'none')" + +**Step 3: Confirm selection** +- Summarize what will be fetched +- Ask for confirmation before proceeding + +**Step 4: Fetch selected items** +- Download only the selected files +- Report which files were successfully fetched + +### Example Interactions + +#### Example 1: Browse and Select +**User**: "Show me available agents" + +**Response**: +``` +Here are the available agents from the spec2cloud repository: + +| # | Agent | Description | +|---|-------|-------------| +| 1 | architect | Creates ADRs, makes technology choices | +| 2 | azure | Azure deployment specialist | +| 3 | dev | Developer for implementation | +| 4 | devlead | Reviews requirements for technical feasibility | +| 5 | modernizer | Legacy modernization strategist | +| 6 | planner | Creates implementation plans | +| 7 | pm | Product Manager for PRDs/FRDs | +| 8 | spec2cloud | Main orchestrator agent | +| 9 | tech-analyst | Reverse engineering analyst | + +Which agents would you like to fetch? +Enter numbers (e.g., "1,3,5"), "all", or "none": +``` + +**User**: "1, 2, 7" + +**Response**: +``` +You've selected: +- architect.agent.md +- azure.agent.md +- pm.agent.md + +Shall I fetch these 3 agents to .github/agents/? (yes/no) +``` + +#### Example 2: Quick List +**User**: "What prompts are available?" + +**Response**: Display the prompts table, then ask which ones to fetch. + +#### Example 3: Fetch Specific by Name +**User**: "Fetch the azure agent and deploy prompt" + +**Action**: Fetch those specific files without showing the full catalog. + +--- + +### How to Fetch + +1. **Use the `fetch` tool** to download from raw GitHub URLs: + - Agents: `https://raw.githubusercontent.com/EmeaAppGbb/spec2cloud/main/.github/agents/{filename}` + - Prompts: `https://raw.githubusercontent.com/EmeaAppGbb/spec2cloud/main/.github/prompts/{filename}` + +2. **Save files to the local workspace**: + - Agents → `.github/agents/` in current project + - Prompts → `.github/prompts/` in current project + +3. **Create directories** if they don't exist + +4. **Report results** to user with list of fetched files + +### Quick Commands + +Users can also use direct commands: +- `"fetch all agents"` - Downloads all 9 agents +- `"fetch all prompts"` - Downloads all 10 prompts +- `"fetch everything from spec2cloud"` - Downloads all agents and prompts +- `"fetch agent 1,3,5"` - Downloads agents by number +- `"fetch the pm agent"` - Downloads specific agent by name + +## Continuous Improvement + +As the orchestrator, you should: +- **Learn from patterns**: Recognize common request types and optimize routing +- **Improve delegation**: Refine how you pass context to agents +- **Enhance coordination**: Get better at multi-agent workflows +- **Provide better feedback**: Help users understand the agent ecosystem + +## Summary + +You are the **central coordinator** of a specialized multi-agent system. Your job is to: +1. **Understand** what the user wants +2. **Identify** which agent(s) can help +3. **Delegate** tasks with clear instructions using `runSubagent` +4. **Coordinate** multi-agent workflows when needed +5. **Report** back to the user with synthesized results + +Think of yourself as a project manager who knows each team member's expertise and knows exactly who to assign tasks to for the best results. + +```` diff --git a/.github/prompts/adr.prompt.md b/.github/prompts/adr.prompt.md index 8a7f0009..d4fc3317 100644 --- a/.github/prompts/adr.prompt.md +++ b/.github/prompts/adr.prompt.md @@ -1,3 +1,4 @@ +```prompt --- agent: architect --- diff --git a/.github/skills/human-in-the-loop/SKILL.md b/.github/skills/human-in-the-loop/SKILL.md new file mode 100644 index 00000000..c32fb741 --- /dev/null +++ b/.github/skills/human-in-the-loop/SKILL.md @@ -0,0 +1,146 @@ +--- +name: human-in-the-loop +description: Implement human-in-the-loop approval patterns where agents request user approval before proceeding. Use when adding approval workflows, user confirmations, or HITL features. +--- + +# Implementing Human-in-the-Loop Approval + +**HITL** allows agents to request user approval before proceeding. Requires backend-frontend coordination. + +## Backend: Create Approval Requests + +### 1. Use ApprovalRequestHelper + +Located at `src/agentic-api/ApprovalRequestHelper.cs`: + +```csharp +public static class ApprovalRequestHelper +{ + public static FunctionApprovalRequestContent CreateApprovalRequest( + string functionName, + Dictionary arguments) + { + return new FunctionApprovalRequestContent( + Guid.NewGuid().ToString(), + new FunctionCallContent(functionName, functionName, arguments: arguments) + ); + } +} +``` + +### 2. Return Approval Requests from Executors + +Instead of returning a direct response, return an approval request: + +```csharp +public override async ValueTask HandleAsync(UserInputEvent input, IWorkflowContext context, CancellationToken ct) +{ + var responseText = await GenerateContent(input); + + // Return approval request instead of direct response + return ApprovalRequestHelper.CreateApprovalRequest( + functionName: "approve_copyright_command", // Must match frontend hook name + arguments: new Dictionary { { "copyright", responseText } } + ); +} +``` + +### 3. Handle Approval Responses in Input Executor + +```csharp +private async ValueTask HandleChatMessagesAsync(List messages, IWorkflowContext context, CancellationToken ct) +{ + // Check for approval response (comes back as ChatRole.Tool with FunctionResultContent) + var approvalMessage = messages.LastOrDefault(m => m.Role == ChatRole.Tool); + var functionResult = approvalMessage?.Contents.OfType().FirstOrDefault(); + + var textApproved = functionResult?.Result?.ToString()?.Contains("text-approved"); + var textRejected = functionResult?.Result?.ToString()?.Contains("text-rejected"); + + // Route based on approval status + if (textApproved == true) return new UserInputEvent { NextStep = WorkflowSteps.GenerateImage }; + if (textRejected == true) return new UserInputEvent { NextStep = WorkflowSteps.RegenerateText }; + return new UserInputEvent { NextStep = WorkflowSteps.GenerateText }; // Start workflow +} +``` + +**Note**: `AGUIWorkflowAgent` automatically converts `FunctionApprovalRequestContent` to `FunctionCallContent` for the frontend (auto-registered via `.AddAsAIAgent()`). + +## Frontend: Add useHumanInTheLoop Hook + +```typescript +import { useHumanInTheLoop } from "@copilotkit/react-core"; + +export default function Page() { + const [approvedContent, setApprovedContent] = useState(null); + + useHumanInTheLoop({ + name: "approve_copyright_command", // Must match backend functionName + description: "Ask the user to approve the generated text content", + parameters: [ + { name: "copyright", type: "string", description: "The text to approve", required: true }, + ], + render: ({ args, respond }) => { + if (!respond) return <>; + return ( +
+
{args.copyright}
+ + +
+ ); + }, + }); + // ... +} +``` + +## Key Matching Requirements + +| Frontend | Backend | Must Match | +|----------|---------|------------| +| `name` | `functionName` | Exact string match | +| `parameters[].name` | `arguments` dictionary keys | Key names | +| `respond()` value | `FunctionResultContent.Result` check | Response strings | + +## Workflow with Conditional Routing + +```csharp +public Workflow BuildWorkflow(string name) +{ + var chatInput = new DummyChatInputExecutor(_inputLogger); + var textGenerator = new TextGeneratorExecutor(_logger, _chatClient); + var imageGenerator = new ImageGeneratorExecutor(_logger, _chatClient, _imageGenerator); + + return new WorkflowBuilder(chatInput) + .WithName(name) + .AddSwitch(chatInput, switchBuilder => + switchBuilder + .AddCase(input => input?.NextStep == WorkflowSteps.GenerateText, textGenerator) + .AddCase(input => input?.NextStep == WorkflowSteps.GenerateImage, imageGenerator) + .WithDefault(textGenerator)) + .WithOutputFrom(textGenerator) + .WithOutputFrom(imageGenerator) + .Build(); +} +``` + +## Execution Flow + +``` +User → InputExecutor (checks approvals) → Executor → Returns FunctionApprovalRequestContent + → Frontend shows approval UI → User responds → InputExecutor routes to next step +``` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Approval UI not appearing | Check `functionName` matches exactly, executor registered with `.WithOutputFrom()` | +| Response not reaching backend | Ensure `respond()` called with string, check `ChatRole.Tool` messages in input executor | + +## Best Practices + +- Use descriptive names: `approve_copyright_command`, `validate_data_command` +- Use clear response values: `"text-approved"`, `"image-rejected"` +- Always provide both approve and reject options diff --git a/.github/skills/image-generation/SKILL.md b/.github/skills/image-generation/SKILL.md new file mode 100644 index 00000000..2531a823 --- /dev/null +++ b/.github/skills/image-generation/SKILL.md @@ -0,0 +1,88 @@ +--- +name: image-generation +description: Add text-to-image generation using IImageGenerator and Azure AI Foundry models (Flux, GPT-Image, DALL-E). Use when implementing image generation features in workflows. +--- + +# Using IImageGenerator for Text-to-Image Generation + +**IImageGenerator** provides text-to-image generation using Azure AI Foundry models (Flux, GPT-Image, DALL-E, etc.). + +## 1. Register in Program.cs + +The deployment name is auto-populated by `azd provision`: + +```csharp +string imageDeploymentName = builder.Configuration["AZURE_IMAGE_MODEL_DEPLOYMENT_NAME"] + ?? throw new InvalidOperationException("AZURE_IMAGE_MODEL_DEPLOYMENT_NAME is not set."); + +#pragma warning disable MEAI001 +builder.Services.AddSingleton(_ => + new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + .GetImageClient(imageDeploymentName) + .AsIImageGenerator()); +#pragma warning restore MEAI001 +``` + +## 2. Inject and Use in Executors + +```csharp +public class MyExecutor(ILogger logger, IChatClient chatClient, IImageGenerator imageGenerator) + : Executor("MyExecutor") +{ + public override async ValueTask HandleAsync(InputEvent input, IWorkflowContext context, CancellationToken ct) + { + var options = new ImageGenerationOptions + { + MediaType = "image/png", + ResponseFormat = ImageGenerationResponseFormat.Hosted, // or .Base64 + Size = "1024x1024", // Model-dependent + Quality = "standard", // or "hd" + Style = "natural" // or "vivid" + }; + + var response = await imageGenerator.GenerateImagesAsync("A futuristic city at sunset", options, ct); + var dataContent = response.Contents.OfType().First(); + + return new OutputEvent { Text = "Image generated!", ImageUrl = dataContent.Uri?.ToString() }; + } +} +``` + +## 3. Send to UI + +Use `YieldOutputAsync` or return with `ImageUrl` property: + +```csharp +await context.YieldOutputAsync(new AgentMessage { Text = "Here's your image:", ImageUrl = dataContent.Uri.ToString() }); +``` + +## 4. Frontend Handling + +In `CustomMessageRenderer.tsx`: + +```typescript +export function CustomMessageRenderer({ message }: { message: { text?: string; imageUrl?: string } }) { + return ( +
+ {message.text &&

{message.text}

} + {message.imageUrl && Generated by AI} +
+ ); +} +``` + +## Best Practices + +- Wrap in try-catch for error handling +- Log prompts and URLs for debugging +- Use `Hosted` response format for UI display +- Image generation typically takes 10-30 seconds + +## Image Generation Options + +| Option | Values | Notes | +|--------|--------|-------| +| `ResponseFormat` | `Hosted`, `Base64` | Use `Hosted` for UI | +| `Size` | `1024x1024`, `512x512`, etc. | Model-dependent | +| `Quality` | `standard`, `hd` | HD takes longer | +| `Style` | `natural`, `vivid` | Artistic style | diff --git a/.github/skills/new-workflow/SKILL.md b/.github/skills/new-workflow/SKILL.md new file mode 100644 index 00000000..bce80c67 --- /dev/null +++ b/.github/skills/new-workflow/SKILL.md @@ -0,0 +1,127 @@ +--- +name: new-workflow +description: Create a new agent workflow with executors, registration, and AGUI protocol integration. Use when adding new AI agent capabilities, creating executors, or building workflow graphs. +--- + +# Adding a New Agent Workflow + +Follow these steps to create a new workflow in the agentic-shell-dotnet project. + +## 1. Create Workflow File + +Create a new file at `src/agentic-api/Workflows/MyWorkflow.cs`: + +```csharp +using Microsoft.Agents.AI.Workflows; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; + +namespace agentic_api.Workflows; + +// Input Executor: Receives chat input, converts to internal event +public class MyChatInputExecutor(ILogger logger) + : ExecutorBase(logger) +{ + protected override ValueTask ExecuteAsync(IConversationUpdate input, CancellationToken ct) + { + var userMessage = input switch + { + ChatMessage msg => msg.Text, + TurnToken token => token.Text, + _ => "Hello" + }; + return ValueTask.FromResult(new ExecutionResult(new UserInputEvent { Input = userMessage })); + } +} + +// Processing Executor: Handles business logic, calls AI models +public class MyProcessingExecutor(ILogger logger, IChatClient chatClient) + : ExecutorBase(logger) +{ + protected override async ValueTask ExecuteAsync(UserInputEvent input, CancellationToken ct) + { + var response = await chatClient.CompleteAsync($"User message: {input.Input}", cancellationToken: ct); + return new ExecutionResult(new WorkflowOutputEvent(response.Message.Text ?? "Hello!")); + } +} + +// Factory: Builds the workflow graph +public class MyWorkflowFactory( + ILogger inputLogger, + ILogger processingLogger, + IChatClient chatClient) +{ + public Workflow BuildWorkflow(string name) + { + var inputExecutor = new MyChatInputExecutor(inputLogger); + var processingExecutor = new MyProcessingExecutor(processingLogger, chatClient); + + return new WorkflowBuilder(inputExecutor) + .WithName(name) + .AddEdge(inputExecutor, processingExecutor) + .WithOutputFrom(processingExecutor) // Required for streaming to UI + .Build(); + } +} +``` + +## 2. Register in Program.cs + +Add the workflow registration in `src/agentic-api/Program.cs`: + +```csharp +builder.Services.AddSingleton(); +builder.AddWorkflow("MyWorkflow", (sp, name) => + sp.GetRequiredService().BuildWorkflow(name)) + .AddAsAIAgent(); // Wraps with AGUIWorkflowAgent for AGUI protocol compatibility +``` + +## What `.AddAsAIAgent()` Does + +- Wraps workflow with `AGUIWorkflowAgent` for AGUI protocol +- Registers agent with AGUI endpoint (via `app.MapAGUI()`) +- Makes workflow accessible via `/api/copilotkit` + +## Workflow Patterns + +### Basic Linear Workflow +``` +User Input → InputExecutor → ProcessingExecutor → Response +``` + +### Conditional Routing with Switch +```csharp +return new WorkflowBuilder(chatInput) + .WithName(name) + .AddSwitch(chatInput, switchBuilder => + switchBuilder + .AddCase(input => input?.NextStep == WorkflowSteps.StepA, executorA) + .AddCase(input => input?.NextStep == WorkflowSteps.StepB, executorB) + .WithDefault(executorA)) + .WithOutputFrom(executorA) + .WithOutputFrom(executorB) + .Build(); +``` + +## Streaming Messages to UI + +Use `YieldOutputAsync` to stream intermediate results: + +```csharp +await context.YieldOutputAsync(new AgentMessage { Text = "Processing..." }); +``` + +**Critical**: Every executor calling `YieldOutputAsync` must be registered with `.WithOutputFrom()`: + +```csharp +var workflow = new WorkflowBuilder(inputExecutor) + .AddEdge(inputExecutor, processingExecutor) + .WithOutputFrom(processingExecutor) // Required! + .Build(); +``` + +## Key Files Reference + +- `src/agentic-api/Program.cs` - Backend config and workflow registration +- `src/agentic-api/Workflows/DummyWorkflow.cs` - Example workflow +- `src/agentic-api/AGUIWorkflowAgent.cs` - AGUI adapter diff --git a/.github/skills/project-commands/SKILL.md b/.github/skills/project-commands/SKILL.md new file mode 100644 index 00000000..cf208b62 --- /dev/null +++ b/.github/skills/project-commands/SKILL.md @@ -0,0 +1,85 @@ +--- +name: project-commands +description: Run common project commands for setup, local development, building, deploying, and testing. Use when setting up the project, running locally, building, deploying to Azure, or running tests. +--- + +# Project Commands + +Common commands for working with the agentic-shell-dotnet project. + +## Setup + +Authenticate and provision Azure resources: + +```bash +az login && azd auth login && azd provision +``` + +This command: +1. Logs into Azure CLI +2. Logs into Azure Developer CLI +3. Provisions all required Azure resources (AI services, Container Apps, etc.) + +## Run Locally + +Start the application with .NET Aspire orchestration: + +```bash +aspire run +``` + +This starts all services with proper environment injection: +- **Aspire Dashboard**: http://localhost:15888 +- **UI (Next.js)**: http://localhost:3000 +- **API (ASP.NET Core)**: http://localhost:5149 + +**Note**: Always use `aspire run` for local development to ensure environment variables are properly injected. + +## Build + +Build all components: + +```bash +./build.sh +``` + +## Deploy to Azure + +Deploy the application to Azure Container Apps: + +```bash +azd deploy +``` + +## Run Tests + +### Backend Tests (.NET) + +```bash +dotnet test tests/agentic-api-tests/agentic-api-tests.csproj +``` + +### Frontend Tests (Next.js) + +```bash +cd src/agentic-ui && npm test +``` + +## Quick Reference + +| Task | Command | +|------|---------| +| Setup | `az login && azd auth login && azd provision` | +| Run locally | `aspire run` | +| Build all | `./build.sh` | +| Deploy | `azd deploy` | +| Backend tests | `dotnet test tests/agentic-api-tests/agentic-api-tests.csproj` | +| Frontend tests | `cd src/agentic-ui && npm test` | + +## Local Development URLs + +| Service | URL | +|---------|-----| +| Aspire Dashboard | http://localhost:15888 | +| Frontend UI | http://localhost:3000 | +| Backend API | http://localhost:5149 | diff --git a/.vscode/launch.json b/.vscode/launch.json index 94e0ef19..5d70b4e0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,8 @@ "type": "aspire", "request": "launch", "name": "Aspire: Launch default apphost", - "program": "${workspaceFolder}" + "program": "${workspaceFolder}", + "preLaunchTask": "build-all" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..7b8954d1 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build-backend", + "type": "shell", + "command": "dotnet", + "args": [ + "build", + "${workspaceFolder}/src/agentic-api/agentic-api.csproj" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "build-frontend", + "type": "shell", + "command": "npm", + "args": [ + "run", + "build" + ], + "options": { + "cwd": "${workspaceFolder}/src/agentic-ui" + }, + "problemMatcher": [], + "group": "build" + }, + { + "label": "build-all", + "dependsOn": [ + "build-backend", + "build-frontend" + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index 543b8c9b..ac3364b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,1362 +1,134 @@ # AGENTS.md -> **AI Agent Instructions for agentic-shell-dotnet** -> This document provides comprehensive, prescriptive guidance for AI coding agents working on this project. -> Human developers should refer to [README.md](README.md) and `/specs/docs/` for general documentation. +> **AI Agent Instructions for agentic-shell-dotnet** +> Human developers should refer to [README.md](README.md) and `/specs/docs/` for documentation. --- ## Project Overview -**agentic-shell-dotnet** is a microservices-based AI agent application demonstrating the Microsoft Agent Framework. The application provides an intelligent chat interface powered by Azure OpenAI, featuring: +**agentic-shell-dotnet** is a microservices-based AI agent application using Microsoft Agent Framework. -- **Frontend**: Next.js 16 + React 19 + TypeScript + CopilotKit -- **Backend**: ASP.NET Core 10 + Microsoft Agent Framework -- **Orchestration**: .NET Aspire for local development -- **Deployment**: Azure Container Apps with Azure AI services -- **Status**: Prototype/Demo (not production-ready) +| Component | Stack | +|-----------|-------| +| **Frontend** | Next.js 16 + React 19 + TypeScript + CopilotKit | +| **Backend** | ASP.NET Core 10 + Microsoft Agent Framework | +| **Orchestration** | .NET Aspire | +| **Deployment** | Azure Container Apps + Azure AI services | +| **Status** | Prototype/Demo (not production-ready) | -**Key Characteristics:** -- Workflow-based agent architecture using Microsoft Agent Framework -- Preview/beta software stack (70% of dependencies) -- Clean microservices separation with AGUI protocol -- Infrastructure as Code with Azure Bicep -- No authentication, testing, or production hardening yet implemented +**Key files**: `src/agentic-api/Program.cs` (backend), `src/agentic-api/Workflows/DummyWorkflow.cs` (demo workflow), `src/agentic-ui/app/page.tsx` (frontend), `apphost.cs` (Aspire orchestration) --- -## Architecture Quick Reference +## Technology Stack (Critical Versions) -### System Components +**Backend**: .NET 10.0, `Microsoft.Agents.AI.*` 1.0.0-preview.251125.1, `Azure.AI.OpenAI` 2.5.0-beta.1 +**Frontend**: Next.js 16.0.3, React 19.2.0, `@copilotkit/react-*` ^1.10.6, `@ag-ui/client` ^0.0.41 -``` -┌─────────────┐ -│ Browser │ -└──────┬──────┘ - │ HTTPS - ▼ -┌─────────────────────────────┐ -│ agentic-ui (Frontend) │ -│ Next.js 16 + React 19 │ -│ Port: 3000 │ -└──────────┬──────────────────┘ - │ HTTP - │ /api/copilotkit - ▼ -┌─────────────────────────────┐ -│ agentic-api (Backend) │ -│ ASP.NET Core 10 + Agents │ -│ Port: 8080 (5149 local) │ -└──────────┬──────────────────┘ - │ SDK calls - ▼ -┌─────────────────────────────┐ -│ Azure AI Services │ -│ - Azure OpenAI (GPT-5 Mini)│ -│ - Azure AI Foundry │ -│ - Cosmos DB (provisioned) │ -│ - AI Search (provisioned) │ -└─────────────────────────────┘ -``` - -### Repository Structure - -``` -/ -├── src/ -│ ├── agentic-api/ # Backend API service (.NET 10) -│ │ ├── Program.cs # Application startup -│ │ ├── AGUIWorkflowAgent.cs # AGUI protocol adapter -│ │ └── Workflows/ # Agent workflow implementations -│ │ └── DummyWorkflow.cs # Demo workflow -│ └── agentic-ui/ # Frontend web app (Next.js 16) -│ ├── app/ -│ │ ├── page.tsx # Main landing page with chat -│ │ └── api/copilotkit/ # Backend connection endpoint -│ └── package.json -├── tests/ -│ └── agentic-api-tests/ # Backend unit tests (xUnit) -├── infra/ # Infrastructure as Code (Bicep) -│ ├── main.bicep # Main deployment template -│ ├── resources.bicep # Azure resources definition -│ └── modules/ # Reusable Bicep modules -├── specs/ # Product specifications -│ ├── docs/ # Reverse engineering documentation -│ │ ├── architecture/ # Architecture documentation -│ │ ├── technology/ # Technology stack analysis -│ │ └── infrastructure/ # Deployment documentation -│ └── features/ # Feature specifications -│ ├── ai-chat-interface.md -│ └── marketing-campaign-workflow.md -├── apphost.cs # .NET Aspire orchestration -├── azure.yaml # Azure Developer CLI config -└── AGENTS.md # This file -``` - ---- - -## Setup Commands - -### Prerequisites - -**Required:** -- **.NET 10.0 SDK** - Install from [dotnet.microsoft.com](https://dotnet.microsoft.com/download) -- **Node.js 20.x LTS** - Install from [nodejs.org](https://nodejs.org/) -- **Azure CLI** - Install from [learn.microsoft.com/cli/azure/install-azure-cli](https://learn.microsoft.com/cli/azure/install-azure-cli) -- **Azure Developer CLI (azd)** - Install from [learn.microsoft.com/azure/developer/azure-developer-cli/install-azd](https://learn.microsoft.com/azure/developer/azure-developer-cli/install-azd) - -**Optional (for development):** -- **Docker Desktop** - For dev container support -- **Visual Studio Code** - Recommended IDE -- **.NET Aspire workload** - Run: `dotnet workload install aspire` - -### Initial Setup - -```bash -# 1. Authenticate to Azure -az login -azd auth login - -# 2. Provision Azure resources (one-time setup) -azd provision -# Follow prompts to select subscription and location -# This will automatically populate apphost.settings.json with Azure OpenAI configuration -``` - -### Local Development - -```bash -# Run with .NET Aspire (required for proper environment variable injection) -aspire run -# Opens Aspire dashboard at http://localhost:15888 -# Frontend available at http://localhost:3000 -# Backend available at http://localhost:5149 -``` - -### Build Commands - -```bash -# Build all services (recommended) -./build.sh - -# Or build services individually: - -# Backend build -cd src/agentic-api -dotnet restore -dotnet build - -# Frontend build -cd src/agentic-ui -npm install -npm run build - -# Build Docker images locally -cd src/agentic-api -docker build -t agentic-api:latest . - -cd src/agentic-ui -docker build -t agentic-ui:latest . -``` - -### Deployment - -```bash -# Deploy to Azure (builds + pushes + deploys) -azd deploy - -# Deploy specific service -azd deploy agentic-api -azd deploy agentic-ui - -# Full provision + deploy -azd up -``` - -### Testing Commands - -**Backend - xUnit testing project (located in tests/agentic-api-tests):** - -```bash -# Run backend tests -cd tests/agentic-api-tests -dotnet test - -# Run with detailed output -dotnet test --verbosity detailed - -# Run with code coverage -dotnet test --collect:"XPlat Code Coverage" - -# Run from project root -dotnet test tests/agentic-api-tests/agentic-api-tests.csproj -``` - -**Frontend - Tests configured (Jest + React Testing Library):** - -```bash -cd src/agentic-ui -npm test # Run tests in watch mode -npm run test:ci # Run tests once (CI mode) -npm run test:coverage # Run with coverage report -``` - ---- - -## Technology Stack & Versions - -### Backend (.NET) - -**Critical: All backend dependencies use EXACT versions. Do not change versions without testing.** - -| Package | Version | Status | Notes | -|---------|---------|--------|-------| -| `.NET SDK` | `10.0` | Stable | Target framework: net10.0 | -| `Microsoft.Agents.AI.Workflows` | `1.0.0-preview.251125.1` | ⚠️ Preview | Core workflow orchestration | -| `Microsoft.Agents.AI.Hosting` | `1.0.0-preview.251125.1` | ⚠️ Preview | Agent hosting infrastructure | -| `Microsoft.Agents.AI.Hosting.AGUI.AspNetCore` | `1.0.0-preview.251125.1` | ⚠️ Preview | AGUI protocol integration | -| `Microsoft.Agents.AI.Hosting.OpenAI` | `1.0.0-alpha.251125.1` | ⚠️ Alpha | OpenAI-specific hosting | -| `Azure.AI.OpenAI` | `2.5.0-beta.1` | ⚠️ Beta | Azure OpenAI SDK | -| `Azure.Identity` | `1.18.0-beta.1` | ⚠️ Beta | Authentication | -| `Aspire.Azure.AI.Inference` | `13.0.0-preview.1.25560.3` | ⚠️ Preview | Aspire AI integration | - -### Frontend (Node.js) - -**Critical: Package versions use caret ranges (^). Lock file ensures deterministic builds.** - -| Package | Version | Status | Notes | -|---------|---------|--------|-------| -| `next` | `16.0.3` | Stable | React framework with SSR | -| `react` | `19.2.0` | Stable | Core React library | -| `react-dom` | `19.2.0` | Stable | React DOM rendering | -| `@copilotkit/react-core` | `^1.10.6` | Stable | Core CopilotKit integration | -| `@copilotkit/react-ui` | `^1.10.6` | Stable | UI components | -| `@ag-ui/client` | `^0.0.41` | ⚠️ Pre-1.0 | Microsoft Agent Framework client | -| `tailwindcss` | `^4` | Stable | Utility-first CSS | -| `typescript` | `^5` | Stable | Type safety | - -### Infrastructure - -- **Orchestration**: .NET Aspire 13.0.0 -- **IaC**: Azure Bicep (latest) -- **Cloud Platform**: Azure (Container Apps, OpenAI, AI Foundry, Cosmos DB, AI Search) - ---- - -## Development Workflow - -### Adding a New Agent Workflow - -**Example: Create a "GreetingWorkflow"** - -1. **Create workflow file**: `src/agentic-api/Workflows/GreetingWorkflow.cs` - -```csharp -using Microsoft.Agents.AI.Workflows; -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; - -namespace agentic_api.Workflows; - -// Executor 1: Handle chat input -public class GreetingChatInputExecutor(ILogger logger) - : ExecutorBase(logger) -{ - protected override ValueTask ExecuteAsync( - IConversationUpdate input, - CancellationToken cancellationToken) - { - var userMessage = input switch - { - ChatMessage msg => msg.Text, - TurnToken token => token.Text, - _ => "Hello" - }; - - Logger.LogInformation("User said: {Message}", userMessage); - - var userEvent = new UserInputEvent - { - Input = userMessage - }; - - return ValueTask.FromResult(new ExecutionResult(userEvent)); - } -} - -// Executor 2: Generate greeting response -public class GreetingGeneratorExecutor( - ILogger logger, - IChatClient chatClient) - : ExecutorBase(logger) -{ - protected override async ValueTask ExecuteAsync( - UserInputEvent input, - CancellationToken cancellationToken) - { - var prompt = $""" - You are a friendly AI assistant. Greet the user warmly. - User message: {input.Input} - """; - - var response = await chatClient.CompleteAsync( - prompt, - cancellationToken: cancellationToken); - - Logger.LogInformation("AI response: {Response}", response.Message.Text); - - var output = new WorkflowOutputEvent(response.Message.Text ?? "Hello!"); - return new ExecutionResult(output); - } -} - -// Factory to build the workflow -public class GreetingWorkflowFactory( - ILogger inputLogger, - ILogger generatorLogger, - IChatClient chatClient) -{ - public Workflow BuildWorkflow(string name) - { - var inputExecutor = new GreetingChatInputExecutor(inputLogger); - var generatorExecutor = new GreetingGeneratorExecutor(generatorLogger, chatClient); - - return new WorkflowBuilder(inputExecutor) - .WithName(name) - .AddEdge(inputExecutor, generatorExecutor) - .WithOutputFrom(generatorExecutor) - .Build(); - } -} -``` - -2. **Register workflow in `Program.cs`** - This registers the workflow with the AGUI endpoint as an agent: - -```csharp -// Add factory registration -builder.Services.AddSingleton(); - -// Register workflow as AI agent -// This makes the workflow accessible via the AGUI endpoint (/agui) -builder.AddWorkflow("GreetingWorkflow", (sp, name) => - sp.GetRequiredService().BuildWorkflow(name)) -.AddAsAIAgent(); // Wraps workflow with AGUIWorkflowAgent for protocol compatibility -``` - -**Important**: The `.AddAsAIAgent()` extension method: -- Wraps your workflow with `AGUIWorkflowAgent` for AGUI protocol compatibility -- Registers the agent with the AGUI endpoint (configured via `app.MapAGUI()` in Program.cs) -- Makes the workflow accessible to the frontend via `/api/copilotkit` - -3. **Test locally**: Run `aspire run` and interact via chat UI. - -### Adding a Frontend Component - -**Example: Add a custom chat header** - -1. **Create component**: `src/agentic-ui/app/components/ChatHeader.tsx` - -```typescript -export function ChatHeader() { - return ( -
-

AI Assistant

-

Powered by Azure OpenAI

-
- ); -} -``` - -2. **Use in page**: `src/agentic-ui/app/page.tsx` - -```typescript -import { ChatHeader } from "./components/ChatHeader"; - -export default function Home() { - return ( - - - - {/* existing content */} - - - ); -} -``` - -3. **Test**: Run `npm run dev` in `src/agentic-ui/` - -### Modifying Infrastructure - -**Example: Add Azure Cache for Redis** - -1. **Add module to `infra/resources.bicep`**: - -```bicep -module redis 'br/public:avm/res/cache/redis:0.1.0' = { - name: 'redis-${resourceToken}' - params: { - name: 'redis-${resourceToken}' - location: location - sku: { - name: 'Basic' - family: 'C' - capacity: 0 - } - } -} -``` - -2. **Add connection string output**: - -```bicep -output redisConnectionString string = redis.outputs.connectionString -``` - -3. **Update Container App environment variables** in `main.bicep`: - -```bicep -{ - name: 'REDIS_CONNECTION_STRING' - value: resources.outputs.redisConnectionString -} -``` - -4. **Deploy**: Run `azd deploy` +**Do not change versions without testing** - 70% of dependencies are preview/beta. --- ## Code Style & Conventions ### Backend (.NET) - -**File naming**: PascalCase for classes, folders, and files -- ✅ `DummyWorkflow.cs`, `AGUIWorkflowAgent.cs` -- ❌ `dummyWorkflow.cs`, `agui-workflow-agent.cs` - -**Code style**: -- **Nullable reference types**: Enabled (required) -- **Implicit usings**: Enabled -- **Target framework**: `net10.0` -- **Naming**: PascalCase for public members, camelCase with underscore prefix for private fields -- **Async**: Always use `async`/`await`, never `.Result` or `.Wait()` -- **DI**: Constructor injection for all dependencies -- **Logging**: Use `ILogger` for structured logging - -**Example pattern**: - -```csharp -public class MyService(ILogger logger, IChatClient chatClient) -{ - private readonly ILogger _logger = logger; - private readonly IChatClient _chatClient = chatClient; - - public async Task ProcessAsync(string input, CancellationToken cancellationToken) - { - _logger.LogInformation("Processing input: {Input}", input); - - try - { - var result = await _chatClient.CompleteAsync(input, cancellationToken: cancellationToken); - return result.Message.Text ?? string.Empty; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error processing input"); - throw; - } - } -} -``` +- **File naming**: PascalCase (`DummyWorkflow.cs`, `AGUIWorkflowAgent.cs`) +- **Nullable reference types**: Enabled | **Target**: `net10.0` +- **Naming**: PascalCase public, `_camelCase` private fields +- **Async**: Always `async`/`await`, never `.Result` or `.Wait()` +- **DI**: Constructor injection only | **Logging**: `ILogger` ### Frontend (TypeScript/React) - -**File naming**: kebab-case for files, PascalCase for React components -- ✅ `chat-header.tsx`, `user-profile.tsx` -- Component: `export function ChatHeader() {}` - -**Code style**: -- **TypeScript**: Strict mode enabled -- **React 19**: Use hooks, functional components only -- **JSX**: Use `.tsx` extension -- **Props**: Always type props explicitly -- **State**: Use `useState`, `useContext`, `useReducer` -- **Effects**: Minimize `useEffect`, prefer derived state -- **Styling**: Tailwind CSS utility classes - -**Example pattern**: - -```typescript -interface ChatMessageProps { - message: string; - sender: 'user' | 'ai'; - timestamp: Date; -} - -export function ChatMessage({ message, sender, timestamp }: ChatMessageProps) { - return ( -
-

- {timestamp.toLocaleTimeString()} -

-

{message}

-
- ); -} -``` +- **File naming**: kebab-case (`chat-header.tsx`) | **Components**: PascalCase +- **TypeScript**: Strict mode | **React 19**: Functional components + hooks only +- **Props**: Always type explicitly | **Styling**: Tailwind CSS ### Bicep (Infrastructure) - -**File naming**: kebab-case -- ✅ `main.bicep`, `ai-project.bicep`, `fetch-container-image.bicep` - -**Code style**: -- **Naming**: Use descriptive resource names with `${resourceToken}` suffix -- **Modules**: Use Azure Verified Modules (`br/public:avm/...`) when available -- **Outputs**: Expose all connection strings and endpoints as outputs -- **Parameters**: Use `@description` decorator for all parameters - -**Example pattern**: - -```bicep -@description('The location for all resources') -param location string = resourceGroup().location - -@description('Unique token for resource naming') -param resourceToken string - -module appServicePlan 'br/public:avm/res/web/serverfarm:0.1.0' = { - name: 'plan-${resourceToken}' - params: { - name: 'plan-${resourceToken}' - location: location - sku: { - name: 'B1' - tier: 'Basic' - } - } -} - -output appServicePlanId string = appServicePlan.outputs.resourceId -``` +- **File naming**: kebab-case (`main.bicep`, `ai-project.bicep`) +- Use Azure Verified Modules (`br/public:avm/...`) when available +- Use `@description` decorator for parameters --- ## Environment Variables -### Backend (agentic-api) - -**Required for Azure OpenAI:** +**Backend** (auto-populated by `azd provision`): ```bash AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5-mini +AZURE_IMAGE_MODEL_DEPLOYMENT_NAME= +# Auth: az login OR AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET ``` -**Required for local development authentication:** -```bash -# Use one of these authentication methods -AZURE_TENANT_ID= -AZURE_CLIENT_ID= -AZURE_CLIENT_SECRET= - -# OR authenticate via Azure CLI -az login -``` - -**Optional (provisioned but unused):** -```bash -AZURE_COSMOS_ENDPOINT=https://YOUR-COSMOS.documents.azure.com:443/ -AZURE_AI_SEARCH_ENDPOINT=https://YOUR-SEARCH.search.windows.net -AZURE_AI_PROJECT_ENDPOINT=https://YOUR-PROJECT.cognitiveservices.azure.com/ -``` - -**Production (Azure Container Apps):** -- `AZURE_CLIENT_ID` - Automatically set by managed identity -- `APPLICATIONINSIGHTS_CONNECTION_STRING` - Automatically set - -### Frontend (agentic-ui) - -**Required:** -```bash -AGENT_API_URL=http://localhost:5149 # Local development -AGENT_API_URL=https://your-api.azurecontainerapps.io # Production -``` - -**Optional:** -```bash -PORT=3000 # Default Next.js port -NODE_ENV=development # or production -``` - -### Configuration Files +**Frontend**: `AGENT_API_URL=http://localhost:5149` (local) or Container Apps URL (prod) -**Local development configuration:** -- Create `apphost.settings.json` (gitignored) with: - -```json -{ - "openAiEndpoint": "https://YOUR-RESOURCE.openai.azure.com/", - "openAiDeployment": "gpt-5-mini" -} -``` - -**Do NOT commit:** -- `apphost.settings.json` (gitignored) -- Any file containing Azure endpoints, keys, or secrets -- `.env` or `.env.local` files - ---- - -## Testing Instructions - -### Backend Testing Setup (xUnit) - -**Testing project location**: `tests/agentic-api-tests/` - -**Current state**: Backend testing framework is established with xUnit. - -**To add new tests:** - -```bash -# Navigate to test project -cd tests/agentic-api-tests - -# Add additional testing packages as needed -dotnet add package FluentAssertions -dotnet add package Moq -dotnet add package Testcontainers - -# Create test file: WorkflowTests.cs -``` - -**Example test pattern**: - -```csharp -public class DummyWorkflowTests -{ - private readonly Mock _mockChatClient; - private readonly Mock> _mockLogger; - - public DummyWorkflowTests() - { - _mockChatClient = new Mock(); - _mockLogger = new Mock>(); - } - - [Fact] - public async Task Execute_WithValidInput_ReturnsGreeting() - { - // Arrange - var executor = new GreetingExecutor(_mockLogger.Object, _mockChatClient.Object); - var input = new UserInputEvent { Input = "Hello" }; - - _mockChatClient - .Setup(x => x.CompleteAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(new ChatCompletion(new ChatMessage(ChatRole.Assistant, "Hi there!"))); - - // Act - var result = await executor.ExecuteAsync(input, CancellationToken.None); - - // Assert - result.Should().NotBeNull(); - result.Output.Text.Should().Contain("Hi there!"); - } -} -``` - -**Run tests**: -```bash -cd tests/agentic-api-tests -dotnet test -dotnet test --collect:"XPlat Code Coverage" # With coverage -``` - -### Frontend Testing Setup (Jest + React Testing Library) - -```bash -cd src/agentic-ui - -# Install testing dependencies -npm install --save-dev @testing-library/react @testing-library/jest-dom jest-environment-jsdom - -# Create jest.config.js -``` - -**jest.config.js**: - -```javascript -const nextJest = require('next/jest') - -const createJestConfig = nextJest({ - dir: './', -}) - -const customJestConfig = { - setupFilesAfterEnv: ['/jest.setup.js'], - testEnvironment: 'jest-environment-jsdom', - moduleNameMapper: { - '^@/(.*)$': '/$1', - }, -} - -module.exports = createJestConfig(customJestConfig) -``` - -**Example test pattern** (`__tests__/ChatHeader.test.tsx`): - -```typescript -import { render, screen } from '@testing-library/react'; -import { ChatHeader } from '@/app/components/ChatHeader'; - -describe('ChatHeader', () => { - it('renders the title', () => { - render(); - expect(screen.getByText('AI Assistant')).toBeInTheDocument(); - }); - - it('renders the subtitle', () => { - render(); - expect(screen.getByText('Powered by Azure OpenAI')).toBeInTheDocument(); - }); -}); -``` - -**Run tests**: -```bash -npm test # Watch mode -npm run test:ci # Run once -npm run test:coverage # With coverage -``` - -### Integration Testing - -**Not yet implemented. Required setup:** - -1. **Backend integration tests**: Test agent workflows end-to-end with test doubles for Azure services -2. **Frontend integration tests**: Test API integration with mock backend -3. **E2E tests**: Use Playwright to test full user flows - ---- - -## Debugging - -### Backend Debugging (VS Code) - -**Launch configuration** (`.vscode/launch.json`): - -```json -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Backend API", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/src/agentic-api/bin/Debug/net10.0/agentic-api.dll", - "args": [], - "cwd": "${workspaceFolder}/src/agentic-api", - "stopAtEntry": false, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development", - "AZURE_OPENAI_ENDPOINT": "https://YOUR-RESOURCE.openai.azure.com/", - "AZURE_OPENAI_DEPLOYMENT_NAME": "gpt-5-mini" - } - } - ] -} -``` - -**Set breakpoints** in executors, Program.cs, or workflow code. - -### Frontend Debugging (VS Code) - -**Launch configuration**: - -```json -{ - "name": "Debug Frontend (Chrome)", - "type": "chrome", - "request": "launch", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}/src/agentic-ui", - "sourceMapPathOverrides": { - "webpack://_N_E/*": "${webRoot}/*" - } -} -``` - -**Debug in browser**: Use Chrome/Edge DevTools, set breakpoints in TypeScript source. - -### Common Issues - -**Issue**: `Azure.Identity.AuthenticationFailedException: DefaultAzureCredential failed to retrieve a token` - -**Solution**: -```bash -# Authenticate via Azure CLI -az login - -# Verify authentication -az account show - -# Set environment variables (or use apphost.settings.json) -export AZURE_OPENAI_ENDPOINT=https://YOUR-RESOURCE.openai.azure.com/ -export AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5-mini -``` - -**Issue**: Frontend shows "Failed to connect to agent" - -**Solution**: -1. Verify backend is running: `curl http://localhost:5149/health` -2. Check `AGENT_API_URL` in frontend `.env.local` -3. Verify CORS configuration in `Program.cs` - -**Issue**: `Microsoft.Agents.AI` namespace not found - -**Solution**: -```bash -# Ensure .NET 10.0 SDK is installed -dotnet --version # Should show 10.0.x - -# Restore packages -cd src/agentic-api -dotnet restore -``` +**Do NOT commit**: `apphost.settings.json`, `.env`, `.env.local`, any secrets --- ## Architecture Patterns ### Workflow Executor Pattern - -**Every agent workflow follows this pattern:** - -1. **Input Executor**: Receives external input (ChatMessage/TurnToken), converts to internal event -2. **Processing Executor(s)**: Handle business logic, call AI models, process data -3. **Output Executor**: Returns results to the framework - -**Flow**: -``` -User Input → ChatInputExecutor → [ProcessingExecutors...] → OutputExecutor → Response -``` - -### Streaming Messages to the UI - -**When building workflows with Microsoft Agent Framework, to stream messages back to the UI:** - -**1. Emit events from executors using `YieldOutputAsync`:** - -At any point where you want to send intermediate results or status updates to the UI, call: - -```csharp -await context.YieldOutputAsync(new AgentMessage -{ - Text = "Your message content here", - // Map additional properties as needed -}); -``` - -This is particularly useful for: -- Multi-step workflows where each executor produces visible output -- Iterative processes (e.g., one agent creates an artifact, another reviews and approves it) -- Progress updates during long-running operations - -**Example:** -```csharp -public class ReviewExecutor : ExecutorBase -{ - protected override async ValueTask ExecuteAsync( - ArtifactEvent input, - CancellationToken cancellationToken) - { - // Stream status to UI - await context.YieldOutputAsync(new AgentMessage - { - Text = "Reviewing artifact..." - }); - - var review = await _reviewService.ReviewAsync(input.Artifact); - - // Stream result to UI - await context.YieldOutputAsync(new AgentMessage - { - Text = $"Review complete: {review.Status}" - }); - - return new ExecutionResult(new ReviewEvent { Review = review }); - } -} ``` - -**2. Register executors as output sources in the workflow builder:** - -When building the workflow, mark any executor that calls `YieldOutputAsync` as an output source: - -```csharp -var workflow = new WorkflowBuilder(inputExecutor) - .AddEdge(inputExecutor, processingExecutor) - .WithOutputFrom(processingExecutor) // Register as output source - .AddEdge(processingExecutor, reviewExecutor) - .WithOutputFrom(reviewExecutor) // Register as output source - .Build(); +User Input → InputExecutor → [ProcessingExecutors...] → OutputExecutor → Response ``` -**Important**: Every executor that calls `YieldOutputAsync` must be registered with `.WithOutputFrom()`, otherwise its messages won't be streamed to the UI. +- **InputExecutor**: Receives user input, converts to internal events +- **ProcessingExecutor**: Business logic, AI model calls +- **OutputExecutor**: Formats response for UI -**Example**: -```csharp -// 1. Input Executor -public class MyInputExecutor : ExecutorBase -{ - protected override ValueTask ExecuteAsync(IConversationUpdate input, ...) - { - var event = new MyInputEvent { Data = ExtractData(input) }; - return ValueTask.FromResult(new ExecutionResult(event)); - } -} +### AGUI Protocol Adapter +`AGUIWorkflowAgent` wraps workflows for AGUI compatibility. Auto-registered via `.AddAsAIAgent()`. -// 2. Processing Executor -public class MyProcessingExecutor : ExecutorBase -{ - protected override async ValueTask ExecuteAsync(MyInputEvent input, ...) - { - var result = await ProcessData(input.Data); - return new ExecutionResult(new MyOutputEvent { Result = result }); - } -} +### Dependency Injection +**Always use constructor injection**, never service locator pattern. -// 3. Build Workflow -var workflow = new WorkflowBuilder(inputExecutor) - .AddEdge(inputExecutor, processingExecutor) - .WithOutputFrom(processingExecutor) - .Build(); -``` - -### AGUI Protocol Adapter Pattern - -**AGUIWorkflowAgent** wraps workflows to make them compatible with AGUI protocol. - -**Pattern**: -```csharp -public class AGUIWorkflowAgent : DelegatingAIAgent -{ - public AGUIWorkflowAgent(AIAgent innerAgent) : base(innerAgent) { } - - public override IAsyncEnumerable RunStreamingAsync(...) - { - // Intercept workflow events - // Convert to AGUI-compatible responses - // Stream back to frontend - } -} -``` - -**Usage**: -```csharp -builder.AddWorkflow("MyWorkflow", factoryMethod) - .AddAsAIAgent(); // Automatically wraps with AGUIWorkflowAgent -``` - -### Dependency Injection Pattern - -**All services use constructor injection:** - -```csharp -// ❌ DON'T: Service locator anti-pattern -public class MyService -{ - public void DoWork() - { - var logger = ServiceProvider.GetService(); // WRONG - } -} - -// ✅ DO: Constructor injection -public class MyService(ILogger logger, IChatClient chatClient) -{ - private readonly ILogger _logger = logger; - private readonly IChatClient _chatClient = chatClient; - - public async Task DoWork() - { - _logger.LogInformation("Working..."); - await _chatClient.CompleteAsync("..."); - } -} -``` +### Streaming to UI +Use `YieldOutputAsync` for intermediate results. Executors using this must register with `.WithOutputFrom()`. --- -## Observability & Monitoring +## Observability -### Local Development - .NET Aspire Dashboard - -**Primary tool for local debugging and monitoring:** - -When running `aspire run`, the Aspire dashboard opens automatically at `http://localhost:15888` - -**Dashboard features:** -- **Resources view**: See all running services (agentic-api, agentic-ui) with health status -- **Console logs**: Real-time logs from both backend and frontend -- **Structured logs**: Filter by log level, timestamp, service, and search text -- **Traces**: Distributed tracing across services and dependencies -- **Metrics**: Request counts, duration, and custom metrics - -**Accessing logs:** -```bash -# Start the application -aspire run - -# Dashboard opens at http://localhost:15888 -# Navigate to: -# - "Resources" tab to see service status -# - "Console" tab for real-time logs -# - "Structured" tab for filterable logs -# - "Traces" tab for request tracing -# - "Metrics" tab for performance data -``` - -**Adding structured logging:** - -```csharp -public class MyExecutor(ILogger logger) -{ - public async Task ExecuteAsync(...) - { - // Structured logging automatically appears in Aspire dashboard - logger.LogInformation("Processing request for user {UserId}", userId); - - try - { - await DoWork(); - logger.LogInformation("Request completed in {Duration}ms", sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - logger.LogError(ex, "Error processing request"); - throw; - } - } -} -``` - -**Adding custom metrics:** - -```csharp -using System.Diagnostics.Metrics; - -public class MyExecutor -{ - private static readonly Meter _meter = new("AgenticApi.Workflows"); - private static readonly Counter _requestCounter = _meter.CreateCounter("workflow_requests"); - private static readonly Histogram _duration = _meter.CreateHistogram("workflow_duration_ms"); - - public async Task ExecuteAsync(...) - { - _requestCounter.Add(1); - var sw = Stopwatch.StartNew(); - - try - { - await DoWork(); - _duration.Record(sw.ElapsedMilliseconds); - } - catch (Exception ex) - { - logger.LogError(ex, "Error processing request"); - throw; - } - } -} -``` +**Local**: Aspire Dashboard at `http://localhost:15888` (auto-opens with `aspire run`) +- Resources, Console logs, Structured logs, Traces, Metrics -### Production - Application Insights - -**For Azure deployments:** -- Connection string automatically injected into Container Apps -- Automatic request/dependency tracking -- Console logs forwarded to Application Insights - -**Access production logs:** -```bash -# Azure Portal -az monitor app-insights query \ - --app \ - --analytics-query "traces | order by timestamp desc | take 100" -``` +**Production**: Application Insights (auto-injected connection string) --- -## Documentation Standards - -### Required Documentation Updates - -**When adding features, UPDATE these files:** - -1. **`/specs/features/.md`** - Feature specification -2. **`/specs/docs/architecture/overview.md`** - If architecture changes -3. **`/specs/adr/NNNN-.md`** - Create new MADR for significant decisions -4. **`README.md`** - If setup or usage changes -5. **`AGENTS.md`** - This file (if agent instructions change) - -### MADR (Architecture Decision Record) Template - -**Location**: `/specs/adr/0001-decision-title.md` - -```markdown -# [Number]. [Title] - -Date: YYYY-MM-DD - -## Status - -Accepted | Proposed | Deprecated | Superseded by [NNNN-new-decision.md] - -## Context and Problem Statement - -[Describe the context and problem statement] - -## Decision Drivers - -* [driver 1] -* [driver 2] - -## Considered Options - -* [option 1] -* [option 2] -* [option 3] - -## Decision Outcome - -Chosen option: "[option 1]", because [justification]. - -### Consequences - -**Positive:** -* [improvement or benefit] - -**Negative:** -* [compromising quality attribute or trade-off] -``` - -### Documentation Commands - -```bash -# Serve documentation locally (requires MkDocs) -mkdocs serve - -# Build documentation -mkdocs build - -# Deploy to GitHub Pages -mkdocs gh-deploy -``` - ---- - -## Dependency Management - -### Version Pinning Strategy - -**Backend**: Use EXACT versions (no wildcards) -```xml - -``` - -**Frontend**: Use caret ranges, rely on lock file -```json -"@copilotkit/react-core": "^1.10.6" -``` - -### Update Strategy (Not Yet Implemented) - -**Required: Add Dependabot configuration** - -Create `.github/dependabot.yml`: - -```yaml -version: 2 -updates: - - package-ecosystem: "nuget" - directory: "/src/agentic-api" - schedule: - interval: "weekly" - groups: - microsoft-agents: - patterns: - - "Microsoft.Agents.*" - - - package-ecosystem: "npm" - directory: "/src/agentic-ui" - schedule: - interval: "weekly" - groups: - copilotkit: - patterns: - - "@copilotkit/*" - ag-ui: - patterns: - - "@ag-ui/*" -``` - ---- - -## CI/CD Pipeline - -### Setup CI/CD with Azure Developer CLI - -**Configure CI/CD pipeline:** - -```bash -# Configure CI/CD pipeline (interactive) -azd pipeline config - -# Follow prompts to: -# 1. Select provider (GitHub Actions or Azure DevOps) -# 2. Authenticate to provider -# 3. Configure deployment credentials -# 4. Set up pipeline configuration - -# This will automatically create: -# - .github/workflows/azure-dev.yml (for GitHub Actions) -# - Required secrets in your repository -# - Service principal for Azure authentication -``` - -**What gets created:** - -- **CI/CD workflow file**: Automatically generated with build, test, and deploy jobs -- **Azure credentials**: Service principal created and stored as repository secret -- **Deployment automation**: Triggered on push to main branch - -**Verify pipeline:** - -```bash -# Check pipeline status -azd pipeline config --show -``` - ---- - -## Known Limitations & Technical Debt - -### Critical Gaps - -1. ✅ **No User Authentication** - Public endpoints, no identity management -2. ⚠️ **Limited Test Coverage** - Backend testing framework established, needs test implementation -3. ✅ **No Input Validation** - Prompt injection and abuse possible -4. ✅ **No Rate Limiting** - Cost overruns and abuse possible -5. ✅ **No Error Handling** - Minimal try-catch coverage -6. ✅ **No Monitoring Dashboards** - Basic telemetry only - -### Architectural Limitations - -1. **Single Agent Only** - Only "DummyWorkflow" implemented -2. **No Data Persistence** - Cosmos DB provisioned but unused -3. **No RAG Capabilities** - AI Search provisioned but unused -4. **Stateless Design** - No conversation history or context retention - -### Technical Debt - -- **Unused Azure Resources**: Cosmos DB and AI Search provisioned but not used (~$75/month cost) -- **No SBOM Generation**: Supply chain security not tracked -- **No License Compliance**: Dependency licenses not audited -- **No Load Testing**: Performance characteristics unknown - ---- - -## Quick Reference - -### Common Tasks - -| Task | Command | -|------|---------| -| Run locally (Aspire) | `aspire run` | -| Build all services | `./build.sh` | -| Build backend | `cd src/agentic-api && dotnet build` | -| Build frontend | `cd src/agentic-ui && npm run build` | -| Run backend tests | `cd tests/agentic-api-tests && dotnet test` | -| Run frontend tests | `cd src/agentic-ui && npm test` | -| Deploy to Azure | `azd deploy` | -| View logs | `azd logs` or Azure Portal | -| Update dependencies | `dotnet restore && npm update` | - -### Important Directories - -| Path | Purpose | -|------|---------| -| `src/agentic-api/` | Backend API service (.NET 10) | -| `src/agentic-ui/` | Frontend web app (Next.js 16) | -| `infra/` | Infrastructure as Code (Bicep) | -| `specs/docs/` | Comprehensive documentation | -| `specs/features/` | Feature specifications | -| `specs/adr/` | Architecture Decision Records | - -### Important Files +## Key Files | File | Purpose | |------|---------| -| `apphost.cs` | .NET Aspire orchestration | -| `azure.yaml` | Azure Developer CLI config | -| `src/agentic-api/Program.cs` | Backend startup configuration | +| `src/agentic-api/Program.cs` | Backend config and workflow registration | +| `src/agentic-api/Workflows/DummyWorkflow.cs` | Example workflow implementation | | `src/agentic-api/AGUIWorkflowAgent.cs` | AGUI protocol adapter | -| `src/agentic-ui/app/page.tsx` | Frontend landing page | -| `infra/main.bicep` | Main infrastructure template | - -### Azure Resources - -| Resource | Purpose | Status | -|----------|---------|--------| -| Azure OpenAI | AI model hosting (GPT-5 Mini) | ✅ Used | -| Container Apps | Application hosting | ✅ Used | -| Azure AI Foundry | AI project management | ✅ Used | -| Application Insights | Telemetry and monitoring | ✅ Used | -| Cosmos DB | NoSQL database | ⚠️ Provisioned, unused | -| Azure AI Search | Vector search | ⚠️ Provisioned, unused | +| `src/agentic-ui/app/page.tsx` | Frontend main page | +| `infra/main.bicep` | Infrastructure as Code | --- -## Getting Help - -### Documentation Resources +## Known Limitations -- **Project Documentation**: `/specs/docs/` -- **Feature Specs**: `/specs/features/` -- **Architecture Decisions**: `/specs/adr/` -- **Microsoft Docs MCP**: Use `microsoft.docs.mcp` tool for official Microsoft documentation -- **Context7 MCP**: Use `context7` tool for library-specific documentation (fallback) +- No authentication, input validation, rate limiting +- Cosmos DB & AI Search provisioned but unused +- Single agent (DummyWorkflow) only -### External Resources +--- -- **Microsoft Agent Framework**: [Limited preview documentation available] -- **.NET Aspire**: [learn.microsoft.com/aspire](https://learn.microsoft.com/aspire) -- **Azure Container Apps**: [learn.microsoft.com/azure/container-apps](https://learn.microsoft.com/azure/container-apps) -- **Next.js 16**: [nextjs.org/docs](https://nextjs.org/docs) -- **CopilotKit**: [docs.copilotkit.ai](https://docs.copilotkit.ai) +## Agent Skills -### Troubleshooting +Procedural guides for common tasks are available as Agent Skills in `.github/skills/`: -**For authentication issues**: Check `DefaultAzureCredential` chain and run `az login` -**For build issues**: Verify .NET 10.0 SDK and Node.js 20.x are installed -**For deployment issues**: Run `azd auth login` and verify Azure subscription access -**For runtime issues**: Check Application Insights logs in Azure Portal +| Skill | Use When | +|-------|----------| +| `new-workflow` | Creating new agent workflows and executors | +| `image-generation` | Adding text-to-image capabilities | +| `human-in-the-loop` | Implementing approval workflows | +| `project-commands` | Running setup, build, deploy, test commands | --- -**Last Updated**: December 3, 2025 -**Project Status**: Prototype/Demo - -**For human developers**: Refer to [README.md](README.md) for project overview and [/specs/docs/](specs/docs/) for comprehensive documentation. +**Last Updated**: January 22, 2026 | **Status**: Prototype/Demo diff --git a/README.md b/README.md index 41ff9b9c..87a17ce3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,42 @@ https://github.com/user-attachments/assets/f0529e70-f437-4a14-93bc-4ab5a0450540 +# Enabling a New Blueprint for Building Software + +**Legend:** +- 🟪 Pre-built code +- 🟣 Code created w/ specs +- 🟨 Specs +- 🟧 Agent.md¹ + +--- + +## What it is + +A **Spec** is a structured, machine-readable blueprint that defines what a solution should do (purpose, inputs, outputs) to enable consistent automation, validation, and reuse. + +An **agent.md** file is like a "readme" / a manual designed for machine-readable instructions that help **coding agents** work effectively in your repo. + +--- + +## Evolution Path + +| | **Current status (Crawl)** | **Spec enhanced (Walk)** | **Full Spec ahead (Run)** | +|---|---|---|---| +| **Specs** | | 🟨🟨🟨
🟧 | 🟨🟨🟨🟨 **...**
🟧 | +| **Coded solution (e.g., accelerator)** | 🟪🟪🟪🟪🟪
🟪🟪 | 🟣🟣🟣🟣🟣
🟣🟣 | 🟣🟣🟣 Evals → 🟣🟣🟣
🟣🟣🟣 Add. DB → 🟣🟣🟣 | +| **Customer requirements** | ⚫⚫⚫⚫⚫ | ⚫⚫⚫⚫⚫ | ⚫⚫⚫⚫⚫ | +| **Explanation** | Solutions delivered as rigid codebases that rarely match exact needs - require manual rework | A standardized core codebase is provided (Shell), then tailored to requirements through editable specs. | Entire solution "automatically" composed from specs, eliminating manual coding & adjustments | + +--- + +## Advantages + +- ✓ Opinionated stack +- ✓ Extensible +- ✓ Simplified maintenance, change management +- ✓ Faster time-to-PoC and time-to-prod + ## 🎯 Overview This repository provides a preconfigured development environment and agent-driven workflow that takes you from product concept to deployed application through a structured, step-by-step process. diff --git a/apphost.cs b/apphost.cs index 3605b20f..0dd46867 100644 --- a/apphost.cs +++ b/apphost.cs @@ -1,18 +1,29 @@  -#:sdk Aspire.AppHost.Sdk@13.0.0 -#:package Aspire.Hosting.JavaScript@13.0.0 -#:package Aspire.Hosting.Azure.CognitiveServices@13.0.0 -#:package Aspire.Hosting.Azure.AIFoundry@13.0.0-preview.1.25560.3 +#:sdk Aspire.AppHost.Sdk@13.1.0 +#:package Aspire.Hosting.JavaScript@13.1.0 +#:package Aspire.Hosting.Azure.CognitiveServices@13.1.0 +#:package Aspire.Hosting.Azure.AIFoundry@13.1.0-preview.1.25616.3 +#:package Aspire.Hosting.Azure.CosmosDB@13.1.0 +#:package Aspire.Hosting.Python@13.1.0 var builder = DistributedApplication.CreateBuilder(args); var openAiEndpoint = builder.AddParameter("openAiEndpoint"); var openAiDeployment = builder.AddParameter("openAiDeployment"); +var imageModelDeployment = builder.AddParameter("imageModelDeployment"); + +var cosmosName = builder.AddParameter("cosmosName"); +var cosmosResourceGroup = builder.AddParameter("cosmosResourceGroup"); + +var cosmos = builder.AddAzureCosmosDB("cosmos-db") + .AsExisting(cosmosName, cosmosResourceGroup); var api = builder.AddCSharpApp("agentic-api", "./src/agentic-api") .WithEnvironment("AZURE_OPENAI_ENDPOINT", openAiEndpoint) - .WithEnvironment("AZURE_OPENAI_DEPLOYMENT_NAME", openAiDeployment); + .WithEnvironment("AZURE_OPENAI_DEPLOYMENT_NAME", openAiDeployment) + .WithEnvironment("AZURE_IMAGE_MODEL_DEPLOYMENT_NAME", imageModelDeployment) + .WithReference(cosmos); builder.AddJavaScriptApp("agentic-ui", "./src/agentic-ui") .WithRunScript("dev") @@ -24,4 +35,10 @@ .WithExternalHttpEndpoints() .PublishAsDockerFile(); +// Documentation site using MkDocs +builder.AddPythonModule("docs", "./specs", "mkdocs") + .WithArgs("serve", "--dev-addr", "0.0.0.0:8100") + .WithHttpEndpoint(targetPort: 8100, name: "http") + .ExcludeFromManifest(); + builder.Build().Run(); diff --git a/apphost.settings.template.json b/apphost.settings.template.json index 6b4b7d54..677a673b 100644 --- a/apphost.settings.template.json +++ b/apphost.settings.template.json @@ -1,6 +1,7 @@ { "Parameters": { "openAiEndpoint": "", - "openAiDeployment": "" + "openAiDeployment": "", + "imageModelDeployment": "" } } \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep index 183e5cd8..97e2a7be 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -56,6 +56,7 @@ module resources 'resources.bicep' = { aiFoundryProjectEndpoint: aiModelsDeploy.outputs.ENDPOINT openAiEndpoint: aiModelsDeploy.outputs.OPENAI_ENDPOINT deploymentName: deploymentName + imageDeploymentName: imageModelDeploy.outputs.deploymentName } } @@ -93,6 +94,21 @@ module aiSearchConnection 'modules/ai-search-conn.bicep' = { aiSearchName: resources.outputs.aiSearchName } } + +module imageModelDeploy 'modules/image-model.bicep' = { + scope: rg + name: 'image-model-deployment' + params: { + aiServicesAccountName: aiModelsDeploy.outputs.aiServicesAccountName + deploymentName: 'FLUX2pro' + skuName: 'GlobalStandard' + skuCapacity: 1 + format: 'Black Forest Labs' + modelName: 'FLUX.2-pro' + modelVersion: '1' + } +} + output AZURE_CONTAINER_REGISTRY_ENDPOINT string = resources.outputs.AZURE_CONTAINER_REGISTRY_ENDPOINT output AZURE_RESOURCE_AGENTIC_API_ID string = resources.outputs.AZURE_RESOURCE_AGENTIC_API_ID output AZURE_RESOURCE_AGENTIC_UI_ID string = resources.outputs.AZURE_RESOURCE_AGENTIC_UI_ID @@ -103,3 +119,6 @@ output AZURE_AI_SEARCH_ENDPOINT string = resources.outputs.AZURE_AI_SEARCH_ENDPO output AZURE_RESOURCE_SEARCH_ID string = resources.outputs.AZURE_RESOURCE_SEARCH_ID output AZURE_OPENAI_ENDPOINT string = aiModelsDeploy.outputs.OPENAI_ENDPOINT output AZURE_OPENAI_DEPLOYMENT_NAME string = deploymentName +output AZURE_IMAGE_MODEL_DEPLOYMENT_NAME string = imageModelDeploy.outputs.deploymentName +output AZURE_COSMOS_NAME string = resources.outputs.AZURE_COSMOS_NAME +output AZURE_COSMOS_RESOURCE_GROUP string = resources.outputs.AZURE_COSMOS_RESOURCE_GROUP diff --git a/infra/modules/image-model.bicep b/infra/modules/image-model.bicep new file mode 100644 index 00000000..5cad7e7c --- /dev/null +++ b/infra/modules/image-model.bicep @@ -0,0 +1,40 @@ +@description('The name of the AI Services account') +param aiServicesAccountName string + +@description('The name of the deployment') +param deploymentName string + +@description('The SKU name for the deployment') +param skuName string = 'GlobalStandard' + +@description('The SKU capacity for the deployment') +param skuCapacity int = 1 + +param format string +param modelName string +param modelVersion string + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiServicesAccountName +} + +resource imageDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = { + parent: aiAccount + name: deploymentName + sku: { + name: skuName + capacity: skuCapacity + } + properties: { + model: { + format: format + name: modelName + version: modelVersion + } + + } +} + +output deploymentName string = imageDeployment.name +output deploymentId string = imageDeployment.id + diff --git a/infra/resources.bicep b/infra/resources.bicep index 5d9b6189..39af14eb 100644 --- a/infra/resources.bicep +++ b/infra/resources.bicep @@ -10,6 +10,7 @@ param agenticUiExists bool param aiFoundryProjectEndpoint string param openAiEndpoint string param deploymentName string +param imageDeploymentName string @description('Id of the user or app to assign application roles') param principalId string @@ -96,7 +97,12 @@ module cosmos 'br/public:avm/res/document-db/database-account:0.8.1' = { ] sqlRoleDefinitions: [ { - name: 'service-access-cosmos-sql-role' + name: 'cosmosdb-data-plane-contributor' + dataAction: [ + 'Microsoft.DocumentDB/databaseAccounts/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*' + 'Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*' + ] } ] capabilitiesToAdd: [ 'EnableServerless' ] @@ -230,6 +236,10 @@ module agenticApi 'br/public:avm/res/app/container-app:0.8.0' = { name:'AZURE_OPENAI_DEPLOYMENT_NAME' value: deploymentName } + { + name:'AZURE_IMAGE_MODEL_DEPLOYMENT_NAME' + value: imageDeploymentName + } { name: 'PORT' value: '8080' @@ -349,3 +359,5 @@ output AZURE_RESOURCE_AGENTIC_STORAGE_ID string = '${cosmos.outputs.resourceId}/ output AZURE_AI_SEARCH_ENDPOINT string = search.outputs.endpoint output AZURE_RESOURCE_SEARCH_ID string = search.outputs.resourceId output aiSearchName string = search.outputs.name +output AZURE_COSMOS_NAME string = cosmos.outputs.name +output AZURE_COSMOS_RESOURCE_GROUP string = cosmos.outputs.resourceGroupName diff --git a/infra/scripts/postprovision.ps1 b/infra/scripts/postprovision.ps1 index 75a68c93..d795c231 100644 --- a/infra/scripts/postprovision.ps1 +++ b/infra/scripts/postprovision.ps1 @@ -33,6 +33,9 @@ foreach ($line in $azdEnvOutput) { $OPENAI_ENDPOINT = if ($envVars.ContainsKey('AZURE_OPENAI_ENDPOINT')) { $envVars['AZURE_OPENAI_ENDPOINT'] } else { "" } $OPENAI_DEPLOYMENT = if ($envVars.ContainsKey('AZURE_OPENAI_DEPLOYMENT_NAME')) { $envVars['AZURE_OPENAI_DEPLOYMENT_NAME'] } else { "" } +$IMAGE_MODEL_DEPLOYMENT = if ($envVars.ContainsKey('AZURE_IMAGE_MODEL_DEPLOYMENT_NAME')) { $envVars['AZURE_IMAGE_MODEL_DEPLOYMENT_NAME'] } else { "" } +$COSMOS_NAME = if ($envVars.ContainsKey('AZURE_COSMOS_NAME')) { $envVars['AZURE_COSMOS_NAME'] } else { "" } +$COSMOS_RESOURCE_GROUP = if ($envVars.ContainsKey('AZURE_COSMOS_RESOURCE_GROUP')) { $envVars['AZURE_COSMOS_RESOURCE_GROUP'] } else { "" } # Validate required variables if ([string]::IsNullOrEmpty($OPENAI_ENDPOINT)) { @@ -45,11 +48,29 @@ if ([string]::IsNullOrEmpty($OPENAI_DEPLOYMENT)) { $OPENAI_DEPLOYMENT = "" } +if ([string]::IsNullOrEmpty($IMAGE_MODEL_DEPLOYMENT)) { + Write-Host "Warning: AZURE_IMAGE_MODEL_DEPLOYMENT_NAME environment variable is not set" -ForegroundColor Yellow + $IMAGE_MODEL_DEPLOYMENT = "" +} + +if ([string]::IsNullOrEmpty($COSMOS_NAME)) { + Write-Host "Warning: AZURE_COSMOS_NAME environment variable is not set" -ForegroundColor Yellow + $COSMOS_NAME = "" +} + +if ([string]::IsNullOrEmpty($COSMOS_RESOURCE_GROUP)) { + Write-Host "Warning: AZURE_COSMOS_RESOURCE_GROUP environment variable is not set" -ForegroundColor Yellow + $COSMOS_RESOURCE_GROUP = "" +} + # Update the settings file try { $settingsContent = Get-Content $SETTINGS_FILE -Raw | ConvertFrom-Json $settingsContent.Parameters.openAiEndpoint = $OPENAI_ENDPOINT $settingsContent.Parameters.openAiDeployment = $OPENAI_DEPLOYMENT + $settingsContent.Parameters.imageModelDeployment = $IMAGE_MODEL_DEPLOYMENT + $settingsContent.Parameters.cosmosName = $COSMOS_NAME + $settingsContent.Parameters.cosmosResourceGroup = $COSMOS_RESOURCE_GROUP $settingsContent | ConvertTo-Json -Depth 10 | Set-Content $SETTINGS_FILE } catch { Write-Host "Error updating settings file: $_" -ForegroundColor Red @@ -59,3 +80,6 @@ try { Write-Host "apphost.settings.json configured successfully!" -ForegroundColor Green Write-Host " - OpenAI Endpoint: $OPENAI_ENDPOINT" -ForegroundColor Cyan Write-Host " - OpenAI Deployment: $OPENAI_DEPLOYMENT" -ForegroundColor Cyan +Write-Host " - Image Model Deployment: $IMAGE_MODEL_DEPLOYMENT" -ForegroundColor Cyan +Write-Host " - Cosmos DB Name: $COSMOS_NAME" -ForegroundColor Cyan +Write-Host " - Cosmos DB Resource Group: $COSMOS_RESOURCE_GROUP" -ForegroundColor Cyan diff --git a/infra/scripts/postprovision.sh b/infra/scripts/postprovision.sh index d2811eef..8df3931f 100755 --- a/infra/scripts/postprovision.sh +++ b/infra/scripts/postprovision.sh @@ -29,6 +29,9 @@ eval "$(azd env get-values)" OPENAI_ENDPOINT="${AZURE_OPENAI_ENDPOINT:-}" OPENAI_DEPLOYMENT="${AZURE_OPENAI_DEPLOYMENT_NAME:-}" +IMAGE_MODEL_DEPLOYMENT="${AZURE_IMAGE_MODEL_DEPLOYMENT_NAME:-}" +COSMOS_NAME="${AZURE_COSMOS_NAME:-}" +COSMOS_RESOURCE_GROUP="${AZURE_COSMOS_RESOURCE_GROUP:-}" # Validate required variables if [ -z "$OPENAI_ENDPOINT" ]; then @@ -41,19 +44,39 @@ if [ -z "$OPENAI_DEPLOYMENT" ]; then OPENAI_DEPLOYMENT="" fi +if [ -z "$IMAGE_MODEL_DEPLOYMENT" ]; then + echo -e "\033[0;33mWarning: AZURE_IMAGE_MODEL_DEPLOYMENT_NAME environment variable is not set\033[0m" + IMAGE_MODEL_DEPLOYMENT="" +fi + +if [ -z "$COSMOS_NAME" ]; then + echo -e "\033[0;33mWarning: AZURE_COSMOS_NAME environment variable is not set\033[0m" + COSMOS_NAME="" +fi + +if [ -z "$COSMOS_RESOURCE_GROUP" ]; then + echo -e "\033[0;33mWarning: AZURE_COSMOS_RESOURCE_GROUP environment variable is not set\033[0m" + COSMOS_RESOURCE_GROUP="" +fi + # Update the settings file using jq if command -v jq &> /dev/null; then # Use jq if available for proper JSON manipulation - jq --arg endpoint "$OPENAI_ENDPOINT" --arg deployment "$OPENAI_DEPLOYMENT" \ - '.Parameters.openAiEndpoint = $endpoint | .Parameters.openAiDeployment = $deployment' \ + jq --arg endpoint "$OPENAI_ENDPOINT" --arg deployment "$OPENAI_DEPLOYMENT" --arg imageModel "$IMAGE_MODEL_DEPLOYMENT" \ + --arg cosmosName "$COSMOS_NAME" --arg cosmosResourceGroup "$COSMOS_RESOURCE_GROUP" \ + '.Parameters.openAiEndpoint = $endpoint | .Parameters.openAiDeployment = $deployment | .Parameters.imageModelDeployment = $imageModel | .Parameters.cosmosName = $cosmosName | .Parameters.cosmosResourceGroup = $cosmosResourceGroup' \ "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE" else # Fallback to sed if jq is not available (less robust but works for simple cases) sed -i.bak "s|\"openAiEndpoint\".*:.*\".*\"|\"openAiEndpoint\": \"$OPENAI_ENDPOINT\"|g" "$SETTINGS_FILE" sed -i.bak "s|\"openAiDeployment\".*:.*\".*\"|\"openAiDeployment\": \"$OPENAI_DEPLOYMENT\"|g" "$SETTINGS_FILE" - rm -f "$SETTINGS_FILE.bak" + sed -i.bak "s|\"imageModelDeployment\".*:.*\".*\"|\"imageModelDeployment\": \"$IMAGE_MODEL_DEPLOYMENT\"|g" "$SETTINGS_FILE" sed -i.bak "s|\"cosmosName\".*:.*\"\.\*\"|\"cosmosName\": \"$COSMOS_NAME\"|g" "$SETTINGS_FILE" + sed -i.bak "s|\"cosmosResourceGroup\".*:.*\"\.\*\"|\"cosmosResourceGroup\": \"$COSMOS_RESOURCE_GROUP\"|g" "$SETTINGS_FILE" rm -f "$SETTINGS_FILE.bak" fi echo -e "\033[0;32mapphost.settings.json configured successfully!\033[0m" echo -e "\033[0;36m - OpenAI Endpoint: $OPENAI_ENDPOINT\033[0m" echo -e "\033[0;36m - OpenAI Deployment: $OPENAI_DEPLOYMENT\033[0m" +echo -e "\033[0;36m - Image Model Deployment: $IMAGE_MODEL_DEPLOYMENT\033[0m" +echo -e "\033[0;36m - Cosmos DB Name: $COSMOS_NAME\033[0m" +echo -e "\033[0;36m - Cosmos DB Resource Group: $COSMOS_RESOURCE_GROUP\033[0m" diff --git a/specs/.gitignore b/specs/.gitignore new file mode 100644 index 00000000..de8e85c9 --- /dev/null +++ b/specs/.gitignore @@ -0,0 +1,9 @@ +# Python virtual environment +.venv/ + +# MkDocs build output +site/ + +# Python cache +__pycache__/ +*.py[cod] diff --git a/specs/features/ai-chat-interface.md b/specs/docs/features/ai-chat-interface.md similarity index 74% rename from specs/features/ai-chat-interface.md rename to specs/docs/features/ai-chat-interface.md index 4cddd27a..f08f4553 100644 --- a/specs/features/ai-chat-interface.md +++ b/specs/docs/features/ai-chat-interface.md @@ -40,13 +40,19 @@ - ✅ Text input field is accessible - ✅ Placeholder text guides user - ✅ Enter key sends message -- ❌ Character limit not enforced -- ❌ No input validation +- ✅ Character limit enforced (4000 characters) +- ✅ Input validation implemented -**Gaps:** -- No maximum message length -- No rate limiting on message sending -- No profanity or content filtering +**Implementation:** +- Created `ChatInput.tsx` component with: + - Character counter with visual feedback + - Real-time validation + - Rate limiting (10 messages per minute, 2 seconds between messages) + - Visual warnings for rate limits and character limits + - ARIA labels for accessibility + +**Remaining Gaps:** +- No profanity or content filtering (requires external service) ### FR-3: Send Messages to AI @@ -67,14 +73,22 @@ - ✅ AI responses displayed in chat - ✅ Responses appear as assistant messages - ✅ Text formatting preserved -- ❌ No markdown rendering -- ❌ No code highlighting -- ❌ No rich media support - -**Gaps:** -- No streaming indicator (typing animation) -- No error state display -- No response time tracking +- ✅ Markdown rendering supported +- ✅ Code highlighting implemented +- ❌ No rich media support (images, charts) + +**Implementation:** +- Created `CustomMessageRenderer.tsx` component with: + - react-markdown for markdown parsing + - remark-gfm for GitHub Flavored Markdown support + - rehype-highlight for code syntax highlighting + - Custom styling for links, code blocks, and inline code +- Created `TypingIndicator.tsx` for visual feedback during response generation +- Created `ErrorDisplay.tsx` for error states with retry functionality + +**Remaining Gaps:** +- No rich media support (images, videos, charts) +- Response time tracking not yet implemented ### FR-5: Streaming Responses (Partial) @@ -108,13 +122,18 @@ **Requirement:** Chat interface should be available 99% of the time -**Current State:** ❓ **Unknown** +**Current State:** ⚠️ **Partially Implemented** -**Gaps:** -- No health checks implemented -- No monitoring dashboards -- No uptime tracking +**Implementation:** +- Added `/health` endpoint for health checks +- Health check includes basic readiness check +- Can be used by load balancers and monitoring systems + +**Remaining Gaps:** +- No monitoring dashboards configured +- No uptime tracking in place - No incident response plan +- No SLA definitions ### NFR-3: Scalability @@ -216,19 +235,32 @@ ### Required Error Handling Capabilities **User-Facing Errors:** -- Display clear, actionable error messages when AI service is unavailable -- Provide fallback responses when processing fails -- Show connection status indicators +- ✅ Display clear, actionable error messages when AI service is unavailable +- ✅ Provide fallback responses when processing fails +- ✅ Show connection status indicators via ErrorDisplay component **System Error Handling:** -- Handle AI service timeouts gracefully -- Retry failed requests with exponential backoff -- Log errors for monitoring and debugging -- Implement circuit breaker for service protection - -**Current State:** -- Basic error handling present (fallback messages) -- Missing: Specific exception handling, retry logic, circuit breaker patterns +- ✅ Handle AI service timeouts gracefully (120-second timeout configured) +- ⚠️ Retry failed requests with exponential backoff (Polly configured, needs HttpClient wiring) +- ✅ Log errors for monitoring and debugging +- ❌ Circuit breaker not yet implemented + +**Implementation:** +- **Backend:** + - Created `ErrorHandlingMiddleware` for consistent error responses + - Added user-friendly error messages for common exceptions + - Configured request timeouts (120 seconds) + - Added Polly for resilience patterns + - Enhanced logging with structured error information +- **Frontend:** + - Created `ErrorDisplay.tsx` with retry functionality + - Visual feedback for different error states + - Dismiss and retry actions for users + +**Remaining Gaps:** +- Circuit breaker pattern not implemented +- Retry logic needs to be wired to HttpClient instances +- No distributed tracing for error correlation ## Limitations and Known Issues diff --git a/specs/docs/index.md b/specs/docs/index.md new file mode 100644 index 00000000..b19dbbec --- /dev/null +++ b/specs/docs/index.md @@ -0,0 +1,47 @@ +# Agentic Shell .NET Documentation + +Welcome to the technical documentation and feature specifications for the **agentic-shell-dotnet** project. + +## Overview + +**agentic-shell-dotnet** is a microservices-based AI agent application built using the Microsoft Agent Framework. + +| Component | Stack | +|-----------|-------| +| **Frontend** | Next.js 16 + React 19 + TypeScript + CopilotKit | +| **Backend** | ASP.NET Core 10 + Microsoft Agent Framework | +| **Orchestration** | .NET Aspire | +| **Deployment** | Azure Container Apps + Azure AI services | + +## Quick Start + +```bash +# Setup +az login && azd auth login && azd provision + +# Run locally +aspire run # Dashboard: http://localhost:15888 + +# Build & Deploy +./build.sh +azd deploy +``` + +## Documentation Sections + +### Technical Documentation + +- **[Architecture](architecture/overview.md)** - System design and security +- **[Infrastructure](infrastructure/deployment.md)** - Deployment and operations +- **[Integration](integration/apis.md)** - API specifications +- **[Technology](technology/stack.md)** - Stack and dependencies + +### Feature Requirements + +- **[AI Chat Interface](features/ai-chat-interface.md)** - Chat interface specifications + +## Key Resources + +- [Project Repository](https://github.com/EmeaAppGbb/agentic-shell-dotnet) +- [AGENTS.md](https://github.com/EmeaAppGbb/agentic-shell-dotnet/blob/main/AGENTS.md) - AI Agent Instructions +- [README.md](https://github.com/EmeaAppGbb/agentic-shell-dotnet/blob/main/README.md) - Human Developer Guide diff --git a/specs/mkdocs.yml b/specs/mkdocs.yml new file mode 100644 index 00000000..5000e3dd --- /dev/null +++ b/specs/mkdocs.yml @@ -0,0 +1,59 @@ +site_name: Shell .NET Documentation +site_description: Technical documentation and feature specifications for the Shell .NET application +site_author: EMEA App GBB Team + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.expand + - navigation.top + - search.highlight + - content.code.copy + +plugins: + - search + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - admonition + - pymdownx.details + - tables + - toc: + permalink: true + +docs_dir: docs + +nav: + - Home: index.md + - Technical Documentation: + - Architecture: + - Overview: architecture/overview.md + - Security: architecture/security.md + - Infrastructure: + - Deployment: infrastructure/deployment.md + - Integration: + - APIs: integration/apis.md + - Technology: + - Stack: technology/stack.md + - Dependencies: technology/dependencies.md + - Feature Requirements: + - AI Chat Interface: features/ai-chat-interface.md diff --git a/specs/requirements.txt b/specs/requirements.txt new file mode 100644 index 00000000..fef26ebb --- /dev/null +++ b/specs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs>=1.6.0 +mkdocs-material>=9.5.0 diff --git a/src/agentic-api/AGUIWorkflowAgent.cs b/src/agentic-api/AGUIWorkflowAgent.cs index 12b88c06..00578a27 100644 --- a/src/agentic-api/AGUIWorkflowAgent.cs +++ b/src/agentic-api/AGUIWorkflowAgent.cs @@ -1,5 +1,4 @@ using System.Runtime.CompilerServices; -using System.Text.Json; using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; @@ -21,84 +20,67 @@ public override async IAsyncEnumerable RunStreamingAsync AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - var allUpdates = new List(); await foreach (var update in this.InnerAgent.RunStreamingAsync(messages, thread, options, cancellationToken).ConfigureAwait(false)) { switch (update.RawRepresentation) { case WorkflowOutputEvent outputEvent: - allUpdates.Add(update); - yield return CreateUpdateFromEvent(update, outputEvent.Data); - break; - - // case ExecutorCompletedEvent completedEvent: - // allUpdates.Add(update); - // yield return CreateUpdateFromEvent(update, completedEvent.Data); - // break; - - // case SuperStepCompletedEvent superStepEvent: - // allUpdates.Add(update); - // yield return CreateUpdateFromEvent(update, superStepEvent.Data); - // break; - + { + yield return ExtractFunctionCallUpdate(update, outputEvent.Data); + //yield return CreateUpdateFromEvent(update, outputEvent.Data); + break; + } + default: - yield return update; + yield return ExtractFunctionCallUpdate(update, update.RawRepresentation); break; } } } - private static AgentRunResponseUpdate CreateUpdateFromEvent(AgentRunResponseUpdate update, object? data) - { - var textContent = SerializeData(data); - - return new AgentRunResponseUpdate - { - AdditionalProperties = update.AdditionalProperties, - AgentId = update.AgentId, - AuthorName = update.AuthorName, - CreatedAt = update.CreatedAt, - Contents = { new TextContent(textContent) }, - ContinuationToken = update.ContinuationToken, - MessageId = update.MessageId, - RawRepresentation = update.RawRepresentation, - ResponseId = update.ResponseId, - Role = update.Role - }; - } - - private static string SerializeData(object? data) + private static AgentRunResponseUpdate ExtractFunctionCallUpdate(AgentRunResponseUpdate update, object? data) { - if (data == null) + IList? updatedContents = null; + var content = data; +#pragma warning disable MEAI001 // Type is for evaluation purposes only + if (content is FunctionApprovalRequestContent request) { - return string.Empty; - } + updatedContents ??= [.. update.Contents]; + var functionCall = request.FunctionCall; + var approvalId = request.Id; - // If it's already a string, return as-is - if (data is string str) - { - return str; + updatedContents.Add(new FunctionCallContent( + callId: approvalId, + name: functionCall.Name, + arguments: functionCall.Arguments)); } - - // For primitive types, use ToString() - if (data.GetType().IsPrimitive || data is DateTime || data is DateTimeOffset || data is Guid) + else if (content is TextContent textContent) { - return data.ToString() ?? string.Empty; + updatedContents ??= [.. update.Contents]; + updatedContents.Add(new TextContent(textContent.Text)); } +#pragma warning restore MEAI001 - // For complex types, serialize to JSON - try + if (updatedContents is not null) { - return JsonSerializer.Serialize(data, new JsonSerializerOptions + var chatUpdate = update.AsChatResponseUpdate(); + // Yield a tool call update that represents the approval request + return new AgentRunResponseUpdate(new ChatResponseUpdate() { - WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); - } - catch - { - // Fallback to ToString() if serialization fails - return data.ToString() ?? string.Empty; + Role = chatUpdate.Role, + Contents = updatedContents, + MessageId = chatUpdate.MessageId, + AuthorName = chatUpdate.AuthorName, + CreatedAt = chatUpdate.CreatedAt, + RawRepresentation = chatUpdate.RawRepresentation, + ResponseId = chatUpdate.ResponseId, + AdditionalProperties = chatUpdate.AdditionalProperties + }) + { + AgentId = update.AgentId, + ContinuationToken = update.ContinuationToken + }; } + return update; } } \ No newline at end of file diff --git a/src/agentic-api/ApprovalRequestHelper.cs b/src/agentic-api/ApprovalRequestHelper.cs new file mode 100644 index 00000000..49aa64f8 --- /dev/null +++ b/src/agentic-api/ApprovalRequestHelper.cs @@ -0,0 +1,30 @@ +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. +using Microsoft.Extensions.AI; + +namespace agentic_api.Workflows; + +/// +/// Helper class for creating human-in-the-loop approval requests in workflows. +/// +public static class ApprovalRequestHelper +{ + /// + /// Creates a function approval request that prompts the user to approve an item. + /// + /// The name of the approval function to call + /// The arguments dictionary containing the data to be approved + /// A FunctionApprovalRequestContent that will prompt the user for approval + public static FunctionApprovalRequestContent CreateApprovalRequest( + string functionName, + Dictionary arguments) + { + return new FunctionApprovalRequestContent( + Guid.NewGuid().ToString(), + new FunctionCallContent( + functionName, + functionName, + arguments: arguments + ) + ); + } +} diff --git a/src/agentic-api/Program.cs b/src/agentic-api/Program.cs index d1c64711..e54ee6f9 100644 --- a/src/agentic-api/Program.cs +++ b/src/agentic-api/Program.cs @@ -15,31 +15,59 @@ builder.Services.AddHttpClient().AddLogging(); builder.Services.AddAGUI(); +// Configure request timeout from configuration or use default +var timeoutSeconds = builder.Configuration.GetValue("RequestTimeoutSeconds") ?? 120; +builder.Services.AddRequestTimeouts(options => +{ + options.DefaultPolicy = new Microsoft.AspNetCore.Http.Timeouts.RequestTimeoutPolicy + { + Timeout = TimeSpan.FromSeconds(timeoutSeconds) + }; +}); + +// Add health checks with basic readiness check +builder.Services.AddHealthChecks() + .AddCheck("self", () => Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy("API is running")); + string endpoint = builder.Configuration["AZURE_OPENAI_ENDPOINT"] ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set."); string deploymentName = builder.Configuration["AZURE_OPENAI_DEPLOYMENT_NAME"] ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set."); +string imageDeploymentName = builder.Configuration["AZURE_IMAGE_MODEL_DEPLOYMENT_NAME"] + ?? throw new InvalidOperationException("AZURE_IMAGE_MODEL_DEPLOYMENT_NAME is not set."); + // Register IChatClient builder.Services.AddSingleton(_ => new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) .GetChatClient(deploymentName) .AsIChatClient()); +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +builder.Services.AddSingleton(_ => + new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential()) + .GetImageClient(imageDeploymentName) + .AsIImageGenerator()); +#pragma warning restore MEAI001 // Ty + // Register the dummy workflow factory builder.Services.AddSingleton(); builder.Services.AddOpenAIResponses(); builder.Services.AddOpenAIConversations(); -builder.AddWorkflow("DummyWorkflow" , (sp, name) => { +builder.AddWorkflow("DummyWorkflow", (sp, name) => +{ var factory = sp.GetRequiredService(); return factory.BuildWorkflow("DummyWorkflow"); }).AddAsAIAgent(); var app = builder.Build(); +// Add request timeouts +app.UseRequestTimeouts(); + // Get the dummy workflow and convert it to an agent var dummyWorkflowFactory = app.Services.GetRequiredService(); var dummyWorkflow = dummyWorkflowFactory.BuildWorkflow("DummyWorkflow"); @@ -51,6 +79,9 @@ // Map the dummy workflow agent to the default AGUI endpoint app.MapAGUI("/", dummyAgent); +// Map health check endpoint +app.MapHealthChecks("/health"); + if (builder.Environment.IsDevelopment()) { // Map DevUI endpoint to /devui diff --git a/src/agentic-api/Workflows/DummyWorkflow.cs b/src/agentic-api/Workflows/DummyWorkflow.cs index 6915af07..150759d1 100644 --- a/src/agentic-api/Workflows/DummyWorkflow.cs +++ b/src/agentic-api/Workflows/DummyWorkflow.cs @@ -1,3 +1,4 @@ +#pragma warning disable MEAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. using Microsoft.Agents.AI; using Microsoft.Agents.AI.Workflows; using Microsoft.Extensions.AI; @@ -10,33 +11,67 @@ namespace agentic_api.Workflows; public class DummyWorkflowFactory { private readonly ILogger _inputLogger; - private readonly ILogger _greetingLogger; + private readonly ILogger _textGeneratorLogger; + private readonly ILogger _imageGeneratorLogger; + private readonly ILogger _finalLogger; private readonly IChatClient _chatClient; + private readonly IImageGenerator _imageGenerator; public DummyWorkflowFactory( ILogger chatInputLogger, - ILogger greetingLogger, - IChatClient chatClient) + ILogger textGeneratorLogger, + ILogger imageGeneratorLogger, + ILogger finalLogger, + IChatClient chatClient, + IImageGenerator imageGenerator) { _inputLogger = chatInputLogger; - _greetingLogger = greetingLogger; + _textGeneratorLogger = textGeneratorLogger; + _imageGeneratorLogger = imageGeneratorLogger; + _finalLogger = finalLogger; _chatClient = chatClient; + _imageGenerator = imageGenerator; } public Workflow BuildWorkflow(string name) { // Create executors var chatInput = new DummyChatInputExecutor(_inputLogger); - var greeting = new GreetingExecutor(_greetingLogger, _chatClient); + var textGenerator = new TextGeneratorExecutor(_textGeneratorLogger, _chatClient); + var imageGenerator = new ImageGeneratorExecutor(_imageGeneratorLogger, _chatClient, _imageGenerator); + var final = new FinalExecutor(_finalLogger); - // Build simple workflow: ChatInput -> Greeting + // Build workflow with conditional routing based on user input var workflowBuilder = new WorkflowBuilder(chatInput) .WithName(name) - .AddEdge(chatInput, greeting) - .WithOutputFrom(greeting); + .AddSwitch(chatInput, switchBuilder => + switchBuilder.AddCase(GenerateText(), textGenerator) + .AddCase(GenerateImage(), imageGenerator) + .AddCase(FinalizeWorkflow(), final) + .WithDefault(textGenerator) + ) + .WithOutputFrom(textGenerator) + .WithOutputFrom(imageGenerator) + .WithOutputFrom(final); return workflowBuilder.Build(); } + + public static Func GenerateText() => (input) => + { + return input?.NextStep == DummyWorkflowSteps.GenerateText; + }; + + public static Func GenerateImage() => (input) => + { + return input?.NextStep == DummyWorkflowSteps.GenerateImage; + }; + + public static Func FinalizeWorkflow() => (input) => + { + return input?.NextStep == DummyWorkflowSteps.Finalize; + }; + } /// @@ -62,11 +97,29 @@ private async ValueTask HandleChatMessagesAsync( CancellationToken cancellationToken = default) { var lastUserMessage = messages.LastOrDefault(m => m.Role == ChatRole.User); - var userInput = lastUserMessage?.Text ?? "Hello"; + var approvalMessage = messages.LastOrDefault(m => m.Role == ChatRole.Tool); + var functionResult = approvalMessage?.Contents.OfType().FirstOrDefault(); + + var textApproved = functionResult?.Result?.ToString()?.Contains("text-approved"); + var imageApproved = functionResult?.Result?.ToString()?.Contains("image-approved"); + + if (textApproved == true) + { + _logger.LogInformation("Text content approved by user."); + return new UserInputEvent { Input = lastUserMessage?.Text ?? "Hello", NextStep = DummyWorkflowSteps.GenerateImage }; + } - _logger.LogInformation("Dummy Workflow started with input: {Input}", userInput); + else if (imageApproved == true) + { + _logger.LogInformation("Image content approved by user."); + return new UserInputEvent { Input = lastUserMessage?.Text ?? "Hello", NextStep = DummyWorkflowSteps.Finalize }; + } - return new UserInputEvent { Input = userInput }; + else + { + _logger.LogInformation("No approvals detected, proceeding to generate text content."); + return new UserInputEvent { Input = lastUserMessage?.Text ?? "Hello", NextStep = DummyWorkflowSteps.GenerateText }; + } } private async ValueTask HandleTurnTokenAsync( @@ -83,55 +136,192 @@ private async ValueTask HandleTurnTokenAsync( } /// -/// Greeting executor that uses IChatClient to generate friendly AI greetings. +/// Text generator executor that uses IChatClient to generate text content with human-in-the-loop approval. /// -public sealed class GreetingExecutor : Executor +public sealed class TextGeneratorExecutor : Executor { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly AIAgent _agent; - - public GreetingExecutor(ILogger logger, IChatClient chatClient) : base("Greeting") + public TextGeneratorExecutor(ILogger logger, IChatClient chatClient) : base("TextGenerator") { _logger = logger; + _agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions { - Name = "GreetingAgent", - Instructions = "You are a friendly AI assistant. Greet the user warmly and respond to their message with enthusiasm." - }); + Name = "TextGeneratorAgent", + ChatOptions = new ChatOptions + { + Instructions = "You are a helpful AI assistant that generates text content based on user input. Create clear, concise, and relevant responses. Keep your response under 4000 characters. Be brief and to the point." + }}); } - public override async ValueTask HandleAsync( + public override async ValueTask HandleAsync( UserInputEvent input, IWorkflowContext context, CancellationToken cancellationToken = default) { try { - _logger.LogInformation("Greeting executor received: {Input}", input.Input); - _logger.LogInformation("Calling AI agent to generate greeting response"); + _logger.LogInformation("Text generator executor received: {Input}", input.Input); + _logger.LogInformation("Calling AI agent to generate text content"); + + var options = new ChatOptions + { + MaxOutputTokens = 1000 // Approximately 4000 characters (1 token ≈ 4 chars) + }; - var response = await _agent.RunAsync(new ChatMessage(ChatRole.User, input.Input), cancellationToken: cancellationToken); + var agentResponse = await _agent.RunAsync( + new ChatMessage(ChatRole.User, input.Input), + options: new AgentRunOptions { AdditionalProperties = new() { ["ChatOptions"] = options } }, + cancellationToken: cancellationToken); + + var responseText = agentResponse.Text ?? "Generated text content"; + _logger.LogInformation($"AI agent responded with {responseText.Length} characters"); - var responseText = response.Text ?? "Hi there!"; - _logger.LogInformation("AI agent responded with: {Response}", responseText); + return ApprovalRequestHelper.CreateApprovalRequest( + functionName: "approve_copyright_command", + arguments: new Dictionary + { + { "copyright", responseText } + } + ); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in text generator executor: {Message}. Type: {Type}. StackTrace: {StackTrace}", + ex.Message, ex.GetType().Name, ex.StackTrace); + return new TextContent("Sorry, I encountered an error while generating text content."); + } + } +} + + +/// +/// Final executor that completes the workflow and returns a final response. +/// +public sealed class FinalExecutor : Executor +{ + private readonly ILogger _logger; + public FinalExecutor(ILogger logger) : base("Final") + { + _logger = logger; + } + + public override async ValueTask HandleAsync( + UserInputEvent input, + IWorkflowContext context, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Final executor received: {Input}", input.Input); + _logger.LogInformation("Completing workflow"); + + + var responseText = "Workflow completed successfully!"; + _logger.LogInformation($"Final response: {responseText}"); + + return new TextContent(responseText); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in final executor: {Message}. Type: {Type}. StackTrace: {StackTrace}", + ex.Message, ex.GetType().Name, ex.StackTrace); + return new TextContent("An error occurred while finalizing the workflow."); + } + } +} + +/// +/// Image generator executor that creates images using IImageGenerator with human-in-the-loop approval. +/// +public sealed class ImageGeneratorExecutor : Executor +{ + private readonly ILogger _logger; + private readonly IImageGenerator _imageGenerator; + + private readonly AIAgent _agent; + public ImageGeneratorExecutor(ILogger logger, IChatClient chatClient, IImageGenerator imageGenerator) : base("ImageGenerator") + { + _logger = logger; + _agent = new ChatClientAgent(chatClient, new ChatClientAgentOptions + { + Name = "ImagePromptAgent", + ChatOptions = new ChatOptions + { + Instructions = "You are an expert in generating prompts for image generation models. Take the user input and create a safe, detailed, and descriptive image generation prompt." + } + }); + _imageGenerator = imageGenerator; + } + + public override async ValueTask HandleAsync( + UserInputEvent input, + IWorkflowContext context, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Image generator executor received: {Input}", input.Input); + _logger.LogInformation("Calling AI agent to generate image prompt"); + + var agentResponse = await _agent.RunAsync(new ChatMessage(ChatRole.User, input.Input), cancellationToken: cancellationToken); + + var imagePrompt = agentResponse.Text ?? "A scenic landscape"; - return new AgentRunResponse { Text = responseText }; + // Generate an image from a text prompt + var options = new ImageGenerationOptions + { + MediaType = "image/png", + ResponseFormat = ImageGenerationResponseFormat.Hosted + }; + + + var response = await _imageGenerator.GenerateImagesAsync(imagePrompt, options); + var dataContent = response.Contents.OfType().First(); + + _logger.LogInformation($"Image was created at {dataContent.Uri}"); + return ApprovalRequestHelper.CreateApprovalRequest( + functionName: "approve_design_command", + arguments: new Dictionary + { + { "design", dataContent.Uri } + } + ); } catch (Exception ex) { - _logger.LogError(ex, "Error in greeting executor: {Message}. Type: {Type}. StackTrace: {StackTrace}", + _logger.LogError(ex, "Error in image generator executor: {Message}. Type: {Type}. StackTrace: {StackTrace}", ex.Message, ex.GetType().Name, ex.StackTrace); - return new AgentRunResponse { Text = "Hi! I had trouble processing your message, but I'm here to help!" }; + return new TextContent("Sorry, I encountered an error while generating the image."); } } } +public class WorkflowState +{ + public bool TextApproved { get; set; } + public bool ImageApproved { get; set; } + + public string? TextContent { get; set; } + public string? ImageContent { get; set; } +} + public class UserInputEvent { - public string Input { get; set; } + public required string Input { get; set; } + public DummyWorkflowSteps NextStep { get; set; } } public class AgentRunResponse { - public string Text { get; set; } + public required string Text { get; set; } + public string? ImageUrl { get; set; } +} + +public enum DummyWorkflowSteps +{ + GenerateText, + GenerateImage, + Finalize } \ No newline at end of file diff --git a/src/agentic-api/agentic-api.csproj b/src/agentic-api/agentic-api.csproj index 3f84f0ad..0b880283 100644 --- a/src/agentic-api/agentic-api.csproj +++ b/src/agentic-api/agentic-api.csproj @@ -9,17 +9,17 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/src/agentic-api/appsettings.json b/src/agentic-api/appsettings.json index 10f68b8c..465f71de 100644 --- a/src/agentic-api/appsettings.json +++ b/src/agentic-api/appsettings.json @@ -5,5 +5,6 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "RequestTimeoutSeconds": 120 } diff --git a/src/agentic-ui/app/components/ChatInput.tsx b/src/agentic-ui/app/components/ChatInput.tsx new file mode 100644 index 00000000..996ec16f --- /dev/null +++ b/src/agentic-ui/app/components/ChatInput.tsx @@ -0,0 +1,172 @@ +"use client"; + +import { useState, useEffect, useRef } from 'react'; + +interface ChatInputProps { + onSubmit: (message: string) => void; + disabled?: boolean; + placeholder?: string; +} + +const MAX_MESSAGE_LENGTH = 4000; +const MAX_LENGTH_BUFFER = 100; // Allow slight overflow for warning display +const RATE_LIMIT_INTERVAL = 2000; // 2 seconds between messages +const RATE_LIMIT_MAX_MESSAGES = 10; // Max 10 messages per minute + +/** + * Chat input component with validation, character limit, and rate limiting + */ +export function ChatInput({ onSubmit, disabled = false, placeholder = "Ask me anything..." }: ChatInputProps) { + const [message, setMessage] = useState(''); + const [charCount, setCharCount] = useState(0); + const [isRateLimited, setIsRateLimited] = useState(false); + const [rateLimitCountdown, setRateLimitCountdown] = useState(0); + const [messageTimestamps, setMessageTimestamps] = useState([]); + const inputRef = useRef(null); + + // Update character count when message changes + useEffect(() => { + setCharCount(message.length); + }, [message]); + + // Rate limit countdown timer + useEffect(() => { + if (rateLimitCountdown > 0) { + const timer = setTimeout(() => { + setRateLimitCountdown(rateLimitCountdown - 100); + }, 100); + return () => clearTimeout(timer); + } else if (isRateLimited) { + setIsRateLimited(false); + } + }, [rateLimitCountdown, isRateLimited]); + + // Clean up old timestamps (older than 1 minute) + useEffect(() => { + const interval = setInterval(() => { + const now = Date.now(); + setMessageTimestamps(prev => prev.filter(ts => now - ts < 60000)); + }, 5000); + return () => clearInterval(interval); + }, []); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Validate message + const trimmedMessage = message.trim(); + if (!trimmedMessage || trimmedMessage.length === 0) { + return; + } + + // Character limit is already enforced by UI, this is a safety check + if (trimmedMessage.length > MAX_MESSAGE_LENGTH) { + // Error is already visible in the UI, just prevent submission + return; + } + + // Check rate limiting + const now = Date.now(); + const recentTimestamps = messageTimestamps.filter(ts => now - ts < 60000); + + // Check if too many messages in the last minute + if (recentTimestamps.length >= RATE_LIMIT_MAX_MESSAGES) { + setIsRateLimited(true); + setRateLimitCountdown(5000); // 5 second cooldown + return; + } + + // Check if sending too quickly + const lastTimestamp = recentTimestamps[recentTimestamps.length - 1]; + if (lastTimestamp && now - lastTimestamp < RATE_LIMIT_INTERVAL) { + setIsRateLimited(true); + const remaining = RATE_LIMIT_INTERVAL - (now - lastTimestamp); + setRateLimitCountdown(remaining); + return; + } + + // Submit the message + onSubmit(trimmedMessage); + setMessage(''); + setMessageTimestamps([...recentTimestamps, now]); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Submit on Enter (without Shift) + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + const isOverLimit = charCount > MAX_MESSAGE_LENGTH; + const canSubmit = !disabled && !isRateLimited && charCount > 0 && !isOverLimit; + + return ( +
+
+