diff --git a/.env.example b/.env.example index 9c7b55a..af88820 100644 --- a/.env.example +++ b/.env.example @@ -31,3 +31,9 @@ CRON_SECRET= IDEXX_API_KEY= ANTECH_API_KEY= ZOETIS_API_KEY= + +# OpenVPM Agent (AI). Without ANTHROPIC_API_KEY the agent UI shows a setup +# notice and agent runs are disabled; everything else works normally. +ANTHROPIC_API_KEY= +# Optional model override for the agent (defaults to claude-sonnet-4-6). +AGENT_MODEL= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 008b103..6695135 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,7 @@ jobs: - run: pnpm type-check + - run: pnpm test + - run: pnpm build # Note: next build includes linting — no separate lint step needed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8e9464..e51a07f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,16 +31,47 @@ Thank you for your interest in contributing to OpenVPM! 1. Create a feature branch from `main` 2. Make your changes -3. Run `pnpm build` to verify no type errors +3. Run `pnpm test` (unit) and `pnpm build` (type-check + lint) to verify 4. Submit a pull request +## Testing + +- **Unit tests** (Vitest): `pnpm test`. Co-locate as `*.test.ts` next to the code + (e.g. `lib/compat/openvpm/__tests__/`). Pure logic — mappers, helpers — should + be unit-tested without a database. +- **E2E tests** (Playwright): `pnpm test:e2e`. + ## Code Style - TypeScript strict mode - Tailwind CSS for styling (follow existing design tokens) -- tRPC for all API endpoints +- tRPC for all internal API endpoints - Drizzle ORM for database queries +## Adding a compatibility endpoint or target + +The public REST API ([docs/api](docs/api/README.md)) is OpenVPM's integration +moat: integrators (and AI agents) plug into a frozen, vendor-shaped contract. +The layout makes adding endpoints — and entire vendor-compatible "targets" — +repeatable: + +- **Route handlers**: `apps/web/app/api/v1//route.ts`. Keep them + **thin** — authenticate → validate → tenant-scoped Drizzle query → map → + respond. No business logic or shape knowledge in the handler. +- **Auth**: call `authenticateApiKey(req, "")` from `lib/api-auth.ts`. + **Every** query MUST be scoped by `ctx.practiceId` and filter + `isNull(table.deletedAt)` — a cross-tenant read is a security bug. +- **Mappers**: `lib/compat//mappers.ts` holds **pure** functions that + translate internal rows ↔ the target's shapes (enum crosswalks, date formats). + The target's request/response shapes live in `lib/compat//schema.ts`. +- **A new target** (e.g. an existing PIMS's public API) is a new + `lib/compat//` module plus an `app/api/compat//v1/` namespace — + the auth, pagination, and error helpers are shared. + +Every endpoint ships with mapper unit tests **and** a contract test +(`schema.parse(toApiX(row))`) so internal changes can't silently break the +public contract. + ## License By contributing, you agree that your contributions will be licensed under the MIT License. diff --git a/README.md b/README.md index 038ffcb..891b1a2 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,17 @@ The Docker setup includes PostgreSQL 16 with health checks, MinIO for S3-compati OpenVPM is **API-first**. Every action the UI performs goes through the same API available to third-party integrations. This is the killer feature — no other open-source PIMS has a real, well-documented, read-write API. +### REST API (v1) + +A versioned, public REST API over the core records, authenticated with scoped API keys — built so integrators (booking, reminders, client comms, AI agents) can read clients/patients and create appointments without touching the internal client. Response shapes are owned by an explicit contract and frozen independently of the database, so internal changes never break integrations. + +```bash +curl https://demo.openvpm.com/api/v1/clients \ + -H "Authorization: Bearer ovpm_…" +``` + +See [docs/api](docs/api/README.md) for endpoints, scopes, rate limits, and the error format. This namespace also serves as the foundation for vendor-compatible "identical-twin" APIs (point an existing integration at OpenVPM with zero changes). + ### Webhooks Subscribe to real-time events: @@ -256,6 +267,10 @@ OpenVPM's structured data models and event streams make it the ideal foundation The PIMS is the system of record. AI agents are first-class citizens. +### OpenVPM Agent + +OpenVPM ships with a built-in AI agent that operates on practice data through a typed tool layer (find clients/patients, pull a clinical summary, list overdue vaccinations, calculate a weight-based drug dose, book appointments). It runs a Claude tool-use loop scoped to a single practice, gates every write behind an explicit opt-in, and degrades gracefully when no model key is set. Bring your own `ANTHROPIC_API_KEY`; the agent is open source and fully inspectable. Available in-app under **Agent** and via the `agent` tRPC router. + ## Why Open Source Matters for Veterinary Medicine The veterinary industry is at a crossroads. AI is arriving. Data interoperability is becoming critical. And the dominant PIMS vendors are still charging hundreds per month for software that crashes, frustrates staff, and locks clinics into proprietary ecosystems. diff --git a/apps/web/app/(dashboard)/agent/page.tsx b/apps/web/app/(dashboard)/agent/page.tsx new file mode 100644 index 0000000..c2830eb --- /dev/null +++ b/apps/web/app/(dashboard)/agent/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useState } from "react"; +import { Bot, Send, ChevronDown, ChevronRight, AlertTriangle, Wrench } from "lucide-react"; +import { trpc } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; + +const SUGGESTIONS = [ + "Which patients are overdue for vaccinations?", + "Summarize today's appointments.", + "What's the carprofen dose for a 12 kg dog?", + "Pull a clinical summary for the next patient checked in.", +]; + +export default function AgentPage() { + const status = trpc.agent.status.useQuery(); + const run = trpc.agent.run.useMutation(); + const [instruction, setInstruction] = useState(""); + const [allowWrites, setAllowWrites] = useState(false); + const [traceOpen, setTraceOpen] = useState(false); + + const configured = status.data?.configured ?? true; + + function submit() { + if (!instruction.trim() || run.isPending) return; + run.mutate({ instruction: instruction.trim(), allowWrites }); + } + + return ( +
+
+
+ +
+
+

OpenVPM Agent

+

+ Ask the agent to work on your practice data. It uses real tools and + never invents records. +

+
+
+ + {!configured && ( +
+ +
+ The agent is not configured yet. Set ANTHROPIC_API_KEY{" "} + in your environment to enable agent runs. +
+
+ )} + +
+