From dfa7bb3bc4238994c7bfb37ba9cd744fd1de853f Mon Sep 17 00:00:00 2001 From: a2a-platform Date: Mon, 29 Jun 2026 01:34:41 +0000 Subject: [PATCH] deploy --- .a2a/agent.dsl.json | 4 +- README.md | 5 ++- a2a.yaml | 2 +- agent.py | 81 +++++++++++++++++++++++++++++++++++--- skills/activities/SKILL.md | 10 +++-- skills/courses/SKILL.md | 1 + 6 files changed, 90 insertions(+), 13 deletions(-) diff --git a/.a2a/agent.dsl.json b/.a2a/agent.dsl.json index 44f5e15..47f637a 100644 --- a/.a2a/agent.dsl.json +++ b/.a2a/agent.dsl.json @@ -3,7 +3,7 @@ "language": "python", "name": "learnhouse-agent", "description": "LearnHouse is an open-source platform tailored for learning experiences.", - "version": "1.2.12", + "version": "1.2.13", "entrypoint": { "module": "agent", "class_name": "LearnhouseAgent", @@ -172,7 +172,7 @@ "source": "python-a2a-pack", "project_manifest": { "name": "learnhouse-agent", - "version": "1.2.12", + "version": "1.2.13", "entrypoint": "agent:LearnhouseAgent" } } diff --git a/README.md b/README.md index a741590..8e68142 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,9 @@ Deployment context: using that chapter's own `chapter_id`. Do not attach all activities to the first chapter. - Visible lesson activities should normally use `TYPE_DYNAMIC` / - `SUBTYPE_DYNAMIC_MARKDOWN`, markdown content/details, `lock_type=public`, and - `published=true` unless the user explicitly asks for drafts. + `SUBTYPE_DYNAMIC_MARKDOWN`, `content.markdown_url`, `lock_type=public`, and + `published=true` unless the user explicitly asks for drafts. Inline markdown + is converted to a `data:text/markdown` URL when no external URL is available. - Activity updates must not send `published_version`; this LearnHouse backend currently rejects that field. - Do not use `GET /api/v1/health` as a preflight or blocker for API-token diff --git a/a2a.yaml b/a2a.yaml index 3c835fa..35cc352 100644 --- a/a2a.yaml +++ b/a2a.yaml @@ -1,5 +1,5 @@ name: learnhouse-agent -version: 1.2.12 +version: 1.2.13 entrypoint: agent:LearnhouseAgent description: LearnHouse is an open-source platform tailored for learning experiences. runtime: diff --git a/agent.py b/agent.py index 11ed2f7..0541df3 100644 --- a/agent.py +++ b/agent.py @@ -5,6 +5,7 @@ import json import re from pathlib import Path from typing import Any +from urllib.parse import quote import httpx from a2a_pack.deepagents import create_a2a_deep_agent @@ -53,8 +54,11 @@ LEARNHOUSE_CONTEXT = ( "create at least one activity in each chapter using that chapter's specific " "chapter_id. Do not attach all activities to the first chapter. For visible " "lesson content, create activities with activity_type TYPE_DYNAMIC, " - "activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, markdown content/details, " - "lock_type public, and published=true unless the user requests drafts. " + "activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, content.markdown_url pointing " + "to fetchable markdown, lock_type public, and published=true unless the " + "user requests drafts. The renderer ignores inline content.body and " + "content.markdown unless content.markdown_url is also configured; when no " + "external markdown URL is available, use a data:text/markdown URL. " "After writing course content, verify every chapter has at least one " "activity through course metadata or the chapter activity endpoint. When " "updating activities, do not send published_version; this LearnHouse " @@ -77,7 +81,7 @@ class OperationInput(BaseModel): class LearnhouseAgent(A2AAgent): name = "learnhouse-agent" description = "LearnHouse is an open-source platform tailored for learning experiences." - version = "1.2.12" + version = "1.2.13" consumer_setup = ConsumerSetup.from_fields( ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://learnhouse.a2acloud.io).", required=False, input_type="url"), ConsumerSetupField.secret( @@ -309,8 +313,10 @@ class LearnhouseAgent(A2AAgent): parts.append( "For LearnHouse activity creation, send JSON fields name, " "chapter_id, activity_type, activity_sub_type, content, " - "details, lock_type, and published. Use the target " - "chapter's own chapter_id; do not reuse the first chapter ID." + "details, lock_type, and published. Markdown activities " + "must include content.markdown_url. If only inline markdown " + "is available, this agent converts it to a data:text/markdown URL. " + "Use the target chapter's own chapter_id; do not reuse the first chapter ID." ) if _is_learnhouse_update_activity(operation): parts.append( @@ -348,6 +354,7 @@ class LearnhouseAgent(A2AAgent): body = json.loads(stripped) except ValueError: pass + body = _normalize_learnhouse_activity_body(operation, body) if _operation_uses_multipart(operation): request_kwargs["data"] = body else: @@ -540,6 +547,70 @@ def _is_learnhouse_update_activity(operation: dict[str, Any]) -> bool: ) +def _normalize_learnhouse_activity_body(operation: dict[str, Any], body: Any) -> Any: + if not ( + _is_learnhouse_create_activity(operation) + or _is_learnhouse_update_activity(operation) + ): + return body + if not isinstance(body, dict): + return body + + normalized = dict(body) + if _is_learnhouse_update_activity(operation): + normalized.pop("published_version", None) + + content = normalized.get("content") + if not isinstance(content, dict): + content = {} + else: + content = dict(content) + + details = normalized.get("details") + details_dict = details if isinstance(details, dict) else {} + metadata = normalized.get("extra_metadata") + metadata_dict = metadata if isinstance(metadata, dict) else {} + + sub_type = str(normalized.get("activity_sub_type") or content.get("activity_sub_type") or "") + has_markdown_hint = ( + sub_type == "SUBTYPE_DYNAMIC_MARKDOWN" + or any(key in content for key in ("markdown_url", "markdown", "body", "text")) + or any(key in details_dict for key in ("markdown", "body", "text")) + or any(key in metadata_dict for key in ("markdown", "body", "text")) + ) + if not has_markdown_hint: + return normalized + + normalized.setdefault("activity_type", "TYPE_DYNAMIC") + normalized["activity_sub_type"] = "SUBTYPE_DYNAMIC_MARKDOWN" + markdown_url = str(content.get("markdown_url") or "").strip() + if not markdown_url: + markdown_text = _extract_markdown_text(content, details_dict, metadata_dict, normalized) + if markdown_text: + content["markdown_url"] = _markdown_data_url(markdown_text) + normalized["content"] = content + + if isinstance(details, dict): + normalized["details"] = dict(details, content_type=details.get("content_type") or "markdown") + elif details is None: + normalized["details"] = {"content_type": "markdown"} + return normalized + + +def _extract_markdown_text(*sources: dict[str, Any]) -> str: + for source in sources: + for key in ("markdown", "body", "text", "content"): + value = source.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + title = str(sources[-1].get("name") or "Lesson").strip() + return f"# {title}\n" + + +def _markdown_data_url(markdown_text: str) -> str: + return "data:text/markdown;charset=utf-8," + quote(markdown_text, safe="") + + def _apply_learnhouse_defaults( operation: dict[str, Any], parameters: dict[str, Any], diff --git a/skills/activities/SKILL.md b/skills/activities/SKILL.md index 0a20dd5..98da290 100644 --- a/skills/activities/SKILL.md +++ b/skills/activities/SKILL.md @@ -13,9 +13,13 @@ For write, update, or delete operations, state the intended action before callin For visible lesson content, prefer `POST /api/v1/activities/` with `activity_type=TYPE_DYNAMIC`, `activity_sub_type=SUBTYPE_DYNAMIC_MARKDOWN`, -markdown stored in `content`/`details`, `lock_type=public`, and `published=true` -unless the user asks for drafts. Always use the target chapter's own -`chapter_id`; do not attach every activity to the first chapter. On +`content.markdown_url`, `lock_type=public`, and `published=true` unless the user +asks for drafts. The LearnHouse markdown renderer reads `activity.content.markdown_url`; +it does not render inline `content.body` or `content.markdown` by itself. If no +external markdown URL is available, provide inline markdown in `content.markdown` +or `content.body`; the agent runtime converts it to a `data:text/markdown` URL +before calling LearnHouse. Always use the target chapter's own `chapter_id`; do +not attach every activity to the first chapter. On `PUT /api/v1/activities/{activity_uuid}`, do not send `published_version`; this LearnHouse backend rejects that field. diff --git a/skills/courses/SKILL.md b/skills/courses/SKILL.md index 8abca65..f5f59e1 100644 --- a/skills/courses/SKILL.md +++ b/skills/courses/SKILL.md @@ -22,6 +22,7 @@ Deployment context for this LearnHouse instance: - Course creation must send multipart form fields `name`, `description`, `public`, and `about`. If the user only gives a title, use it as `name`, provide short `description` and `about` values, and set `public=false` unless the user asks for a public course. - When the user asks for a course with lessons, modules, or activities, do not stop after creating the course and chapters. Create at least one activity per chapter, using each chapter's own returned `chapter_id`. - Do not attach all generated activities to the first chapter. Verify the resulting course metadata shows activities distributed under the intended chapters. +- Markdown lesson activities must have `content.markdown_url`. Inline markdown is converted to a `data:text/markdown` URL by the runtime when no external URL is available. - For smoke/status checks, use course count or course list with default `org_slug=default`; do not call `GET /api/v1/health` as a preflight. - Course creation and course management require `LEARNHOUSE_AUTH` consumer setup.