deploy
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
2
a2a.yaml
2
a2a.yaml
@@ -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:
|
||||||
|
|||||||
81
agent.py
81
agent.py
@@ -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],
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user