This commit is contained in:
a2a-platform
2026-06-29 01:34:41 +00:00
parent a519e7b558
commit dfa7bb3bc4
6 changed files with 90 additions and 13 deletions

View File

@@ -3,7 +3,7 @@
"language": "python", "language": "python",
"name": "learnhouse-agent", "name": "learnhouse-agent",
"description": "LearnHouse is an open-source platform tailored for learning experiences.", "description": "LearnHouse is an open-source platform tailored for learning experiences.",
"version": "1.2.12", "version": "1.2.13",
"entrypoint": { "entrypoint": {
"module": "agent", "module": "agent",
"class_name": "LearnhouseAgent", "class_name": "LearnhouseAgent",
@@ -172,7 +172,7 @@
"source": "python-a2a-pack", "source": "python-a2a-pack",
"project_manifest": { "project_manifest": {
"name": "learnhouse-agent", "name": "learnhouse-agent",
"version": "1.2.12", "version": "1.2.13",
"entrypoint": "agent:LearnhouseAgent" "entrypoint": "agent:LearnhouseAgent"
} }
} }

View File

@@ -21,8 +21,9 @@ Deployment context:
using that chapter's own `chapter_id`. Do not attach all activities to the using that chapter's own `chapter_id`. Do not attach all activities to the
first chapter. first chapter.
- Visible lesson activities should normally use `TYPE_DYNAMIC` / - Visible lesson activities should normally use `TYPE_DYNAMIC` /
`SUBTYPE_DYNAMIC_MARKDOWN`, markdown content/details, `lock_type=public`, and `SUBTYPE_DYNAMIC_MARKDOWN`, `content.markdown_url`, `lock_type=public`, and
`published=true` unless the user explicitly asks for drafts. `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 - Activity updates must not send `published_version`; this LearnHouse backend
currently rejects that field. currently rejects that field.
- Do not use `GET /api/v1/health` as a preflight or blocker for API-token - Do not use `GET /api/v1/health` as a preflight or blocker for API-token

View File

@@ -1,5 +1,5 @@
name: learnhouse-agent name: learnhouse-agent
version: 1.2.12 version: 1.2.13
entrypoint: agent:LearnhouseAgent entrypoint: agent:LearnhouseAgent
description: LearnHouse is an open-source platform tailored for learning experiences. description: LearnHouse is an open-source platform tailored for learning experiences.
runtime: runtime:

View File

@@ -5,6 +5,7 @@ import json
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from urllib.parse import quote
import httpx import httpx
from a2a_pack.deepagents import create_a2a_deep_agent 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 " "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 " "chapter_id. Do not attach all activities to the first chapter. For visible "
"lesson content, create activities with activity_type TYPE_DYNAMIC, " "lesson content, create activities with activity_type TYPE_DYNAMIC, "
"activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, markdown content/details, " "activity_sub_type SUBTYPE_DYNAMIC_MARKDOWN, content.markdown_url pointing "
"lock_type public, and published=true unless the user requests drafts. " "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 " "After writing course content, verify every chapter has at least one "
"activity through course metadata or the chapter activity endpoint. When " "activity through course metadata or the chapter activity endpoint. When "
"updating activities, do not send published_version; this LearnHouse " "updating activities, do not send published_version; this LearnHouse "
@@ -77,7 +81,7 @@ class OperationInput(BaseModel):
class LearnhouseAgent(A2AAgent): class LearnhouseAgent(A2AAgent):
name = "learnhouse-agent" name = "learnhouse-agent"
description = "LearnHouse is an open-source platform tailored for learning experiences." description = "LearnHouse is an open-source platform tailored for learning experiences."
version = "1.2.12" version = "1.2.13"
consumer_setup = ConsumerSetup.from_fields( 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.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( ConsumerSetupField.secret(
@@ -309,8 +313,10 @@ class LearnhouseAgent(A2AAgent):
parts.append( parts.append(
"For LearnHouse activity creation, send JSON fields name, " "For LearnHouse activity creation, send JSON fields name, "
"chapter_id, activity_type, activity_sub_type, content, " "chapter_id, activity_type, activity_sub_type, content, "
"details, lock_type, and published. Use the target " "details, lock_type, and published. Markdown activities "
"chapter's own chapter_id; do not reuse the first chapter ID." "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): if _is_learnhouse_update_activity(operation):
parts.append( parts.append(
@@ -348,6 +354,7 @@ class LearnhouseAgent(A2AAgent):
body = json.loads(stripped) body = json.loads(stripped)
except ValueError: except ValueError:
pass pass
body = _normalize_learnhouse_activity_body(operation, body)
if _operation_uses_multipart(operation): if _operation_uses_multipart(operation):
request_kwargs["data"] = body request_kwargs["data"] = body
else: 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( def _apply_learnhouse_defaults(
operation: dict[str, Any], operation: dict[str, Any],
parameters: dict[str, Any], parameters: dict[str, Any],

View File

@@ -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 For visible lesson content, prefer `POST /api/v1/activities/` with
`activity_type=TYPE_DYNAMIC`, `activity_sub_type=SUBTYPE_DYNAMIC_MARKDOWN`, `activity_type=TYPE_DYNAMIC`, `activity_sub_type=SUBTYPE_DYNAMIC_MARKDOWN`,
markdown stored in `content`/`details`, `lock_type=public`, and `published=true` `content.markdown_url`, `lock_type=public`, and `published=true` unless the user
unless the user asks for drafts. Always use the target chapter's own asks for drafts. The LearnHouse markdown renderer reads `activity.content.markdown_url`;
`chapter_id`; do not attach every activity to the first chapter. On 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`; `PUT /api/v1/activities/{activity_uuid}`, do not send `published_version`;
this LearnHouse backend rejects that field. this LearnHouse backend rejects that field.

View File

@@ -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. - 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`. - 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. - 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. - 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. - Course creation and course management require `LEARNHOUSE_AUTH` consumer setup.