Template
Add IT-One AI agent template, eval gate, and deployment skeleton
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Copy to .env locally. Never commit real values.
|
||||
OPENAI_API_KEY=
|
||||
TAVILY_API_KEY=
|
||||
VECTOR_DB_URL=
|
||||
VECTOR_DB_TOKEN=
|
||||
REGISTRY_URL=
|
||||
REGISTRY_USER=
|
||||
REGISTRY_TOKEN=
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a reproducible agent or evaluation problem
|
||||
title: "[bug] "
|
||||
labels: bug
|
||||
---
|
||||
|
||||
## What happened?
|
||||
|
||||
## Reproduction
|
||||
|
||||
## Expected behavior
|
||||
|
||||
## Evaluation impact
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Propose a scoped capability or prompt change
|
||||
title: "[feature] "
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
## Proposed behavior
|
||||
|
||||
## Prompt, tool, or memory impact
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate-image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate deployment files
|
||||
run: test -f deploy/Dockerfile && test -f deploy/docker-compose.yml
|
||||
- name: Build image when registry credentials exist
|
||||
if: ${{ secrets.REGISTRY_URL != '' && secrets.REGISTRY_TOKEN != '' }}
|
||||
env:
|
||||
REGISTRY_URL: ${{ secrets.REGISTRY_URL }}
|
||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
echo "$REGISTRY_TOKEN" | docker login "$REGISTRY_URL" --username "${{ secrets.REGISTRY_USER }}" --password-stdin
|
||||
docker build -f deploy/Dockerfile -t "$REGISTRY_URL/agent:${{ gitea.sha }}" .
|
||||
docker push "$REGISTRY_URL/agent:${{ gitea.sha }}"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Eval Gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
evaluate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
- name: Run offline quality gate
|
||||
run: python3 evals/run_evals.py --threshold 0.95
|
||||
@@ -1,3 +1,28 @@
|
||||
# agent-template
|
||||
# IT-One AI Agent Template
|
||||
|
||||
Canonical public template for autonomous AI agents, prompts, tools, memory, evaluations, deployment, and CI/CD.
|
||||
A public, reusable starting point for autonomous agents and multi-agent systems. Generate a repository from this template, then rename it using the conventions below.
|
||||
|
||||
## Naming
|
||||
|
||||
- `agent-<name>` — autonomous agent.
|
||||
- `swarm-<name>` or `orch-<name>` — orchestrator or multi-agent system.
|
||||
- `eval-<agent-name>-benchmark` — dataset or quality benchmark.
|
||||
|
||||
## Layout
|
||||
|
||||
Prompts are versioned separately from implementation. `src/tools` contains integrations, `src/memory` contains retrieval/state adapters, `src/core` contains the agent graph, and `evals` contains deterministic quality checks.
|
||||
|
||||
## Quality gate
|
||||
|
||||
Every pull request runs `.gitea/workflows/eval-quality.yml`. The evaluator must keep Accuracy and Tool Calling Success at or above **95%**. A failing evaluator blocks the protected `main` branch. The sample evaluator is offline-safe; replace its cases and assertions for a real agent.
|
||||
|
||||
## Secrets
|
||||
|
||||
Never commit provider or database credentials. Configure organization-level Actions secrets in Gitea and reference them as `${{ secrets.OPENAI_API_KEY }}`, `${{ secrets.TAVILY_API_KEY }}`, `${{ secrets.VECTOR_DB_URL }}`, and `${{ secrets.REGISTRY_TOKEN }}` only when a project actually needs them.
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
python -m evals.run_evals --threshold 0.95
|
||||
python -m src.main
|
||||
```
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
CMD ["python", "-m", "src.main"]
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
agent:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile
|
||||
env_file:
|
||||
- ../.env
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,14 @@
|
||||
# System architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[User request] --> Core[Agent graph]
|
||||
Core --> Prompt[Versioned prompts]
|
||||
Core --> Tools[External tools]
|
||||
Core --> Memory[Memory / RAG]
|
||||
Core --> Output[Validated response]
|
||||
Evals[Offline evals] --> Gate[PR quality gate]
|
||||
Gate --> Core
|
||||
```
|
||||
|
||||
The core must keep orchestration deterministic enough to test. Tools should have explicit schemas and timeouts. Memory adapters must make retention and deletion behavior visible.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Tool definitions
|
||||
|
||||
Every tool should document its input schema, output schema, timeout, retry policy, authentication source, and failure mode. Production credentials come from Gitea Actions secrets, never from source files.
|
||||
|
||||
| Tool | Purpose | Failure policy |
|
||||
| --- | --- | --- |
|
||||
| CRM client | Read or update customer records | Fail closed; redact identifiers |
|
||||
| Web search | Retrieve public references | Timeout and cite sources |
|
||||
@@ -0,0 +1,8 @@
|
||||
def score_case(case: dict, result: dict) -> tuple[bool, str]:
|
||||
answer = str(result.get("answer", ""))
|
||||
expected = str(case.get("expected_answer_contains", ""))
|
||||
if expected and expected.lower() not in answer.lower():
|
||||
return False, "answer substring mismatch"
|
||||
if len(result.get("tool_calls", [])) != int(case.get("expected_tool_calls", 0)):
|
||||
return False, "tool call count mismatch"
|
||||
return True, "ok"
|
||||
@@ -0,0 +1,20 @@
|
||||
import argparse, json, pathlib, sys
|
||||
from src.core.agent import handle
|
||||
from evals.assert_rules import score_case
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--threshold", type=float, default=0.95)
|
||||
args = parser.parse_args()
|
||||
cases = [json.loads(line) for line in pathlib.Path("evals/test_cases.jsonl").read_text().splitlines() if line.strip()]
|
||||
passed = 0
|
||||
for case in cases:
|
||||
ok, reason = score_case(case, handle(case["input"]))
|
||||
passed += int(ok)
|
||||
print(f"{case['id']}: {'PASS' if ok else 'FAIL'} ({reason})")
|
||||
score = passed / len(cases) if cases else 0.0
|
||||
print(f"accuracy={score:.3f} threshold={args.threshold:.3f}")
|
||||
return 0 if score >= args.threshold else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,5 @@
|
||||
{"id":"greeting","input":"Hello","expected_answer_contains":"Hello","expected_tool_calls":0}
|
||||
{"id":"empty-safe","input":" ","expected_answer_contains":"","expected_tool_calls":0}
|
||||
{"id":"explicit-request","input":"List the next step","expected_answer_contains":"List","expected_tool_calls":0}
|
||||
{"id":"no-invention","input":"Return the supplied value","expected_answer_contains":"supplied","expected_tool_calls":0}
|
||||
{"id":"traceable","input":"Show the request","expected_answer_contains":"request","expected_tool_calls":0}
|
||||
@@ -0,0 +1,3 @@
|
||||
[
|
||||
{"user":"Summarize the account status.","assistant":"I will inspect the documented account fields and cite the source of each result."}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
# Extract data
|
||||
|
||||
Return a structured object with `facts`, `missing_fields`, and `source_refs`. Keep user-provided values distinct from inferred values.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Qualify
|
||||
|
||||
1. Identify the request and desired outcome.
|
||||
2. Check authorization and irreversible effects.
|
||||
3. Select only the tools needed.
|
||||
4. Record assumptions for evaluation.
|
||||
@@ -0,0 +1,3 @@
|
||||
# System prompt
|
||||
|
||||
You are a reliable IT-One assistant. Follow the user request, state uncertainty, protect private data, and use tools only when their documented purpose applies. Never invent tool results, credentials, or citations. Ask for clarification when an irreversible action is ambiguous.
|
||||
@@ -0,0 +1,9 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=68"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "it-one-agent"
|
||||
version = "0.1.0"
|
||||
description = "IT-One agent template"
|
||||
requires-python = ">=3.11"
|
||||
@@ -0,0 +1 @@
|
||||
# Add provider SDKs only when the agent needs them.
|
||||
@@ -0,0 +1,5 @@
|
||||
from .state import AgentState
|
||||
|
||||
def handle(request: str) -> dict:
|
||||
state = AgentState(request=request)
|
||||
return {"answer": request.strip(), "facts": state.facts, "tool_calls": state.tool_calls}
|
||||
@@ -0,0 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class AgentState:
|
||||
request: str
|
||||
facts: list[str] = field(default_factory=list)
|
||||
tool_calls: list[str] = field(default_factory=list)
|
||||
@@ -0,0 +1,7 @@
|
||||
from src.core.agent import handle
|
||||
|
||||
def main() -> None:
|
||||
print(handle("offline example"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Memory interface. Production implementations must document retention and deletion."""
|
||||
|
||||
class VectorStore:
|
||||
def add(self, key: str, text: str) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def search(self, query: str, limit: int = 5) -> list[dict]:
|
||||
return []
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Minimal CRM adapter boundary; add a real client behind this interface."""
|
||||
|
||||
def get_customer(customer_id: str) -> dict:
|
||||
if not customer_id:
|
||||
raise ValueError("customer_id is required")
|
||||
raise NotImplementedError("configure a provider-backed implementation")
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Search adapter boundary with an explicit, testable result shape."""
|
||||
|
||||
def search(query: str, limit: int = 5) -> list[dict]:
|
||||
if not query.strip():
|
||||
return []
|
||||
return [{"title": "offline-placeholder", "url": "about:blank", "snippet": query[:200]}][:limit]
|
||||
Reference in New Issue
Block a user