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",
"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"
}
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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],

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
`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.

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.
- 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.