Skip to content
Open
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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- [CLI reference](#cli-reference)
- [How photo ID works](#how-photo-id-works)
- [App care report](#app-care-report)
- [App SDK contracts](#app-sdk-contracts)
- [Species catalog](#species-catalog)
- [Diagrams](#diagrams)
- [Repository layout](#repository-layout)
Expand Down Expand Up @@ -192,6 +193,32 @@ plantguide demo photo -i data/samples/photos/monstera_demo.jpg --out data/out/ph

Reports include `query_tags` / image path, ranked `matches`, `care` card, and (for photo demo) `watering` + optional `care_svg`.

## App SDK contracts

PlantGuide publishes stable app-integration contracts under `schemas/`:

| File | Purpose |
| --- | --- |
| `schemas/plantguide-care-report.schema.json` | JSON Schema for `plantguide.app.care_report.v1` payloads emitted by `plantguide.integrations.sdk` |
| `schemas/plantguide-sdk.d.ts` | TypeScript interfaces for app clients consuming care reports and care cards |

The schema is checked against real Python SDK payloads in CI.

```powershell
plantguide app demo --sample data/samples/obs_monstera.json --out data/out/e2e-monstera-care-report.json
```

TypeScript clients can vendor or import the declarations:

```ts
import type { PlantGuideCareReport } from "./schemas/plantguide-sdk";

function renderCare(report: PlantGuideCareReport) {
if (!report.ready_for_ui || !report.care_card) return;
console.log(report.care_card.common_name, report.care_card.water);
}
```

---

## Species catalog
Expand Down
179 changes: 179 additions & 0 deletions schemas/plantguide-care-report.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://mergeos-bounties.github.io/PlantGuide/schemas/plantguide-care-report.schema.json",
"title": "PlantGuide app care report",
"description": "Stable JSON contract emitted by plantguide.integrations.sdk care report helpers.",
"type": "object",
"additionalProperties": false,
"required": [
"report_type",
"integration_version",
"ready_for_ui",
"sample_id",
"source",
"query_tags",
"expected_species",
"hit_top1",
"top_species_id",
"top_match",
"matches",
"care_card",
"license_safe_evidence"
],
"properties": {
"report_type": {
"const": "plantguide.app.care_report.v1"
},
"integration_version": {
"const": "plantguide.sdk.v1"
},
"ready_for_ui": {
"type": "boolean"
},
"sample_id": {
"type": ["string", "null"]
},
"source": {
"type": ["string", "null"]
},
"query_tags": {
"type": "array",
"items": {
"type": "string"
}
},
"expected_species": {
"type": ["string", "null"]
},
"hit_top1": {
"type": ["boolean", "null"]
},
"top_species_id": {
"type": ["string", "null"]
},
"top_match": {
"$ref": "#/$defs/match"
},
"matches": {
"type": "array",
"items": {
"$ref": "#/$defs/match"
}
},
"care_card": {
"$ref": "#/$defs/care_card"
},
"license_safe_evidence": {
"type": "object",
"additionalProperties": false,
"required": ["source", "source_type", "external_assets"],
"properties": {
"source": {
"type": "string"
},
"source_type": {
"enum": ["sample_fixture", "inline_tags"]
},
"external_assets": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
},
"$defs": {
"match": {
"type": ["object", "null"],
"required": ["species_id", "common_name", "scientific_name", "score", "matched_tags"],
"properties": {
"species_id": {
"type": "string"
},
"common_name": {
"type": "string"
},
"scientific_name": {
"type": "string"
},
"score": {
"type": "number"
},
"matched_tags": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": true
},
"care_card": {
"type": ["object", "null"],
"required": [
"species_id",
"common_name",
"scientific_name",
"summary",
"light",
"water",
"soil",
"humidity",
"temperature_c",
"fertilizer",
"toxicity",
"common_issues",
"tips"
],
"properties": {
"species_id": {
"type": "string"
},
"common_name": {
"type": "string"
},
"scientific_name": {
"type": "string"
},
"summary": {
"type": "string"
},
"light": {
"type": "string"
},
"water": {
"type": "string"
},
"soil": {
"type": "string"
},
"humidity": {
"type": "string"
},
"temperature_c": {
"type": "string"
},
"fertilizer": {
"type": "string"
},
"toxicity": {
"type": "string"
},
"common_issues": {
"type": "array",
"items": {
"type": "string"
}
},
"tips": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": true
}
}
}
58 changes: 58 additions & 0 deletions schemas/plantguide-sdk.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export interface PlantGuideMatch {
species_id: string;
common_name: string;
scientific_name: string;
score: number;
matched_tags: string[];
[key: string]: unknown;
}

export interface PlantGuideCareCard {
species_id: string;
common_name: string;
scientific_name: string;
summary: string;
light: string;
water: string;
soil: string;
humidity: string;
temperature_c: string;
fertilizer: string;
toxicity: string;
common_issues: string[];
tips: string[];
[key: string]: unknown;
}

export interface PlantGuideLicenseSafeEvidence {
source: string;
source_type: "sample_fixture" | "inline_tags";
external_assets: string[];
}

export interface PlantGuideCareReport {
report_type: "plantguide.app.care_report.v1";
integration_version: "plantguide.sdk.v1";
ready_for_ui: boolean;
sample_id: string | null;
source: string | null;
query_tags: string[];
expected_species: string | null;
hit_top1: boolean | null;
top_species_id: string | null;
top_match: PlantGuideMatch | null;
matches: PlantGuideMatch[];
care_card: PlantGuideCareCard | null;
license_safe_evidence: PlantGuideLicenseSafeEvidence;
}

export declare function assessForApp(tags: string | string[], topK?: number): Promise<unknown>;
export declare function careForApp(speciesId: string): Promise<PlantGuideCareCard>;
export declare function careReportFromSample(
samplePath: string,
topK?: number,
): Promise<PlantGuideCareReport>;
export declare function careReportFromTags(
tags: string | string[],
topK?: number,
): Promise<PlantGuideCareReport>;
61 changes: 61 additions & 0 deletions tests/test_sdk_contracts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from plantguide.integrations.sdk import care_report_from_sample, care_report_from_tags

SCHEMA_PATH = Path("schemas/plantguide-care-report.schema.json")
TYPES_PATH = Path("schemas/plantguide-sdk.d.ts")


def test_care_report_schema_matches_sample_payload_contract() -> None:
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
report = care_report_from_sample(Path("data/samples/obs_monstera.json"), top_k=2)

assert set(schema["required"]) == set(report)
assert schema["properties"]["report_type"]["const"] == report["report_type"]
assert schema["properties"]["integration_version"]["const"] == report["integration_version"]
assert schema["properties"]["license_safe_evidence"]["properties"]["source_type"]["enum"]
_assert_required_keys(
schema["$defs"]["care_card"]["required"],
report["care_card"],
)
_assert_required_keys(
schema["$defs"]["match"]["required"],
report["top_match"],
)


def test_care_report_schema_matches_inline_tags_payload_contract() -> None:
schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8"))
report = care_report_from_tags("succulent,thick leaves,drought", top_k=1)

assert set(schema["required"]) == set(report)
assert report["sample_id"] is None
assert report["license_safe_evidence"]["source_type"] == "inline_tags"
_assert_required_keys(
schema["$defs"]["care_card"]["required"],
report["care_card"],
)
_assert_required_keys(
schema["$defs"]["match"]["required"],
report["top_match"],
)


def test_typescript_contract_exports_match_schema_names() -> None:
types = TYPES_PATH.read_text(encoding="utf-8")

assert "export interface PlantGuideCareReport" in types
assert "report_type: \"plantguide.app.care_report.v1\"" in types
assert "integration_version: \"plantguide.sdk.v1\"" in types
assert "source_type: \"sample_fixture\" | \"inline_tags\"" in types
assert "export interface PlantGuideCareCard" in types


def _assert_required_keys(required: list[str], payload: Any) -> None:
assert isinstance(payload, dict)
missing = set(required) - set(payload)
assert not missing