diff --git a/README.md b/README.md index 1e00cbc..eca36cd 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,12 @@ Run it locally in minutes against sample data, point it at your real inbox, then ## Prerequisites -- Python 3.13+. Easiest install: [uv](https://docs.astral.sh/uv/), then `uv python install 3.13`. -- [Azure Functions Core Tools v4](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local) ≥ 4.12.0 (the v5 preview is not yet compatible). -- [Azure Developer CLI (`azd`)](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/). -- An Azure subscription. `azd provision` (Quickstart step 2) creates the Microsoft Foundry model deployment the agents need - required even for the offline path. -- For real M365 (or `azd up`): permission to authorize Microsoft 365 connectors, plus the `connector-namespace` CLI extension: - -**macOS / Linux:** -```bash -curl -fsSL https://aka.ms/connector-namespace-cli-install | sh -``` - -**Windows:** -```powershell -powershell -Command "Invoke-WebRequest -Uri 'https://aka.ms/connector-namespace-cli-install' -OutFile 'install.sh'; wsl bash install.sh" -``` +- [uv](https://docs.astral.sh/uv/), then `uv python install 3.13`. +- [Azurite](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=npm-package) for local storage emulation (e.g. `npm install -g azurite`). `func5` launches it on demand if it's on your `PATH`. +- [Azure Developer CLI (`azd`)](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd). +- [Azure Functions Core Tools v5 (preview)](https://github.com/Azure/azure-functions-core-tools/releases). This template calls the binary `func5` so v5 sits alongside any existing v4 `func`. Already on v4? See [Troubleshooting: still using v4](docs/troubleshooting.md#still-using-v4). +- [Azure CLI `connector-namespace` extension](https://github.com/Azure/Connectors/tree/main/public-preview/connector-namespace-cli) — needed for `azd up` and real M365 connectors. +- An Azure subscription. `azd provision` (Quickstart step 2) creates the Microsoft Foundry model deployment the agents need, required even for the offline path. ## Quickstart @@ -40,18 +31,12 @@ Five steps: install, get resources, run locally, try it, deploy. ### 1. Install the tools -**macOS:** +Follow the prereq links above and make sure the v5 binary is on your `PATH` as `func5`. Then run this once per machine to install the Python worker, templates, and extension bundles (which ships the M365 connectors): + ```bash -brew tap azure/functions -brew install azure-functions-core-tools@4 -brew install azure-dev -npm install -g azurite -curl -LsSf https://astral.sh/uv/install.sh | sh +func5 setup --features python ``` -**Linux / Windows / WSL:** -Use the [Core Tools v4 install guide](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools), [azd install guide](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd), [Azurite install guide](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite), and [uv install guide](https://docs.astral.sh/uv/getting-started/installation/). - ### 2. Get the resources you need `azd provision` creates the Foundry model deployment the agents require (needed even offline); `hydrate` copies the settings into `local.settings.json`. No API keys - managed identity throughout. @@ -73,14 +58,13 @@ azd provision ### 3. Run the local client ```bash -azurite --silent --skipApiVersionCheck --location .azurite # terminal A -uv run func start # terminal B -uv run python chat.py # terminal C +func5 run # terminal A (v5 auto-starts Azurite) +uv run python chat.py # terminal B ``` (These commands work identically on macOS, Linux, and Windows.) -> 🪟 **Windows local dev:** If `uv run func start` fails with `ModuleNotFoundError: No module named 'azure_functions_agents'`, the Microsoft Store python.exe alias on your `PATH` is shadowing the venv Python. See [Troubleshooting: Windows local dev](docs/troubleshooting.md#windows-local-dev) for the fix - you'll need to remove the Store Python and disable App execution aliases in Windows Settings. +> 🪟 **Windows local dev:** If `func5 run` fails with `ModuleNotFoundError: No module named 'azure_functions_agents'`, the Microsoft Store python.exe alias on your `PATH` is shadowing the venv Python. See [Troubleshooting: Windows local dev](docs/troubleshooting.md#windows-local-dev) for the fix - you'll need to remove the Store Python and disable App execution aliases in Windows Settings. ### 4. Try it (offline, safe) @@ -93,12 +77,16 @@ Want it to act on your real inbox while still local? See [Go live with real M365 Set who real mail and Teams posts go to, **before** you deploy. Without these, the deployed agents stay in DRY RUN (the chat client's doctor banner will tell you). ```bash -azd env set MAILBOX_OWNER_EMAIL you@your-tenant.com # required for LIVE mail -azd env set TEAMS_TEAM_ID # optional, enables Teams alerts -azd env set TEAMS_CHANNEL_ID # optional, enables Teams alerts +azd env set MAILBOX_OWNER_EMAIL you@your-tenant.com # required for LIVE mail +azd env set TEAMS_TEAM_ID # optional, enables Teams alerts +azd env set TEAMS_CHANNEL_ID # optional, enables Teams alerts +azd env set TEAMS_MENTION_USER_ID "$(az ad signed-in-user show --query id -o tsv)" # @mentions you on urgent alerts +azd env set TEAMS_MENTION_NAME "$(az ad signed-in-user show --query displayName -o tsv)" # display name for the @mention ``` -Get the Teams ids by opening the target channel in Teams → ⋯ → **Get link to channel** (the URL contains both ids), or via [docs/configuration.md](docs/configuration.md). +Get the Teams ids by opening the target channel in Teams → ⋯ → **Get link to channel** (the URL contains both `groupId=` → `TEAMS_TEAM_ID` and `19:...@thread.tacv2` → `TEAMS_CHANNEL_ID`). + +Or run `./infra/scripts/discover-teams-ids.ps1` (or `.sh`) to print all four `azd env set` lines pre-filled. More in [docs/configuration.md](docs/configuration.md). > Already deployed with placeholders? Set them now and re-run `azd up` (or just `azd provision`) to push the new values to the Function App. diff --git a/chat.py b/chat.py index 5e6cb3b..0a4f95a 100644 --- a/chat.py +++ b/chat.py @@ -153,10 +153,9 @@ def _print_host_unreachable_hint() -> None: print(" or check the Function App is running in the Azure portal.") return print(" The local Functions host is not running. Either:") - print(" A) Run it locally (3 terminals):") - print(" terminal A: azurite --silent --location ~/.azurite") - print(" terminal B: uv run func start") - print(" terminal C: uv run python chat.py") + print(" A) Run it locally (2 terminals; v5 auto-starts Azurite):") + print(" terminal A: func5 run") + print(" terminal B: uv run python chat.py") print(" B) Or call the deployed cloud Function App from this shell:") print(" source ./infra/scripts/use-cloud-host.sh") print(" uv run python chat.py") @@ -622,7 +621,7 @@ def _render_result(agent_name: str, result: dict, elapsed: float) -> None: print(" - Teams post with empty TEAMS_TEAM_ID / TEAMS_CHANNEL_ID") print(" - 3 consecutive failures trip the runtime's circuit breaker, which") print(" stops further tool calls but still returns a (partial) summary.") - print(" Run `uv run func start --verbose` to see the exact connector error.") + print(" Run `func5 run --verbose` to see the exact connector error.") live_blocked = mode != "dry_run" and re.search( r"could not read|forbidden|unauthorized|not authoriz", response_text, re.IGNORECASE @@ -662,7 +661,7 @@ def trigger_agent(agent_name: str, mode_icon: str, mode_label: str = "") -> None print(" Check that:") print(" - the .agent.md frontmatter has builtin_endpoints.chat_api: true") print(" - host.json extensions.http.routePrefix matches (this client reads it)") - print(" - you restarted `uv run func start` after editing either file") + print(" - you restarted `func5 run` after editing either file") if details: print(f" {details}") print() @@ -727,7 +726,7 @@ def mark(key: str) -> str: print(" Next step to LIVE: `azd env set MAILBOX_OWNER_EMAIL you@your-tenant.com`,") print(" then `./infra/scripts/hydrate-local-settings.sh`. If you have not already") print(" authorized the Outlook connection, also run (one time)") - print(" `./infra/scripts/authorize-connectors.sh`, then restart `uv run func start`.") + print(" `./infra/scripts/authorize-connectors.sh`, then restart `func5 run`.") elif not _teams_alerts_enabled(): print(" Outlook is LIVE. Set TEAMS_TEAM_ID / TEAMS_CHANNEL_ID to enable Teams alerts") print(" (inbox escalations and the daily briefing's urgent post).") @@ -921,7 +920,7 @@ def chat_with_inbox() -> None: if exc.code == 404: print(f" No agent responded at {chat_url(CHAT_AGENT[0])}") print(" Check inbox-chat.agent.md has builtin_endpoints.chat_api: true and") - print(" that you restarted `uv run func start` after adding it.") + print(" that you restarted `func5 run` after adding it.") if details: print(f" {details}") print() diff --git a/docs/configuration.md b/docs/configuration.md index f0370de..0b52c99 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -12,7 +12,14 @@ Run the agents against your real Outlook + Teams while still developing locally: azd env set MAILBOX_OWNER_EMAIL you@your-tenant.com ./infra/scripts/hydrate-local-settings.sh ./infra/scripts/authorize-connectors.sh -# Ctrl-C the func host, then `uv run func start` again +# Ctrl-C the func host, then `func5 run` again +``` + +Want Teams alerts to @mention you (not just post into the channel)? Inline `az` lookups so you don't have to copy ids: + +```bash +azd env set TEAMS_MENTION_USER_ID "$(az ad signed-in-user show --query id -o tsv)" +azd env set TEAMS_MENTION_NAME "$(az ad signed-in-user show --query displayName -o tsv)" ``` `authorize-connectors.sh` is **required** the first time you go live: it runs a one-time OAuth consent so the connector namespace can read your mailbox and send on your behalf. It opens a browser tab per connection and waits until the connection reports `Connected`. Setting the env vars alone is not enough. Until consent completes, LIVE runs fail with `could not read inbox` (the agent stops and sends nothing). Re-running the script is safe; already-authorized connections are skipped. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 72a936e..4fbf026 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -5,18 +5,26 @@ ## Local setup - **`Port 7071 is unavailable`**. Another `func` is still running. `lsof -nP -iTCP:7071 -sTCP:LISTEN` to find the PID, then `kill `. -- **`ModuleNotFoundError: agent_functions`**. Core Tools picked a Python worker that can't see the venv. Always start with `uv run func start`, not bare `func start`. `uv run` prepends `.venv/bin` so the 3.13 worker is selected. -- **`Connection refused 127.0.0.1:10000`**. Azurite isn't running. Start it in another terminal. -- **`InvalidHeaderValue` + `The API version ... is not supported by Azurite`**. Your SDK is using a newer Storage API than Azurite supports. Upgrade Azurite and start it with `azurite --silent --skipApiVersionCheck --location .azurite`. -- **`No installed bundle workload satisfies Microsoft.Azure.Functions.ExtensionBundle.Preview`**. You're on Core Tools v5 preview. v5 can't load the Preview bundle yet ([tracking issue #5309](https://github.com/Azure/azure-functions-core-tools/issues/5309)). Stay on v4. -- **Worker exits with SIGTERM 143 on startup**. Core Tools < 4.12.0 ships only a Python 3.12 worker. `brew upgrade azure-functions-core-tools@4` to ≥ 4.12.0. +- **`ModuleNotFoundError: agent_functions`**. The Functions host picked a Python worker that can't see the venv. `func5 run` (not bare `func5 start`) detects the uv-managed venv via `pyproject.toml` and selects the 3.13 worker. If you're on a shell where `func5` can't find the venv, `uv run func5 run` is the explicit fallback. +- **Worker exits with SIGTERM 143 on startup**. The Python worker on your machine is older than 3.13. Re-run `func5 setup --features python` to pick up the current preview worker. +- **`No installed bundle workload satisfies Microsoft.Azure.Functions.ExtensionBundle.Preview`**. The extension bundles workload is missing from your v5 install. Run `func5 workload install azure.functions.cli.workloads.extensionbundles` (or `func5 setup --features python`, which installs it as part of the bundle). This was [Azure/azure-functions-core-tools#5309](https://github.com/Azure/azure-functions-core-tools/issues/5309) on early v5 previews and is fixed in `bundles@4.42.0-preview.2` and later. - **Live mode: `403 Forbidden` from MCP**. The connector connection isn't authorized for the signed-in identity. Re-run `./infra/scripts/authorize-connectors.sh` and complete the browser consent for both Outlook and Teams. - **Live mode: agent returns `could not read inbox`**. The Outlook connection has not completed OAuth consent (its status is not `Connected`). Run `./infra/scripts/authorize-connectors.sh`, finish the browser consent as the mailbox owner, wait for `Connected`, then retry. This is a one-time step per connection; env vars alone do not authorize it. - **Windows PowerShell hydrate**. Use `pwsh -File ./infra/scripts/hydrate-local-settings.ps1` (skips ExecutionPolicy without `Set-ExecutionPolicy`). +## Still using v4 + +If you have an existing Azure Functions Core Tools v4 install (`brew install azure-functions-core-tools@4` on macOS, or the v4 installer on Windows / Linux) and don't want to add v5 yet, you can still run this template: + +- Substitute `func` for `func5` everywhere in this repo (`uv run func start`, etc.). +- Start Azurite yourself in a separate terminal before `func start`: `azurite --silent --skipApiVersionCheck --location .azurite`. v4 does not auto-launch it (v5 does, if `azurite` is on your `PATH`). +- Make sure your Core Tools is ≥ 4.12.0 so the Python 3.13 worker ships. + +The v5 path is recommended because it removes the Azurite terminal and ships the extension bundles workload that this template depends on. Once v5 reaches GA, this fallback section goes away. + ## Windows local dev -**Symptom:** `uv run func start` or `func start` fails with: +**Symptom:** `func5 run` fails with: ``` ModuleNotFoundError: No module named 'azure_functions_agents' ``` @@ -55,7 +63,7 @@ ModuleNotFoundError: No module named 'pydantic_core._pydantic_core' 4. **Retry:** ```powershell - uv run func start + func5 run ``` **Note on `hydrate-local-settings.ps1`:** The provisioning script can also set `languageWorkers__python__defaultExecutablePath` to point at your venv Python, which helps — but this mitigation is **not sufficient alone**. The OS-level `PATH` search still happens first, so you must still remove the Store Python and disable aliases. Both steps together ensure the 3.13 worker loads correctly. @@ -68,7 +76,7 @@ ModuleNotFoundError: No module named 'pydantic_core._pydantic_core' | --- | --- | | Connector authorization fails | Reopen the Connector Namespace portal URL from deployment outputs, sign in with the mailbox/channel owner, and reauthorize Outlook and Teams. | | MCP endpoint missing | Run `azd env get-values` and confirm `OUTLOOK_MCP_ENDPOINT` and `TEAMS_MCP_ENDPOINT` are populated. If blank, rerun `azd up` and check Connector Namespace deployment logs. | -| Timer is not firing | Confirm the Functions host shows the timer trigger loaded at startup. The v5 CLI starts Azurite automatically; pass `--no-azurite` only if you intentionally point `AzureWebJobsStorage` elsewhere. | +| Timer is not firing | Confirm the Functions host shows the timer trigger loaded at startup. `func5 run` launches Azurite automatically if it's on your `PATH`; pass `--no-azurite` only if you intentionally point `AzureWebJobsStorage` elsewhere. | | Local run cannot reach Azure | Leave the MCP endpoint variables blank and use `chat.py`; every agent runs DRY RUN and prints its deliverable as text. Option 4 shows what's missing to go live. | | Manual trigger returns 404 | Confirm the Functions host is running and agent function names are `inbox-triage`, `daily-briefing`, and `weekly-rule-suggestions`. | | Deploy fails: `azure-functions==2.1.0 ... No matching distribution found` | The Flex remote build is using Python 3.11.8 instead of 3.13. Use the pre-built deploy workaround in [deploy-python-313.md](deploy-python-313.md). Tracking: [Azure/azure-dev#8538](https://github.com/Azure/azure-dev/issues/8538). | diff --git a/function_app.py b/function_app.py index 07ac3f4..58a0188 100644 --- a/function_app.py +++ b/function_app.py @@ -73,7 +73,7 @@ def _resolve(key: str) -> str: banner.append("Fix: edit local.settings.json, or run:") for key, _ in issues: banner.append(f" azd env set {key} ") - banner.append("Then re-run `./infra/scripts/hydrate-local-settings.sh` and restart `uv run func start`.") + banner.append("Then re-run `./infra/scripts/hydrate-local-settings.sh` and restart `func5 run`.") banner.append("=" * 78) print("\n".join(banner), file=sys.stderr, flush=True) diff --git a/infra/scripts/authorize-connectors.ps1 b/infra/scripts/authorize-connectors.ps1 index 3275685..2ee4836 100644 --- a/infra/scripts/authorize-connectors.ps1 +++ b/infra/scripts/authorize-connectors.ps1 @@ -4,7 +4,7 @@ # # Safe to re-run: already-authorized connections are skipped. # Works without a deployed function app. Runs against the connector namespace -# directly so local `uv run func start` can call real MCP tools. +# directly so local `func5 run` can call real MCP tools. # # Prereq: the `connector-namespace` Azure CLI extension. Install once with: # az extension add --source