Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand Down
1 change: 0 additions & 1 deletion templates/langchain/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})

9 changes: 4 additions & 5 deletions templates/langchain/app/routes/arcade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down
18 changes: 12 additions & 6 deletions templates/langchain/app/routes/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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://<team>.slack.com/archives/<channel>/p<ts>)\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/<threadId>)\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"
Expand Down Expand Up @@ -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"
Expand All @@ -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):
Expand All @@ -246,17 +248,21 @@ 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 (
s[:MAX_TOOL_RESULT_CHARS]
+ f"\n...[truncated {len(s) - MAX_TOOL_RESULT_CHARS} chars]"
)
return result

updates["func"] = _truncated_invoke

return tool.model_copy(update=updates)
Expand Down
Loading