diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 772203e..fa3bf3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,10 @@ jobs: ai_sdk: ${{ steps.filter.outputs.ai_sdk }} mastra: ${{ steps.filter.outputs.mastra }} langchain: ${{ steps.filter.outputs.langchain }} + # JSON array of templates to smoke test. The `matrix` context is not + # available in a job-level `if:`, so we compute the matrix here instead + # of gating individual matrix legs downstream. + templates: ${{ steps.templates.outputs.list }} steps: - uses: actions/checkout@v4 - name: Detect changed paths @@ -42,6 +46,19 @@ jobs: - "templates/mastra/**" langchain: - "templates/langchain/**" + - name: Compute template matrix + id: templates + run: | + if [ "${{ steps.filter.outputs.core }}" = "true" ]; then + echo 'list=["ai-sdk","mastra","langchain"]' >> "$GITHUB_OUTPUT" + else + items=() + [ "${{ steps.filter.outputs.ai_sdk }}" = "true" ] && items+=('"ai-sdk"') + [ "${{ steps.filter.outputs.mastra }}" = "true" ] && items+=('"mastra"') + [ "${{ steps.filter.outputs.langchain }}" = "true" ] && items+=('"langchain"') + joined=$(IFS=,; echo "${items[*]}") + echo "list=[$joined]" >> "$GITHUB_OUTPUT" + fi lint-and-build: name: Lint, Format & Build @@ -57,8 +74,8 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 22 - cache: npm - - run: npm ci + - uses: oven-sh/setup-bun@v2 + - run: bun install --frozen-lockfile - run: npm run lint - run: npm run format:check - run: npm run typecheck @@ -88,7 +105,7 @@ jobs: name: Smoke Test — ${{ matrix.template }} runs-on: ubuntu-latest needs: [changes, lint-and-build] - if: ${{ needs.changes.outputs.core == 'true' || (matrix.template == 'ai-sdk' && needs.changes.outputs.ai_sdk == 'true') || (matrix.template == 'mastra' && needs.changes.outputs.mastra == 'true') || (matrix.template == 'langchain' && needs.changes.outputs.langchain == 'true') }} + if: ${{ needs.changes.outputs.templates != '[]' }} env: NPM_CONFIG_AUDIT: "false" NPM_CONFIG_FUND: "false" @@ -97,14 +114,13 @@ jobs: strategy: fail-fast: false matrix: - template: [ai-sdk, mastra, langchain] + template: ${{ fromJSON(needs.changes.outputs.templates) }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - uses: oven-sh/setup-bun@v2 - if: matrix.template != 'langchain' with: bun-version: latest - name: Cache bun packages @@ -122,7 +138,7 @@ jobs: python-version: "3.12" cache: pip cache-dependency-path: templates/langchain/requirements.txt - - run: npm ci + - run: bun install --frozen-lockfile - name: Download built CLI artifact uses: actions/download-artifact@v4 with: @@ -136,14 +152,29 @@ jobs: working-directory: ci-test-${{ matrix.template }} run: cp .env.example .env + - name: Install Python deps (Python template) + if: matrix.template == 'langchain' + working-directory: ci-test-${{ matrix.template }} + run: pip install -r requirements.txt + + # doctor validates runtime config (gateway URL + LLM key) and pings the + # gateway, so it exits 1 on an unconfigured scaffold. Feed it dummy CI + # values so we exercise its success path. example.com is reliably + # reachable and satisfies the reachability check (returns 200). - name: Doctor (Next.js + TypeScript templates) if: matrix.template != 'langchain' working-directory: ci-test-${{ matrix.template }} + env: + ARCADE_GATEWAY_URL: https://example.com + OPENAI_API_KEY: sk-ci-smoke-test run: bun run doctor - name: Doctor (Python template) if: matrix.template == 'langchain' working-directory: ci-test-${{ matrix.template }} + env: + ARCADE_GATEWAY_URL: https://example.com + OPENAI_API_KEY: sk-ci-smoke-test run: python -m app.doctor - name: Type check (Next.js + TypeScript templates) diff --git a/templates/langchain/app/main.py b/templates/langchain/app/main.py index abe801c..6564b07 100644 --- a/templates/langchain/app/main.py +++ b/templates/langchain/app/main.py @@ -72,4 +72,3 @@ async def dashboard_page(request: Request, db: AsyncSession = Depends(get_db)): if not user: return RedirectResponse("/") return templates.TemplateResponse(request, "dashboard.html", {"user": user}) - diff --git a/templates/langchain/app/routes/arcade.py b/templates/langchain/app/routes/arcade.py index 8884228..2202de6 100644 --- a/templates/langchain/app/routes/arcade.py +++ b/templates/langchain/app/routes/arcade.py @@ -79,8 +79,7 @@ async def connect(request: Request, db: AsyncSession = Depends(get_db)): { "connected": False, "error": ( - "Cannot reach Arcade Gateway. " - "Check ARCADE_GATEWAY_URL and try again." + "Cannot reach Arcade Gateway. Check ARCADE_GATEWAY_URL and try again." ), }, status_code=502, @@ -123,9 +122,9 @@ def _extract_auth_url_from_result(content: str) -> str | None: """Check if a tool result contains an Arcade authorization URL.""" try: parsed = json.loads(content) - url = parsed.get("authorization_url") or ( - parsed.get("structuredContent") or {} - ).get("authorization_url") + url = parsed.get("authorization_url") or (parsed.get("structuredContent") or {}).get( + "authorization_url" + ) if url: return url except (json.JSONDecodeError, AttributeError): diff --git a/templates/langchain/app/routes/plan.py b/templates/langchain/app/routes/plan.py index ef92375..069c3a4 100644 --- a/templates/langchain/app/routes/plan.py +++ b/templates/langchain/app/routes/plan.py @@ -110,15 +110,15 @@ def _build_plan_prompt(): "```\n\n" "URL RULES:\n" "Prefer a direct deep link to the item itself:\n" - "- Slack: use the \"permalink\" field if present" + '- Slack: use the "permalink" field if present' " (https://.slack.com/archives//p)\n" "- GitHub: use the issue or PR URL on github.com\n" "- Linear: use the Linear issue URL\n" "- Gmail: use the Gmail thread URL (https://mail.google.com/mail/u/0/#inbox/)\n" - "- Google Calendar: use the \"htmlLink\" field if present\n" + '- Google Calendar: use the "htmlLink" field if present\n' "If no direct deep link is available, fall back to the most relevant URL" " found anywhere in the tool response.\n" - "Only omit \"url\" if there is truly no URL available in the response at all.\n\n" + 'Only omit "url" if there is truly no URL available in the response at all.\n\n' "Rules:\n" "- One json:task block per ACTIONABLE item (skip empty results, metadata, and errors)\n" "- Brief status text between blocks is fine\n" @@ -216,7 +216,8 @@ async def stream(): # Uses pattern matching so it works regardless of Arcade's exact naming # convention (underscores, dots, or mixed casing are all handled). _KNOWN_SERVICE = re.compile( - r"^(github|gmail|google|calendar|linear|slack)", re.IGNORECASE, + r"^(github|gmail|google|calendar|linear|slack)", + re.IGNORECASE, ) _MUTATION = re.compile( r"create|update|delete|send|reply|post|archive|remove" @@ -228,6 +229,7 @@ def _is_triage_tool(name: str) -> bool: if not _KNOWN_SERVICE.search(name): return False return not _MUTATION.search(name) + MAX_TOOL_RESULT_CHARS = 4000 def _wrap_tool(tool): @@ -246,10 +248,13 @@ async def _truncated_ainvoke(*args, **kwargs): updates = {"coroutine": _truncated_ainvoke} if original_func is not None: + def _truncated_invoke(*args, **kwargs): result = original_func(*args, **kwargs) - s = result if isinstance(result, str) else ( - json.dumps(result) if result else "" + s = ( + result + if isinstance(result, str) + else (json.dumps(result) if result else "") ) if len(s) > MAX_TOOL_RESULT_CHARS: return ( @@ -257,6 +262,7 @@ def _truncated_invoke(*args, **kwargs): + f"\n...[truncated {len(s) - MAX_TOOL_RESULT_CHARS} chars]" ) return result + updates["func"] = _truncated_invoke return tool.model_copy(update=updates)