feat: show document artifact after generating report (#658)

* feat: show document artifact after generating report

* keep chat message content as it is

* use artifactEvent from server

* add deep research example

* bump chat-ui for new editor

* import editor css

* hide warning for workflowEvent<{}>() in eject mode

* fix format

* use CL for better testing

* generate artifact after streaming report in Python

* bump chat-ui to support citations

* use isinstance to check stream

* fix document editor spacing

* Create tame-wolves-obey.md

* add sources to document artifact

* add sources to document artifact in python

* type cast

* no need score

* fix lint

* move handle stream logic to server

* refactor: use chunk.text and chunk.raw

* bump chat-ui 0.5.6 to fix citations

* update changset

* fix lock
This commit is contained in:
Thuc Pham
2025-06-06 16:34:52 +07:00
committed by GitHub
parent 1ff6eaf3e1
commit af9ad3c42d
9 changed files with 122 additions and 5 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"create-llama": patch
"@llamaindex/server": patch
"@create-llama/llama-index-server": patch
---
feat: show document artifact after generating report
@@ -1,7 +1,7 @@
import logging
import os
import uuid
from typing import List, Literal, Optional
from typing import List, Literal, Optional, AsyncGenerator
from app.index import get_index
from llama_index.core.base.llms.types import (
@@ -23,7 +23,18 @@ from llama_index.core.workflow import (
Workflow,
step,
)
from llama_index.server.api.models import ChatRequest, SourceNodesEvent, UIEvent
from llama_index.server.api.models import (
ArtifactEvent,
ArtifactType,
ChatRequest,
SourceNodesEvent,
UIEvent,
Artifact,
DocumentArtifactData,
DocumentArtifactSource,
)
import time
from llama_index.server.utils.stream import write_response_to_stream
from pydantic import BaseModel, Field
logger = logging.getLogger("uvicorn")
@@ -365,8 +376,31 @@ class DeepResearchWorkflow(Workflow):
user_request=self.user_request,
stream=self.stream,
)
final_response = await write_response_to_stream(res, ctx)
ctx.write_event_to_stream(
ArtifactEvent(
data=Artifact(
type=ArtifactType.DOCUMENT,
created_at=int(time.time()),
data=DocumentArtifactData(
title="DeepResearch Report",
content=final_response,
type="markdown",
sources=[
DocumentArtifactSource(
id=node.id_,
)
for node in self.context_nodes
],
),
),
)
)
return StopEvent(
result=res,
result="",
)
@@ -1,4 +1,4 @@
import { toSourceEvent } from "@llamaindex/server";
import { artifactEvent, toSourceEvent } from "@llamaindex/server";
import {
agentStreamEvent,
createStatefulMiddleware,
@@ -339,6 +339,26 @@ export function getWorkflow(index: VectorStoreIndex | LlamaCloudIndex) {
}),
);
}
// Open the generated report in Canvas
sendEvent(
artifactEvent.with({
type: "artifact",
data: {
type: "document",
created_at: Date.now(),
data: {
title: "DeepResearch Report",
content: response,
type: "markdown",
sources: state.contextNodes.map((node) => ({
id: node.node.id_,
})),
},
},
}),
);
return stopAgentEvent.with({
result: response,
});
+1
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "@llamaindex/chat-ui/styles/editor.css";
import "@llamaindex/chat-ui/styles/markdown.css";
import "@llamaindex/chat-ui/styles/pdf.css";
import "./globals.css";
@@ -18,6 +18,7 @@ const eslintConfig = [
"react-hooks/exhaustive-deps": "off",
"@next/next/no-img-element": "off",
"@next/next/no-assign-module-variable": "off",
"@typescript-eslint/no-empty-object-type": "off",
},
},
{
+1
View File
@@ -110,6 +110,7 @@ export type DocumentArtifactData = {
title: string;
content: string;
type: string; // markdown, html,...
sources?: { id: string }[]; // sources that are used to render citation numbers in the document
};
export type CodeArtifact = Artifact<CodeArtifactData> & {
@@ -4,6 +4,7 @@ from llama_index.server.models.artifacts import (
ArtifactType,
CodeArtifactData,
DocumentArtifactData,
DocumentArtifactSource,
)
from llama_index.server.models.chat import ChatAPIMessage, ChatRequest
from llama_index.server.models.hitl import HumanInputEvent, HumanResponseEvent
@@ -20,6 +21,7 @@ __all__ = [
"ArtifactEvent",
"ArtifactType",
"DocumentArtifactData",
"DocumentArtifactSource",
"CodeArtifactData",
"ChatAPIMessage",
"ChatRequest",
@@ -1,6 +1,6 @@
import logging
from enum import Enum
from typing import Literal, Optional, Union
from typing import List, Literal, Optional, Union
from llama_index.core.workflow.events import Event
from llama_index.server.models.chat import ChatAPIMessage
@@ -21,10 +21,16 @@ class CodeArtifactData(BaseModel):
language: str
class DocumentArtifactSource(BaseModel):
id: str
# we can add more fields here
class DocumentArtifactData(BaseModel):
title: str
content: str
type: Literal["markdown", "html"]
sources: Optional[List[DocumentArtifactSource]] = None
class Artifact(BaseModel):
@@ -0,0 +1,45 @@
from typing import AsyncGenerator, Union
from llama_index.core.base.llms.types import (
CompletionResponse,
CompletionResponseAsyncGen,
)
from llama_index.core.workflow import Context
from llama_index.core.agent.workflow.workflow_events import AgentStream
async def write_response_to_stream(
res: Union[CompletionResponse, CompletionResponseAsyncGen],
ctx: Context,
current_agent_name: str = "assistant",
) -> str:
"""
Handle both streaming and non-streaming LLM responses.
Args:
res: The LLM response (either streaming or non-streaming)
ctx: The workflow context for writing events to stream
current_agent_name: The name of the current agent (default: "assistant")
Returns:
The final response text as a string
"""
final_response = ""
if isinstance(res, AsyncGenerator):
# Handle streaming response (CompletionResponseAsyncGen)
async for chunk in res:
ctx.write_event_to_stream(
AgentStream(
delta=chunk.delta or "",
response=final_response,
current_agent_name=current_agent_name,
tool_calls=[],
raw=chunk.raw or "",
)
)
final_response = chunk.text
else:
# Handle non-streaming response (CompletionResponse)
final_response = res.text
return final_response