Skip to content

Commit e0bb3f5

Browse files
committed
add artifact generator agent
1 parent c7b7672 commit e0bb3f5

File tree

7 files changed

+219
-14
lines changed

7 files changed

+219
-14
lines changed

templates/types/multiagent/fastapi/app/agents/multi.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
)
99
from llama_index.core.tools.types import ToolMetadata, ToolOutput
1010
from llama_index.core.tools.utils import create_schema_from_function
11-
from llama_index.core.workflow import Context, Workflow
11+
from llama_index.core.workflow import Context, StopEvent, Workflow
1212

1313

1414
class AgentCallTool(ContextAwareTool):
@@ -35,7 +35,8 @@ async def acall(self, ctx: Context, input: str) -> ToolOutput:
3535
handler = self.agent.run(input=input)
3636
# bubble all events while running the agent to the calling agent
3737
async for ev in handler.stream_events():
38-
ctx.write_event_to_stream(ev)
38+
if type(ev) is not StopEvent:
39+
ctx.write_event_to_stream(ev)
3940
ret: AgentRunResult = await handler
4041
response = ret.response.message.content
4142
return ToolOutput(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from typing import List
2+
3+
from app.agents.single import FunctionCallingAgent
4+
from app.tools.artifact import ArtifactGenerator
5+
from llama_index.core.chat_engine.types import ChatMessage
6+
from llama_index.core.tools import FunctionTool
7+
8+
9+
def create_artifact_generator(chat_history: List[ChatMessage]):
10+
artifact_tool = FunctionTool.from_defaults(ArtifactGenerator.generate_artifact)
11+
12+
return FunctionCallingAgent(
13+
name="ArtifactGenerator",
14+
tools=[artifact_tool],
15+
role="expert in generating artifacts (pdf, html)",
16+
system_prompt="You are generator that help generate artifacts (pdf, html) from a given content.",
17+
chat_history=chat_history,
18+
verbose=True,
19+
)
Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
from typing import List, Optional
2-
from app.agents.single import FunctionCallingAgent
2+
33
from app.agents.multi import AgentCallingAgent
4+
from app.agents.single import FunctionCallingAgent
5+
from app.examples.artifact_generator import create_artifact_generator
46
from app.examples.researcher import create_researcher
57
from llama_index.core.chat_engine.types import ChatMessage
68

79

810
def create_choreography(chat_history: Optional[List[ChatMessage]] = None):
911
researcher = create_researcher(chat_history)
12+
artifact_generator = create_artifact_generator(chat_history)
1013
reviewer = FunctionCallingAgent(
1114
name="reviewer",
1215
role="expert in reviewing blog posts",
1316
system_prompt="You are an expert in reviewing blog posts. You are given a task to review a blog post. Review the post for logical inconsistencies, ask critical questions, and provide suggestions for improvement. Furthermore, proofread the post for grammar and spelling errors. If the post is good, you can say 'The post is good.'",
1417
chat_history=chat_history,
18+
verbose=True,
1519
)
1620
return AgentCallingAgent(
1721
name="writer",
18-
agents=[researcher, reviewer],
22+
agents=[researcher, reviewer, artifact_generator],
1923
role="expert in writing blog posts",
2024
system_prompt="""You are an expert in writing blog posts. You are given a task to write a blog post. Before starting to write the post, consult the researcher agent to get the information you need. Don't make up any information yourself.
2125
After creating a draft for the post, send it to the reviewer agent to receive some feedback and make sure to incorporate the feedback from the reviewer.
22-
You can consult the reviewer and researcher maximal two times. Your output should just contain the blog post.""",
26+
You can consult the reviewer and researcher maximal two times. Your output should just contain the blog post.
27+
Finally, always request the artifact generator to create an artifact (pdf, html) that user can download and use for publishing the blog post.""",
2328
# TODO: add chat_history support to AgentCallingAgent
2429
# chat_history=chat_history,
30+
verbose=True,
2531
)

templates/types/multiagent/fastapi/app/examples/orchestrator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from typing import List, Optional
2-
from app.agents.single import FunctionCallingAgent
2+
33
from app.agents.multi import AgentOrchestrator
4+
from app.agents.single import FunctionCallingAgent
5+
from app.examples.artifact_generator import create_artifact_generator
46
from app.examples.researcher import create_researcher
5-
67
from llama_index.core.chat_engine.types import ChatMessage
78

89

@@ -21,7 +22,8 @@ def create_orchestrator(chat_history: Optional[List[ChatMessage]] = None):
2122
Especially check for logical inconsistencies and proofread the post for grammar and spelling errors.""",
2223
chat_history=chat_history,
2324
)
25+
artifact_generator = create_artifact_generator(chat_history)
2426
return AgentOrchestrator(
25-
agents=[writer, reviewer, researcher],
27+
agents=[writer, reviewer, researcher, artifact_generator],
2628
refine_plan=False,
2729
)

templates/types/multiagent/fastapi/app/examples/workflow.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import AsyncGenerator, List, Optional
22

33
from app.agents.single import AgentRunEvent, AgentRunResult, FunctionCallingAgent
4+
from app.examples.artifact_generator import create_artifact_generator
45
from app.examples.researcher import create_researcher
56
from llama_index.core.chat_engine.types import ChatMessage
67
from llama_index.core.workflow import (
@@ -17,6 +18,9 @@ def create_workflow(chat_history: Optional[List[ChatMessage]] = None):
1718
researcher = create_researcher(
1819
chat_history=chat_history,
1920
)
21+
artifact_generator = create_artifact_generator(
22+
chat_history=chat_history,
23+
)
2024
writer = FunctionCallingAgent(
2125
name="writer",
2226
role="expert in writing blog posts",
@@ -30,7 +34,12 @@ def create_workflow(chat_history: Optional[List[ChatMessage]] = None):
3034
chat_history=chat_history,
3135
)
3236
workflow = BlogPostWorkflow(timeout=360)
33-
workflow.add_workflows(researcher=researcher, writer=writer, reviewer=reviewer)
37+
workflow.add_workflows(
38+
researcher=researcher,
39+
writer=writer,
40+
reviewer=reviewer,
41+
artifact_generator=artifact_generator,
42+
)
3443
return workflow
3544

3645

@@ -40,13 +49,16 @@ class ResearchEvent(Event):
4049

4150
class WriteEvent(Event):
4251
input: str
43-
is_good: bool = False
4452

4553

4654
class ReviewEvent(Event):
4755
input: str
4856

4957

58+
class GenerateArtifactEvent(Event):
59+
input: str
60+
61+
5062
class BlogPostWorkflow(Workflow):
5163
@step()
5264
async def start(self, ctx: Context, ev: StartEvent) -> ResearchEvent:
@@ -80,7 +92,7 @@ async def write(
8092
msg=f"Too many attempts ({MAX_ATTEMPTS}) to write the blog post. Proceeding with the current version.",
8193
)
8294
)
83-
if ev.is_good or too_many_attempts:
95+
if too_many_attempts:
8496
# too many attempts or the blog post is good - stream final response if requested
8597
result = await self.run_agent(
8698
ctx, writer, ev.input, streaming=ctx.data["streaming"]
@@ -93,7 +105,7 @@ async def write(
93105
@step()
94106
async def review(
95107
self, ctx: Context, ev: ReviewEvent, reviewer: FunctionCallingAgent
96-
) -> WriteEvent:
108+
) -> WriteEvent | GenerateArtifactEvent:
97109
result: AgentRunResult = await self.run_agent(ctx, reviewer, ev.input)
98110
review = result.response.message.content
99111
old_content = ctx.data["result"].response.message.content
@@ -105,9 +117,8 @@ async def review(
105117
)
106118
)
107119
if post_is_good:
108-
return WriteEvent(
120+
return GenerateArtifactEvent(
109121
input=f"You're blog post is ready for publication. Please respond with just the blog post. Blog post: ```{old_content}```",
110-
is_good=True,
111122
)
112123
else:
113124
return WriteEvent(
@@ -123,6 +134,16 @@ async def review(
123134
```"""
124135
)
125136

137+
@step()
138+
async def generate_artifact(
139+
self,
140+
ctx: Context,
141+
ev: GenerateArtifactEvent,
142+
artifact_generator: FunctionCallingAgent,
143+
) -> StopEvent:
144+
result: AgentRunResult = await self.run_agent(ctx, artifact_generator, ev.input)
145+
return StopEvent(result=result)
146+
126147
async def run_agent(
127148
self,
128149
ctx: Context,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import os
2+
from enum import Enum
3+
from io import BytesIO
4+
5+
OUTPUT_DIR = "output/tools"
6+
7+
8+
class ArtifactType(Enum):
9+
PDF = "pdf"
10+
HTML = "html"
11+
12+
13+
class ArtifactGenerator:
14+
@classmethod
15+
def _generate_pdf(cls, original_content: str) -> BytesIO:
16+
"""
17+
Generate a PDF from the original content (markdown).
18+
"""
19+
try:
20+
import markdown
21+
from reportlab.lib.pagesizes import letter
22+
from reportlab.lib.styles import getSampleStyleSheet
23+
from reportlab.platypus import Paragraph, SimpleDocTemplate
24+
except ImportError:
25+
raise ImportError(
26+
"Failed to import required modules. Please install reportlab and markdown."
27+
)
28+
29+
# Convert markdown to HTML
30+
html = markdown.markdown(original_content)
31+
32+
buffer = BytesIO()
33+
34+
doc = SimpleDocTemplate(buffer, pagesize=letter)
35+
36+
# Create a list to store the flowables (content elements)
37+
elements = []
38+
styles = getSampleStyleSheet()
39+
# TODO: Make the format nicer
40+
for paragraph in html.split("<p>"):
41+
if paragraph:
42+
clean_text = paragraph.replace("</p>", "").strip()
43+
elements.append(Paragraph(clean_text, styles["Normal"]))
44+
45+
# Build the PDF document
46+
doc.build(elements)
47+
48+
# Reset the buffer position to the beginning
49+
buffer.seek(0)
50+
51+
return buffer
52+
53+
@classmethod
54+
def _generate_html(cls, original_content: str) -> str:
55+
"""
56+
Generate an HTML from the original content (markdown).
57+
"""
58+
try:
59+
import markdown
60+
except ImportError:
61+
raise ImportError(
62+
"Failed to import required modules. Please install markdown."
63+
)
64+
65+
# Convert markdown to HTML
66+
html_content = markdown.markdown(original_content)
67+
68+
# Create a complete HTML document with basic styling
69+
html_document = f"""
70+
<!DOCTYPE html>
71+
<html lang="en">
72+
<head>
73+
<meta charset="UTF-8">
74+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
75+
<title>Generated HTML Document</title>
76+
<style>
77+
body {{
78+
font-family: Arial, sans-serif;
79+
line-height: 1.6;
80+
color: #333;
81+
max-width: 800px;
82+
margin: 0 auto;
83+
padding: 20px;
84+
}}
85+
h1, h2, h3, h4, h5, h6 {{
86+
margin-top: 1.5em;
87+
margin-bottom: 0.5em;
88+
}}
89+
p {{
90+
margin-bottom: 1em;
91+
}}
92+
code {{
93+
background-color: #f4f4f4;
94+
padding: 2px 4px;
95+
border-radius: 4px;
96+
}}
97+
pre {{
98+
background-color: #f4f4f4;
99+
padding: 10px;
100+
border-radius: 4px;
101+
overflow-x: auto;
102+
}}
103+
</style>
104+
</head>
105+
<body>
106+
{html_content}
107+
</body>
108+
</html>
109+
"""
110+
111+
return html_document
112+
113+
@classmethod
114+
def _write_to_file(cls, content: BytesIO, file_path: str):
115+
"""
116+
Write the content to a file.
117+
"""
118+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
119+
with open(file_path, "wb") as file:
120+
file.write(content.getvalue())
121+
122+
@classmethod
123+
def generate_artifact(
124+
cls, original_content: str, artifact_type: str, file_name: str
125+
) -> str:
126+
"""
127+
Generate an artifact from the original content and write it to a file.
128+
Parameters:
129+
original_content: str (markdown style)
130+
artifact_type: str (pdf or html). Use pdf for report, html for blog post.
131+
file_name: str (name of the artifact file), don't need to include the file extension. It will be added automatically based on the artifact type.
132+
Returns:
133+
str (URL to the artifact file): the url that already available to the file server. No need to change the path anymore.
134+
"""
135+
try:
136+
artifact_type = ArtifactType(artifact_type.lower())
137+
except ValueError:
138+
raise ValueError(
139+
f"Invalid artifact type: {artifact_type}. Must be 'pdf' or 'html'."
140+
)
141+
142+
if artifact_type == ArtifactType.PDF:
143+
content = cls._generate_pdf(original_content)
144+
file_extension = "pdf"
145+
elif artifact_type == ArtifactType.HTML:
146+
content = BytesIO(cls._generate_html(original_content).encode("utf-8"))
147+
file_extension = "html"
148+
else:
149+
raise ValueError(f"Unexpected artifact type: {artifact_type}")
150+
151+
file_path = os.path.join(OUTPUT_DIR, f"{file_name}.{file_extension}")
152+
cls._write_to_file(content, file_path)
153+
file_url = f"{os.getenv('FILESERVER_URL_PREFIX')}/{file_path}"
154+
return file_url

templates/types/multiagent/fastapi/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ python-dotenv = "^1.0.0"
1818
uvicorn = { extras = ["standard"], version = "^0.23.2" }
1919
cachetools = "^5.3.3"
2020
aiostream = "^0.5.2"
21+
markdown = "^3.7"
22+
reportlab = "^4.2.2"
2123

2224
[tool.poetry.dependencies.docx2txt]
2325
version = "^0.8"

0 commit comments

Comments
 (0)