mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 13:56:36 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b589c6fce2 | ||
|
|
76d01dbd19 |
No files matched your search
@@ -22,6 +22,7 @@ Deep Analysis → Gap Analysis → Critical Review → Plan → Create Issue →
|
||||
- **No competitor names in public surfaces**: Never mention competitor product, company, or project names in GitHub issues, PRs, commit messages, release notes, docs, or any other public-facing content. Refer to capabilities generically (e.g., "self-improving agent pattern", "external agent framework").
|
||||
- **Test before merge**: never merge unvalidated PRs. Always clone the branch locally and run the tests.
|
||||
- **Minimal code change**: prefer upstream one-line fixes over downstream workarounds.
|
||||
- **Autonomous polling — NEVER ask the user to "check now"**: after posting feedback to `@claude`, submitting a trigger comment, or any state that requires an external agent to push commits, the assistant itself waits ~10 minutes (`sleep 600`) and then re-checks. Do not stop and wait for the human to type "check now". Only surface control back to the human when a round is fully complete (PR merged or 3-round feedback cap hit) or a blocking decision is required (e.g. rebase strategy, conflicting feature intent).
|
||||
|
||||
---
|
||||
|
||||
@@ -536,6 +537,11 @@ sleep 600
|
||||
# → return to Phase 12 (fetch new commits, re-run checks)
|
||||
```
|
||||
|
||||
**The assistant runs this `sleep 600` itself — it does NOT pause the loop to ask the user "check now".** After the 10-minute wait completes, the assistant immediately re-fetches the PR, re-runs the test suite, and either merges, posts the next feedback round, or reports a final status. The human is only prompted when:
|
||||
- The PR merges successfully (report + move to next issue).
|
||||
- The 3-round feedback cap is hit (escalate).
|
||||
- A design decision is needed that the assistant cannot make unilaterally (e.g. "should `allow` policy persist or not?").
|
||||
|
||||
Repeat Phases 12 → 13 → 14 until the mergeability gate in Phase 13 is fully green, or escalate to a human after 3 failed rounds.
|
||||
|
||||
### When the PR has merge conflicts
|
||||
@@ -668,3 +674,4 @@ gh issue view $ISSUE_NUM --repo $REPO --json state
|
||||
6. **Always verify** the issue auto-closed after merge.
|
||||
7. **All `run_command` calls use `timeout`** to avoid hangs (user global rule).
|
||||
8. **No proactive `.md` creation** beyond this workflow file (user global rule).
|
||||
9. **Never ask the user "check now"**: between rounds the assistant runs `sleep 600` itself and keeps driving. Only return control to the human on merge, 3-round cap, or an irresolvable design decision.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.112"
|
||||
version = "0.3.114"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -197,7 +197,20 @@ def __getattr__(name: str):
|
||||
"JSONFileDataStore",
|
||||
"SQLAlchemyDataStore",
|
||||
}
|
||||
_provider_attrs = {"BaseProvider", "RunEvent", "RunEventType"}
|
||||
_provider_attrs = {
|
||||
"BaseProvider",
|
||||
"RunEvent",
|
||||
"RunEventType",
|
||||
# Discovery cards (Issue #48) — PraisonAI-native names
|
||||
"ModelCard",
|
||||
"AgentCard",
|
||||
"TeamCard",
|
||||
# RAG source chunks (Issue #49) — PraisonAI-native names
|
||||
"SourceChunk",
|
||||
"SourceBundle",
|
||||
# Rich reasoning (Issue #50) — PraisonAI-native name
|
||||
"ThoughtStep",
|
||||
}
|
||||
_providers_attrs = {"PraisonAIProvider"}
|
||||
_config_attrs = {"configure"}
|
||||
_action_attrs = {"Action", "action_callback"}
|
||||
@@ -474,6 +487,14 @@ __all__ = [
|
||||
"RunEvent",
|
||||
"RunEventType",
|
||||
"PraisonAIProvider",
|
||||
# Discovery, citations, reasoning schema (Issues #48, #49, #50)
|
||||
# PraisonAI-native, noun-first names (not lifted from any external framework)
|
||||
"ModelCard",
|
||||
"AgentCard",
|
||||
"TeamCard",
|
||||
"SourceChunk",
|
||||
"SourceBundle",
|
||||
"ThoughtStep",
|
||||
# Configuration
|
||||
"configure",
|
||||
# Action classes and decorators
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.112"
|
||||
__version__ = "0.3.114"
|
||||
+217
-1
@@ -69,6 +69,9 @@ class RunEventType(str, Enum):
|
||||
RUN_PAUSED = "run_paused"
|
||||
RUN_CONTINUED = "run_continued"
|
||||
|
||||
# --- RAG / Citations (Issue #49) ---
|
||||
REFERENCES = "references"
|
||||
|
||||
# --- Team variants ---
|
||||
TEAM_RUN_STARTED = "team_run_started"
|
||||
TEAM_RUN_CONTENT = "team_run_content"
|
||||
@@ -84,6 +87,146 @@ class RunEventType(str, Enum):
|
||||
TEAM_MEMORY_UPDATE_COMPLETED = "team_memory_update_completed"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structured payload dataclasses (Issues #48, #49, #50)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelCard:
|
||||
"""Model / provider info advertised by an agent or team (Issue #48).
|
||||
|
||||
PraisonAI-native, non-developer-friendly name: a "card" is the small
|
||||
info block a UI shows next to an agent.
|
||||
"""
|
||||
|
||||
name: str
|
||||
model: str
|
||||
provider: str
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {"name": self.name, "model": self.model, "provider": self.provider}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentCard:
|
||||
"""Agent discovery card — what a chat frontend shows in its picker (Issue #48)."""
|
||||
|
||||
agent_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
model: Optional[ModelCard] = None
|
||||
storage: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"agent_id": self.agent_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"storage": self.storage,
|
||||
}
|
||||
if self.model is not None:
|
||||
d["model"] = self.model.to_dict()
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class TeamCard:
|
||||
"""Team discovery card — what a chat frontend shows for multi-agent teams (Issue #48)."""
|
||||
|
||||
team_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
model: Optional[ModelCard] = None
|
||||
storage: bool = False
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"team_id": self.team_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"storage": self.storage,
|
||||
}
|
||||
if self.model is not None:
|
||||
d["model"] = self.model.to_dict()
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceChunk:
|
||||
"""A single RAG retrieval chunk cited by an answer (Issue #49).
|
||||
|
||||
PraisonAI-native name — "source chunk" is the plain-English term a
|
||||
non-developer would use for "a piece of a source document that was used".
|
||||
"""
|
||||
|
||||
name: str
|
||||
content: str
|
||||
chunk: int = 0
|
||||
chunk_size: int = 0
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": self.content,
|
||||
"chunk": self.chunk,
|
||||
"chunk_size": self.chunk_size,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class SourceBundle:
|
||||
"""A bundle of source chunks retrieved for one query (Issue #49).
|
||||
|
||||
Wire format keeps the industry-standard ``references`` JSON key so
|
||||
third-party frontends see the expected shape; only the Python class
|
||||
name is PraisonAI-native.
|
||||
"""
|
||||
|
||||
query: str
|
||||
chunks: List[SourceChunk] = field(default_factory=list)
|
||||
time_ms: Optional[float] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"query": self.query,
|
||||
"references": [c.to_dict() for c in self.chunks],
|
||||
}
|
||||
if self.time_ms is not None:
|
||||
d["time_ms"] = self.time_ms
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThoughtStep:
|
||||
"""Structured reasoning-step payload (Issue #50).
|
||||
|
||||
PraisonAI-native name — pairs with the existing ``ThinkingSteps`` UI
|
||||
component. All fields beyond ``title`` are optional so providers can
|
||||
adopt the richer schema incrementally without breaking existing
|
||||
behaviour.
|
||||
"""
|
||||
|
||||
title: str
|
||||
result: str = ""
|
||||
reasoning: str = ""
|
||||
action: Optional[str] = None # e.g. "search", "plan", "verify"
|
||||
confidence: Optional[float] = None # 0.0 .. 1.0
|
||||
next_action: Optional[str] = None # what the agent plans to do next
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
d: Dict[str, Any] = {
|
||||
"title": self.title,
|
||||
"result": self.result,
|
||||
"reasoning": self.reasoning,
|
||||
}
|
||||
for key in ("action", "confidence", "next_action"):
|
||||
val = getattr(self, key)
|
||||
if val is not None:
|
||||
d[key] = val
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RunEvent — the single data structure emitted by providers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -126,12 +269,21 @@ class RunEvent:
|
||||
result: Optional[Any] = None
|
||||
tool_call_id: Optional[str] = None
|
||||
|
||||
# Reasoning
|
||||
# Reasoning (Issue #50 — structured fields are optional)
|
||||
step: Optional[str] = None
|
||||
action: Optional[str] = None
|
||||
confidence: Optional[float] = None
|
||||
next_action: Optional[str] = None
|
||||
|
||||
# Agent / Team
|
||||
agent_id: Optional[str] = None
|
||||
agent_name: Optional[str] = None
|
||||
team_id: Optional[str] = None
|
||||
|
||||
# RAG / Citations (Issue #49)
|
||||
query: Optional[str] = None
|
||||
references: Optional[List[Dict[str, Any]]] = None
|
||||
time_ms: Optional[float] = None
|
||||
|
||||
# Error
|
||||
error: Optional[str] = None
|
||||
@@ -152,8 +304,15 @@ class RunEvent:
|
||||
"result",
|
||||
"tool_call_id",
|
||||
"step",
|
||||
"action",
|
||||
"confidence",
|
||||
"next_action",
|
||||
"agent_id",
|
||||
"agent_name",
|
||||
"team_id",
|
||||
"query",
|
||||
"references",
|
||||
"time_ms",
|
||||
"error",
|
||||
"event_id",
|
||||
"timestamp",
|
||||
@@ -220,6 +379,63 @@ class BaseProvider(ABC):
|
||||
"""List available agents. Override if your backend has agents."""
|
||||
return []
|
||||
|
||||
async def list_teams(self) -> List[Dict[str, Any]]:
|
||||
"""List available multi-agent teams. Override if your backend has teams.
|
||||
|
||||
Default returns ``[]`` so providers that don't model teams keep working
|
||||
unchanged (Issue #48).
|
||||
"""
|
||||
return []
|
||||
|
||||
async def run_team(
|
||||
self,
|
||||
team_id: str,
|
||||
message: str,
|
||||
*,
|
||||
session_id: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[RunEvent]:
|
||||
"""Run a multi-agent team. Override if your backend has teams.
|
||||
|
||||
The default implementation delegates to ``run()`` with ``agent_name``
|
||||
set to ``team_id`` so single-agent backends transparently treat a
|
||||
team_id as an agent_name (Issue #48).
|
||||
"""
|
||||
async for ev in self.run(
|
||||
message, session_id=session_id, agent_name=team_id, **kwargs
|
||||
):
|
||||
yield ev
|
||||
|
||||
async def health(self) -> Dict[str, Any]:
|
||||
"""Health check endpoint data. Override for custom checks."""
|
||||
return {"status": "ok", "provider": self.__class__.__name__}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Convenience emit helpers (Issues #49, #50) — keep provider code terse.
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def source_bundle_event(bundle: "SourceBundle") -> RunEvent:
|
||||
"""Build a ``REFERENCES`` event from a ``SourceBundle`` (Issue #49).
|
||||
|
||||
The wire ``type`` stays ``references`` for third-party frontend
|
||||
compatibility; only the Python-side helper name is PraisonAI-native.
|
||||
"""
|
||||
return RunEvent(
|
||||
type=RunEventType.REFERENCES,
|
||||
query=bundle.query,
|
||||
references=[c.to_dict() for c in bundle.chunks],
|
||||
time_ms=bundle.time_ms,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def thought_step_event(step: "ThoughtStep") -> RunEvent:
|
||||
"""Build a ``REASONING_STEP`` event from a ``ThoughtStep`` (Issue #50)."""
|
||||
return RunEvent(
|
||||
type=RunEventType.REASONING_STEP,
|
||||
step=step.title,
|
||||
action=step.action,
|
||||
confidence=step.confidence,
|
||||
next_action=step.next_action,
|
||||
extra_data=step.to_dict(),
|
||||
)
|
||||
@@ -855,13 +855,22 @@ async def health(request: Request) -> JSONResponse:
|
||||
|
||||
|
||||
async def list_agents(request: Request) -> JSONResponse:
|
||||
"""List all registered agents (merges registry + provider)."""
|
||||
"""List all registered agents (merges registry + provider).
|
||||
|
||||
Returns the enriched third-party-chat-frontend-compatible schema
|
||||
(``agent_id``, ``name``, ``description``, ``model``, ``storage``,
|
||||
Issue #48) while keeping the legacy ``created_at`` field for
|
||||
backward compatibility.
|
||||
"""
|
||||
agents = [
|
||||
{
|
||||
"agent_id": name,
|
||||
"name": info["name"],
|
||||
"description": info.get("description", ""),
|
||||
"storage": bool(info.get("storage", False)),
|
||||
"created_at": info["created_at"],
|
||||
}
|
||||
for info in _agents.values()
|
||||
for name, info in _agents.items()
|
||||
]
|
||||
# Also ask the provider for agents
|
||||
try:
|
||||
@@ -870,12 +879,64 @@ async def list_agents(request: Request) -> JSONResponse:
|
||||
existing_names = {a["name"] for a in agents}
|
||||
for pa in provider_agents:
|
||||
if pa.get("name") not in existing_names:
|
||||
# Ensure every entry carries an agent_id for client routing.
|
||||
if "agent_id" not in pa:
|
||||
pa = {**pa, "agent_id": pa.get("name", "")}
|
||||
agents.append(pa)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"agents": agents})
|
||||
|
||||
|
||||
async def list_teams(request: Request) -> JSONResponse:
|
||||
"""List all multi-agent teams (Issue #48).
|
||||
|
||||
Delegates to ``provider.list_teams()``; default providers return ``[]``
|
||||
so single-agent backends transparently return an empty list.
|
||||
"""
|
||||
teams: list[dict[str, Any]] = []
|
||||
try:
|
||||
provider = get_provider()
|
||||
provider_teams = await provider.list_teams()
|
||||
for pt in provider_teams:
|
||||
if "team_id" not in pt:
|
||||
pt = {**pt, "team_id": pt.get("name", "")}
|
||||
teams.append(pt)
|
||||
except Exception:
|
||||
pass
|
||||
return JSONResponse({"teams": teams})
|
||||
|
||||
|
||||
async def run_team_by_id(request: Request) -> StreamingResponse:
|
||||
"""Run a specific team by ID with SSE streaming (Issue #48).
|
||||
|
||||
Mirrors ``run_agent_by_id``; ``BaseProvider.run_team`` falls back to
|
||||
``run()`` when the provider doesn't override it, so single-agent
|
||||
backends still work when a client addresses them as a team.
|
||||
"""
|
||||
team_id = request.path_params["team_id"]
|
||||
try:
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
||||
|
||||
body["team"] = team_id
|
||||
# Team run is modelled as an agent run with agent_name=team_id so the
|
||||
# SSE plumbing (session persistence, event forwarding) is identical.
|
||||
body.setdefault("agent", team_id)
|
||||
request._body = body
|
||||
return await run_agent(request, body)
|
||||
|
||||
|
||||
async def delete_team_session(request: Request) -> JSONResponse:
|
||||
"""Delete a session scoped to a team (Issue #48).
|
||||
|
||||
Provider-agnostic — delegates to the same session-delete path as the
|
||||
per-agent API.
|
||||
"""
|
||||
return await delete_session(request)
|
||||
|
||||
|
||||
async def list_sessions(request: Request) -> JSONResponse:
|
||||
"""List all sessions."""
|
||||
sessions = await _datastore.list_sessions()
|
||||
@@ -2575,6 +2636,16 @@ def create_app(
|
||||
Route("/cancel", cancel_run, methods=["POST"]),
|
||||
Route("/agents/{agent_id}/runs", run_agent_by_id, methods=["POST"]),
|
||||
Route("/api/agents/{agent_id}/runs", run_agent_by_id, methods=["POST"]),
|
||||
# Teams REST surface (Issue #48)
|
||||
Route("/teams", list_teams, methods=["GET"]),
|
||||
Route("/api/teams", list_teams, methods=["GET"]),
|
||||
Route("/teams/{team_id}/runs", run_team_by_id, methods=["POST"]),
|
||||
Route("/api/teams/{team_id}/runs", run_team_by_id, methods=["POST"]),
|
||||
Route(
|
||||
"/teams/{team_id}/sessions/{session_id}",
|
||||
delete_team_session,
|
||||
methods=["DELETE"],
|
||||
),
|
||||
# Dashboard API
|
||||
Route("/api/feedback", api_feedback, methods=["POST"]),
|
||||
Route("/api/feedback", api_feedback_list, methods=["GET"]),
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Protocol-parity tests for Issues #48, #49, #50.
|
||||
|
||||
Covers:
|
||||
- Enriched /agents response + /teams REST surface (#48)
|
||||
- RAG ``REFERENCES`` event + ``SourceChunk`` / ``SourceBundle`` (#49)
|
||||
- Rich ``ThoughtStep`` schema + ``thought_step_event`` helper (#50)
|
||||
|
||||
The tests run against the ASGI app built by ``praisonaiui.build_app()`` so
|
||||
route wiring, schema serialisation, and provider delegation are verified
|
||||
end-to-end without spinning up a real HTTP server.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
import praisonaiui as aiui
|
||||
from praisonaiui.provider import BaseProvider, RunEvent, RunEventType
|
||||
|
||||
# ── Fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class _StubProvider(BaseProvider):
|
||||
"""Provider that advertises one team and one simple run."""
|
||||
|
||||
async def run(self, message, *, session_id=None, agent_name=None, **kw):
|
||||
yield RunEvent(type=RunEventType.RUN_STARTED)
|
||||
yield RunEvent(type=RunEventType.RUN_COMPLETED, content=f"echo:{message}")
|
||||
|
||||
async def list_agents(self):
|
||||
return [
|
||||
{
|
||||
"agent_id": "a1",
|
||||
"name": "helper",
|
||||
"description": "a helper",
|
||||
"model": {"name": "gpt-4o", "model": "gpt-4o", "provider": "openai"},
|
||||
"storage": True,
|
||||
}
|
||||
]
|
||||
|
||||
async def list_teams(self):
|
||||
return [
|
||||
{
|
||||
"team_id": "t1",
|
||||
"name": "squad",
|
||||
"description": "a squad",
|
||||
"model": {"name": "gpt-4o", "model": "gpt-4o", "provider": "openai"},
|
||||
"storage": False,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(monkeypatch):
|
||||
"""Build a fresh app with the stub provider installed."""
|
||||
from praisonaiui.server import create_app
|
||||
|
||||
aiui.set_provider(_StubProvider())
|
||||
app = create_app()
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
# ── Issue #48: REST discovery ───────────────────────────────────────
|
||||
|
||||
|
||||
def test_list_agents_returns_enriched_schema(client):
|
||||
r = client.get("/agents")
|
||||
assert r.status_code == 200
|
||||
agents = r.json()["agents"]
|
||||
assert agents, "expected at least one agent"
|
||||
a = agents[0]
|
||||
# Enriched fields
|
||||
assert "agent_id" in a
|
||||
assert "name" in a
|
||||
assert "description" in a
|
||||
assert "storage" in a
|
||||
|
||||
|
||||
def test_list_teams_returns_team_array(client):
|
||||
r = client.get("/teams")
|
||||
assert r.status_code == 200
|
||||
teams = r.json()["teams"]
|
||||
assert teams and teams[0]["team_id"] == "t1"
|
||||
assert teams[0]["name"] == "squad"
|
||||
|
||||
|
||||
def test_list_teams_api_alias(client):
|
||||
r = client.get("/api/teams")
|
||||
assert r.status_code == 200
|
||||
assert "teams" in r.json()
|
||||
|
||||
|
||||
def test_teams_run_invalid_json_returns_400(client):
|
||||
r = client.post(
|
||||
"/teams/t1/runs",
|
||||
content="not-json",
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_teams_route_delete_endpoint_registered(client):
|
||||
# DELETE on a non-existent team session should not 404-on-route-missing
|
||||
# (it either 204s or returns a body — either way the route exists).
|
||||
r = client.delete("/teams/t1/sessions/nope")
|
||||
assert r.status_code < 500
|
||||
|
||||
|
||||
# ── Issue #49: references (RAG citations) ───────────────────────────
|
||||
|
||||
|
||||
def test_source_chunk_round_trip():
|
||||
r = aiui.SourceChunk(name="doc.md", content="x", chunk=3, chunk_size=400)
|
||||
d = r.to_dict()
|
||||
assert d == {"name": "doc.md", "content": "x", "chunk": 3, "chunk_size": 400}
|
||||
|
||||
|
||||
def test_source_bundle_round_trip():
|
||||
b = aiui.SourceBundle(
|
||||
query="what is x",
|
||||
chunks=[aiui.SourceChunk(name="a.md", content="alpha")],
|
||||
time_ms=12.0,
|
||||
)
|
||||
d = b.to_dict()
|
||||
assert d["query"] == "what is x"
|
||||
assert d["time_ms"] == 12.0
|
||||
# Wire key stays "references" for third-party frontend compatibility
|
||||
assert d["references"][0]["name"] == "a.md"
|
||||
|
||||
|
||||
def test_source_bundle_event_helper():
|
||||
bundle = aiui.SourceBundle(
|
||||
query="q",
|
||||
chunks=[aiui.SourceChunk(name="a.md", content="alpha", chunk=1, chunk_size=50)],
|
||||
time_ms=8.5,
|
||||
)
|
||||
ev = BaseProvider.source_bundle_event(bundle)
|
||||
assert ev.type is RunEventType.REFERENCES
|
||||
payload = ev.to_dict()
|
||||
assert payload["type"] == "references"
|
||||
assert payload["query"] == "q"
|
||||
assert payload["references"][0]["name"] == "a.md"
|
||||
assert payload["time_ms"] == 8.5
|
||||
|
||||
|
||||
def test_references_enum_value_is_stable():
|
||||
assert RunEventType.REFERENCES.value == "references"
|
||||
|
||||
|
||||
# ── Issue #50: rich reasoning step ──────────────────────────────────
|
||||
|
||||
|
||||
def test_thought_step_optional_fields_omitted_when_absent():
|
||||
s = aiui.ThoughtStep(title="plan")
|
||||
d = s.to_dict()
|
||||
# Optional fields stay out of the dict entirely when None
|
||||
assert "action" not in d
|
||||
assert "confidence" not in d
|
||||
assert "next_action" not in d
|
||||
assert d["title"] == "plan"
|
||||
|
||||
|
||||
def test_thought_step_full_fields():
|
||||
s = aiui.ThoughtStep(
|
||||
title="search",
|
||||
result="found 3 refs",
|
||||
reasoning="because the query was specific",
|
||||
action="search",
|
||||
confidence=0.82,
|
||||
next_action="verify",
|
||||
)
|
||||
d = s.to_dict()
|
||||
assert d == {
|
||||
"title": "search",
|
||||
"result": "found 3 refs",
|
||||
"reasoning": "because the query was specific",
|
||||
"action": "search",
|
||||
"confidence": 0.82,
|
||||
"next_action": "verify",
|
||||
}
|
||||
|
||||
|
||||
def test_thought_step_event_helper_carries_fields():
|
||||
step = aiui.ThoughtStep(title="plan", confidence=0.9, action="plan")
|
||||
ev = BaseProvider.thought_step_event(step)
|
||||
assert ev.type is RunEventType.REASONING_STEP
|
||||
d = ev.to_dict()
|
||||
assert d["step"] == "plan"
|
||||
assert d["confidence"] == 0.9
|
||||
assert d["action"] == "plan"
|
||||
# Full structured payload is also in extra_data for lossless replay
|
||||
assert d["extra_data"]["title"] == "plan"
|
||||
|
||||
|
||||
def test_runevent_backcompat_unstructured_step_still_works():
|
||||
"""Old providers pass ``step=<text>`` without any new fields — still valid."""
|
||||
ev = RunEvent(type=RunEventType.REASONING_STEP, step="just some text")
|
||||
d = ev.to_dict()
|
||||
assert d["step"] == "just some text"
|
||||
assert "action" not in d
|
||||
assert "confidence" not in d
|
||||
|
||||
|
||||
# ── Public surface ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_new_symbols_in_dunder_all():
|
||||
expected = {
|
||||
"ModelCard",
|
||||
"AgentCard",
|
||||
"TeamCard",
|
||||
"SourceChunk",
|
||||
"SourceBundle",
|
||||
"ThoughtStep",
|
||||
}
|
||||
missing = expected - set(aiui.__all__)
|
||||
assert not missing, f"Missing from __all__: {sorted(missing)}"
|
||||
|
||||
|
||||
def test_no_competitor_lifted_names_exported():
|
||||
"""Guard rail: PraisonAIUI must not re-export competitor-lifted type names."""
|
||||
lifted = {
|
||||
"ModelInfo",
|
||||
"AgentDetails",
|
||||
"TeamDetails",
|
||||
"Reference",
|
||||
"ReferenceData",
|
||||
"ReasoningStep",
|
||||
}
|
||||
leaked = [n for n in lifted if hasattr(aiui, n)]
|
||||
assert not leaked, (
|
||||
f"PraisonAIUI leaked competitor-lifted names: {leaked}. "
|
||||
"Use the PraisonAI-native ModelCard/AgentCard/TeamCard/SourceChunk/"
|
||||
"SourceBundle/ThoughtStep instead."
|
||||
)
|
||||
Reference in new issue
Block a user