Skip to content

Commit 630a76d

Browse files
committed
Refactor artifact generation tools by introducing separate CodeGenerator and DocumentGenerator classes. Update app_writer to utilize FunctionAgent for code and document generation workflows. Remove deprecated ArtifactGenerator class. Enhance artifact transformation logic in callbacks. Improve system prompts for clarity and instruction adherence.
1 parent d787ecf commit 630a76d

File tree

6 files changed

+334
-214
lines changed

6 files changed

+334
-214
lines changed

llama-index-server/examples/app_writer.py

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,57 @@
1-
from typing import List
2-
31
from fastapi import FastAPI
42

5-
from llama_index.core.agent.workflow import AgentWorkflow
6-
from llama_index.core.tools import BaseTool
3+
from llama_index.core.agent.workflow import AgentWorkflow, FunctionAgent
74
from llama_index.core.workflow import Workflow
85
from llama_index.llms.openai import OpenAI
96
from llama_index.server import LlamaIndexServer, UIConfig
107
from llama_index.server.api.models import ChatRequest
118
from llama_index.server.api.utils import get_last_artifact
12-
from llama_index.server.tools.artifact_generator import ArtifactGenerator
9+
from llama_index.server.tools.artifact import CodeGenerator, DocumentGenerator
1310

1411

1512
def create_workflow(chat_request: ChatRequest) -> Workflow:
16-
tools: List[BaseTool] = [
17-
ArtifactGenerator(
18-
last_artifact=get_last_artifact(chat_request),
19-
llm=OpenAI(model="gpt-4.1"),
20-
).to_tool()
21-
]
22-
agent = AgentWorkflow.from_tools_or_functions(
23-
tools, # type: ignore
13+
app_writer_agent = FunctionAgent(
14+
name="Coder",
15+
description="A skilled full-stack developer.",
16+
system_prompt="""
17+
You are an skilled full-stack developer that can help user update the code by using the code generator tool.
18+
Follow these instructions:
19+
+ Thinking and provide a correct requirement to the code generator tool.
20+
+ Always use the tool to update the code.
21+
+ Don't need to response the code just summarize the code and the changes you made.
22+
""",
23+
tools=[
24+
CodeGenerator(
25+
last_artifact=get_last_artifact(chat_request),
26+
llm=OpenAI(model="gpt-4.1"),
27+
).to_tool()
28+
], # type: ignore
29+
llm=OpenAI(model="gpt-4.1"),
30+
)
31+
doc_writer_agent = FunctionAgent(
32+
name="Writer",
33+
description="A skilled document writer.",
34+
system_prompt="""
35+
You are an skilled document writer that can help user update the document by using the document generator tool.
36+
Follow these instructions:
37+
+ Thinking and provide a correct requirement to the document generator tool.
38+
+ Always use the tool to update the document.
39+
+ Don't need to response the document just summarize the document and the changes you made.
40+
""",
41+
tools=[
42+
DocumentGenerator(
43+
last_artifact=get_last_artifact(chat_request),
44+
llm=OpenAI(model="gpt-4.1"),
45+
).to_tool()
46+
], # type: ignore
2447
llm=OpenAI(model="gpt-4.1-mini"),
25-
system_prompt="You are a helpful assistant that can generate artifacts (code or markdown document), use the provided tools to respond to the user's request.",
2648
)
27-
return agent
49+
workflow = AgentWorkflow(
50+
agents=[app_writer_agent, doc_writer_agent],
51+
root_agent="Coder",
52+
verbose=True,
53+
)
54+
return workflow
2855

2956

3057
def create_app() -> FastAPI:

llama-index-server/llama_index/server/api/callbacks/artifact.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import time
2-
from typing import Any
1+
from typing import Any, Dict, Optional
32

43
from llama_index.core.agent.workflow.workflow_events import ToolCallResult
54
from llama_index.server.api.callbacks.base import EventCallback
@@ -15,20 +14,23 @@ class ArtifactFromToolCall(EventCallback):
1514
default is "artifact"
1615
"""
1716

18-
def __init__(self, tool_name: str = "artifact_generator"):
19-
self.tool_name = tool_name
17+
def __init__(self, tool_prefix: str = "artifact_"):
18+
self.tool_prefix = tool_prefix
2019

21-
def transform_tool_call_result(self, event: ToolCallResult) -> Artifact:
22-
artifact = event.tool_output.raw_output
23-
return Artifact(
24-
created_at=int(time.time()),
25-
type=artifact.get("type"),
26-
data=artifact.get("data"),
27-
)
20+
def transform_tool_call_result(
21+
self, event: ToolCallResult
22+
) -> Optional[Dict[str, Any]]:
23+
artifact: Artifact = event.tool_output.raw_output
24+
if isinstance(artifact, str): # Error tool output
25+
return None
26+
return {
27+
"type": "artifact",
28+
"data": artifact.model_dump(),
29+
}
2830

2931
async def run(self, event: Any) -> Any:
3032
if isinstance(event, ToolCallResult):
31-
if event.tool_name == self.tool_name:
33+
if event.tool_name.startswith(self.tool_prefix):
3234
return event, self.transform_tool_call_result(event)
3335
return event
3436

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from llama_index.server.tools.artifact.code_generator import CodeGenerator
2+
from llama_index.server.tools.artifact.document_generator import DocumentGenerator
3+
4+
__all__ = ["CodeGenerator", "DocumentGenerator"]
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import logging
2+
import re
3+
import time
4+
from typing import List, Optional
5+
6+
from llama_index.core.llms import LLM
7+
from llama_index.core.llms.llm import ChatMessage, MessageRole
8+
from llama_index.core.settings import Settings
9+
from llama_index.core.tools.function_tool import FunctionTool
10+
from llama_index.server.api.models import Artifact, ArtifactType, CodeArtifactData
11+
12+
logger = logging.getLogger(__name__)
13+
14+
CODE_GENERATION_PROMPT = """
15+
You are a highly skilled content creator and software engineer.
16+
Your task is to generate code to resolve the user's request.
17+
18+
Follow these instructions exactly:
19+
20+
1. Carefully read the user's requirements.
21+
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
22+
If the previous code is provided, carefully analyze the code with the request to make the right changes.
23+
2. For code requests:
24+
- If the user does not specify a framework or language, default to a React component using the Next.js framework.
25+
- For Next.js, use Shadcn UI components, Typescript, @types/node, @types/react, @types/react-dom, PostCSS, and TailwindCSS.
26+
The import pattern should be:
27+
```
28+
import { ComponentName } from "@/components/ui/component-name"
29+
import { Markdown } from "@llamaindex/chat-ui"
30+
import { cn } from "@/lib/utils"
31+
```
32+
- Ensure the code is idiomatic, production-ready, and includes necessary imports.
33+
- Only generate code relevant to the user's request—do not add extra boilerplate.
34+
3. Don't be verbose on response, no other text or comments only return the code which wrapped by ```language``` block.
35+
Example:
36+
```typescript
37+
import React from "react";
38+
39+
export default function MyComponent() {
40+
return <div>Hello World</div>;
41+
}
42+
```
43+
"""
44+
45+
46+
class CodeGenerator:
47+
def __init__(
48+
self,
49+
llm: Optional[LLM] = None,
50+
last_artifact: Optional[Artifact] = None,
51+
) -> None:
52+
if llm is None:
53+
if Settings.llm is None:
54+
raise ValueError(
55+
"Missing llm. Please provide a valid LLM or set the LLM using Settings.llm."
56+
)
57+
llm = Settings.llm
58+
self.llm = llm
59+
self.last_artifact = last_artifact
60+
61+
def prepare_chat_messages(
62+
self, requirement: str, language: str, previous_code: Optional[str] = None
63+
) -> List[ChatMessage]:
64+
user_messages: List[ChatMessage] = []
65+
user_messages.append(ChatMessage(role=MessageRole.USER, content=requirement))
66+
if previous_code:
67+
user_messages.append(
68+
ChatMessage(
69+
role=MessageRole.USER,
70+
content=f"```{language}\n{previous_code}\n```",
71+
)
72+
)
73+
else:
74+
user_messages.append(
75+
ChatMessage(
76+
role=MessageRole.USER,
77+
content=f"Write code in {language}. Wrap the code in ```{language}``` block.",
78+
)
79+
)
80+
return user_messages
81+
82+
async def generate_code(
83+
self,
84+
file_name: str,
85+
language: str,
86+
requirement: str,
87+
previous_code: Optional[str] = None,
88+
) -> Artifact:
89+
"""
90+
Generate code based on the provided requirement.
91+
92+
Args:
93+
file_name (str): The name of the file to generate.
94+
language (str): The language of the code to generate (Only "typescript" and "python" is supported now)
95+
requirement (str): Provide a detailed requirement for the code to be generated/updated.
96+
old_content (Optional[str]): Existing code content to be modified or referenced. Defaults to None.
97+
98+
Returns:
99+
Artifact: A dictionary containing the generated artifact details
100+
(type, data).
101+
"""
102+
user_messages = self.prepare_chat_messages(requirement, language, previous_code)
103+
104+
messages: List[ChatMessage] = [
105+
ChatMessage(role=MessageRole.SYSTEM, content=CODE_GENERATION_PROMPT),
106+
*user_messages,
107+
]
108+
109+
try:
110+
response = await self.llm.achat(messages)
111+
raw_content = response.message.content
112+
if not raw_content:
113+
raise ValueError(
114+
"Empty response. Try with a clearer requirement or provide previous code."
115+
)
116+
117+
# Extract code from code block in raw content
118+
code_block = re.search(r"```(.*?)\n(.*?)```", raw_content, re.DOTALL)
119+
if not code_block:
120+
raise ValueError("Couldn't parse code from the response.")
121+
code = code_block.group(2).strip()
122+
return Artifact(
123+
created_at=int(time.time()),
124+
type=ArtifactType.CODE,
125+
data=CodeArtifactData(
126+
file_name=file_name,
127+
code=code,
128+
language=language,
129+
),
130+
)
131+
except Exception as e:
132+
raise ValueError(f"Couldn't generate code. {e}")
133+
134+
def to_tool(self) -> FunctionTool:
135+
"""
136+
Converts the CodeGenerator instance into a FunctionTool.
137+
138+
Returns:
139+
FunctionTool: A tool that can be used by agents.
140+
"""
141+
return FunctionTool.from_defaults(
142+
self.generate_code,
143+
name="artifact_code_generator",
144+
description="Generate/update code based on a requirement.",
145+
)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import logging
2+
import re
3+
import time
4+
from typing import List, Literal, Optional
5+
6+
from llama_index.core.llms import LLM
7+
from llama_index.core.llms.llm import ChatMessage, MessageRole
8+
from llama_index.core.settings import Settings
9+
from llama_index.core.tools.function_tool import FunctionTool
10+
from llama_index.server.api.models import Artifact, ArtifactType, DocumentArtifactData
11+
12+
logger = logging.getLogger(__name__)
13+
14+
DOCUMENT_GENERATION_PROMPT = """
15+
You are a highly skilled writer and content creator.
16+
Your task is to generate documents based on the user's request.
17+
18+
Follow these instructions exactly:
19+
20+
1. Carefully read the user's requirements.
21+
If any details are ambiguous or missing, make reasonable assumptions and clearly reflect those in your output.
22+
If previous content is provided, carefully analyze it with the request to make the right changes.
23+
2. For document creation:
24+
- Create well-structured documents with clear headings, paragraphs, and formatting.
25+
- Use concise and professional language.
26+
- Ensure content is accurate, well-researched, and relevant to the topic.
27+
- Organize information logically with a proper introduction, body, and conclusion when appropriate.
28+
- Add citations or references when necessary.
29+
3. For document editing:
30+
- Maintain the original document's structure unless requested otherwise.
31+
- Improve clarity, flow, and grammar while preserving the original message.
32+
- Remove redundancies and strengthen weak points.
33+
4. Answer in appropriate format which wrapped by ```<format>``` block with a file name for the content, no other text or comments.
34+
Example:
35+
```markdown
36+
# Title
37+
38+
Content
39+
```
40+
"""
41+
42+
43+
class DocumentGenerator:
44+
def __init__(
45+
self,
46+
llm: Optional[LLM] = None,
47+
last_artifact: Optional[Artifact] = None,
48+
) -> None:
49+
if llm is None:
50+
if Settings.llm is None:
51+
raise ValueError(
52+
"Missing llm. Please provide a valid LLM or set the LLM using Settings.llm."
53+
)
54+
llm = Settings.llm
55+
self.llm = llm
56+
self.last_artifact = last_artifact
57+
58+
def prepare_chat_messages(
59+
self, requirement: str, previous_content: Optional[str] = None
60+
) -> List[ChatMessage]:
61+
user_messages: List[ChatMessage] = []
62+
user_messages.append(ChatMessage(role=MessageRole.USER, content=requirement))
63+
if previous_content:
64+
user_messages.append(
65+
ChatMessage(role=MessageRole.USER, content=previous_content)
66+
)
67+
return user_messages
68+
69+
async def generate_document(
70+
self,
71+
file_name: str,
72+
document_format: Literal["markdown", "html"],
73+
requirement: str,
74+
previous_content: Optional[str] = None,
75+
) -> Artifact:
76+
"""
77+
Generate document content based on the provided requirement.
78+
79+
Args:
80+
file_name (str): The name of the file to generate.
81+
document_format (str): The format of the document to generate. (Only "markdown" and "html" are supported now)
82+
requirement (str): A detailed requirement for the document to be generated/updated.
83+
previous_content (Optional[str]): Existing document content to be modified or referenced. Defaults to None.
84+
85+
Returns:
86+
Artifact: The generated document.
87+
"""
88+
user_messages = self.prepare_chat_messages(requirement, previous_content)
89+
90+
messages: List[ChatMessage] = [
91+
ChatMessage(role=MessageRole.SYSTEM, content=DOCUMENT_GENERATION_PROMPT),
92+
*user_messages,
93+
]
94+
95+
try:
96+
response = await self.llm.achat(messages)
97+
raw_content = response.message.content
98+
if not raw_content:
99+
raise ValueError(
100+
"Empty response. Try with a clearer requirement or provide previous content."
101+
)
102+
# Extract content from the response
103+
content = re.search(r"```(.*?)\n(.*?)```", raw_content, re.DOTALL)
104+
if not content:
105+
raise ValueError("Couldn't parse content from the response.")
106+
return Artifact(
107+
created_at=int(time.time()),
108+
type=ArtifactType.DOCUMENT,
109+
data=DocumentArtifactData(
110+
title=file_name,
111+
content=content.group(2).strip(),
112+
type=document_format,
113+
),
114+
)
115+
except Exception as e:
116+
raise ValueError(f"Couldn't generate document. {e}")
117+
118+
def to_tool(self) -> FunctionTool:
119+
"""
120+
Converts the DocumentGenerator instance into a FunctionTool.
121+
122+
Returns:
123+
FunctionTool: A tool that can be used by agents.
124+
"""
125+
return FunctionTool.from_defaults(
126+
self.generate_document,
127+
name="artifact_document_generator",
128+
description="Generate/update documents based on a requirement.",
129+
)

0 commit comments

Comments
 (0)