Skip to content

Commit 7f8fac3

Browse files
agent sdk into chart generation
1 parent 12f619d commit 7f8fac3

File tree

3 files changed

+176
-35
lines changed

3 files changed

+176
-35
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from azure.identity import DefaultAzureCredential
2+
from azure.ai.projects import AIProjectClient
3+
4+
from agents.agent_factory_base import BaseAgentFactory
5+
6+
7+
class ChartAgentFactory(BaseAgentFactory):
8+
"""
9+
Factory class for creating Chart agents that generate chart.js compatible JSON
10+
based on numerical and structured data from RAG responses.
11+
"""
12+
13+
@classmethod
14+
async def create_agent(cls, config):
15+
"""
16+
Asynchronously creates an AI agent configured to convert structured data
17+
into chart.js-compatible JSON using Azure AI Project.
18+
19+
Args:
20+
config: Configuration object containing AI project and model settings.
21+
22+
Returns:
23+
dict: A dictionary containing the created 'agent' and its associated 'client'.
24+
"""
25+
instructions = '''You are an assistant that generates valid Chart.js v4.4.4 compatible JSON.
26+
Your goal is to produce a JSON object that includes:
27+
- `type` (chart type: bar, line, pie, etc.)
28+
- `data` (with `labels` and `datasets`)
29+
- `options` (to enhance rendering and clarity)
30+
Important Rules:
31+
- Combine both the user's query and the provided tabular/textual data to choose the best chart type.
32+
- Only generate a chart if the data contains numbers.
33+
- If no numbers are found, return:
34+
{"error": "Chart cannot be generated due to lack of numerical data."}
35+
- Do NOT include any explanations, markdown formatting, or tooltips — just clean JSON.
36+
- Remove all trailing commas.
37+
- Ensure the JSON can be parsed using `json.loads()` in Python.
38+
- Ensure axis ticks are readable (adjust `ticks.padding`, `maxWidth`, etc.).
39+
- Avoid bars being too narrow or cropped by setting reasonable `barPercentage` and `categoryPercentage`.
40+
'''
41+
42+
project_client = AIProjectClient(
43+
endpoint=config.ai_project_endpoint,
44+
credential=DefaultAzureCredential(exclude_interactive_browser_credential=False),
45+
api_version=config.ai_project_api_version,
46+
)
47+
48+
agent = project_client.agents.create_agent(
49+
model=config.azure_openai_deployment_model,
50+
name=f"KM-ChartAgent-{config.solution_name}",
51+
instructions=instructions,
52+
)
53+
54+
return {
55+
"agent": agent,
56+
"client": project_client
57+
}
58+
59+
@classmethod
60+
async def _delete_agent_instance(cls, agent_wrapper: dict):
61+
"""
62+
Asynchronously deletes the specified chart agent instance from the Azure AI project.
63+
64+
Args:
65+
agent_wrapper (dict): Dictionary containing the 'agent' and 'client' to be removed.
66+
"""
67+
agent_wrapper["client"].agents.delete_agent(agent_wrapper["agent"].id)

src/api/app.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from agents.conversation_agent_factory import ConversationAgentFactory
1818
from agents.search_agent_factory import SearchAgentFactory
1919
from agents.sql_agent_factory import SQLAgentFactory
20+
from agents.chart_agent_factory import ChartAgentFactory
2021
from api.api_routes import router as backend_router
2122
from api.history_routes import router as history_router
2223

@@ -34,13 +35,16 @@ async def lifespan(fastapi_app: FastAPI):
3435
fastapi_app.state.agent = await ConversationAgentFactory.get_agent()
3536
fastapi_app.state.search_agent = await SearchAgentFactory.get_agent()
3637
fastapi_app.state.sql_agent = await SQLAgentFactory.get_agent()
38+
fastapi_app.state.chart_agent=await ChartAgentFactory.get_agent()
3739
yield
3840
await ConversationAgentFactory.delete_agent()
3941
await SearchAgentFactory.delete_agent()
4042
await SQLAgentFactory.delete_agent()
43+
await ChartAgentFactory.delete_agent()
4144
fastapi_app.state.sql_agent = None
4245
fastapi_app.state.search_agent = None
4346
fastapi_app.state.agent = None
47+
fastapi_app.state.chart_agent = None
4448

4549

4650
def build_app() -> FastAPI:

src/api/services/chat_service.py

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
from semantic_kernel.agents import AzureAIAgentThread
2222
from semantic_kernel.exceptions.agent_exceptions import AgentException
2323

24-
from azure.ai.agents.models import TruncationObject
24+
from azure.ai.agents.models import TruncationObject, MessageRole, ListSortOrder
2525

2626
from cachetools import TTLCache
2727

2828
from helpers.utils import format_stream_response
2929
from helpers.azure_openai_helper import get_azure_openai_client
3030
from common.config.config import Config
31+
from agents.chart_agent_factory import ChartAgentFactory
3132

3233
# Constants
3334
HOST_NAME = "CKM"
@@ -86,49 +87,118 @@ def __init__(self, request : Request):
8687
if ChatService.thread_cache is None:
8788
ChatService.thread_cache = ExpCache(maxsize=1000, ttl=3600.0, agent=self.agent)
8889

89-
def process_rag_response(self, rag_response, query):
90+
async def process_rag_response(self, rag_response, query):
9091
"""
91-
Parses the RAG response dynamically to extract chart data for Chart.js.
92+
Uses the ChartAgent directly (agentic call) to extract chart data for Chart.js.
9293
"""
9394
try:
94-
client = get_azure_openai_client()
95-
96-
system_prompt = """You are an assistant that helps generate valid chart data to be shown using chart.js with version 4.4.4 compatible.
97-
Include chart type and chart options.
98-
Pick the best chart type for given data.
99-
Do not generate a chart unless the input contains some numbers. Otherwise return a message that Chart cannot be generated.
100-
Only return a valid JSON output and nothing else.
101-
Verify that the generated JSON can be parsed using json.loads.
102-
Do not include tooltip callbacks in JSON.
103-
Always make sure that the generated json can be rendered in chart.js.
104-
Always remove any extra trailing commas.
105-
Verify and refine that JSON should not have any syntax errors like extra closing brackets.
106-
Ensure Y-axis labels are fully visible by increasing **ticks.padding**, **ticks.maxWidth**, or enabling word wrapping where necessary.
107-
Ensure bars and data points are evenly spaced and not squished or cropped at **100%** resolution by maintaining appropriate **barPercentage** and **categoryPercentage** values."""
108-
user_prompt = f"""Generate chart data for -
109-
{query}
110-
{rag_response}
111-
"""
112-
logger.info(">>> Processing chart data for response: %s", rag_response)
113-
114-
completion = client.chat.completions.create(
115-
model=self.azure_openai_deployment_name,
116-
messages=[
117-
{"role": "system", "content": system_prompt},
118-
{"role": "user", "content": user_prompt},
119-
],
120-
temperature=0,
95+
combined_input = f"{query}\n{rag_response}"
96+
97+
agent_info = await ChartAgentFactory.get_agent()
98+
agent = agent_info["agent"]
99+
client = agent_info["client"]
100+
101+
thread = client.agents.threads.create()
102+
103+
client.agents.messages.create(
104+
thread_id=thread.id,
105+
role=MessageRole.USER,
106+
content=combined_input
107+
)
108+
109+
print(f"thread with id:{thread.id}",flush=True)
110+
print(f"agent id:{agent.id}",flush=True)
111+
print(f"project clinet :{client}",flush=True)
112+
113+
run = client.agents.runs.create_and_process(
114+
thread_id=thread.id,
115+
agent_id=agent.id
121116
)
122117

123-
chart_data = completion.choices[0].message.content.strip().replace("```json", "").replace("```", "")
124-
logger.info(">>> Generated chart data: %s", chart_data)
118+
if run.status == "failed":
119+
print(f"[Chart Agent] Run failed: {run.last_error}")
120+
return {"error": "Chart could not be generated due to agent failure."}
125121

126-
return json.loads(chart_data)
122+
chart_json = ""
123+
messages = client.agents.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
124+
for msg in messages:
125+
if msg.role == MessageRole.AGENT and msg.text_messages:
126+
chart_json = msg.text_messages[-1].text.value.strip()
127+
break
128+
129+
client.agents.threads.delete(thread_id=thread.id)
130+
131+
chart_json = chart_json.replace("```json", "").replace("```", "").strip()
132+
chart_data = json.loads(chart_json)
133+
134+
if not chart_data or "error" in chart_data:
135+
return {
136+
"error": chart_data.get("error", "Chart could not be generated from this data."),
137+
"hint": "Try asking a question with some numerical values, like 'sales per region' or 'calls per day'."
138+
}
139+
140+
return chart_data
127141

128142
except Exception as e:
129-
logger.error("Error processing RAG response: %s", e)
143+
logger.error("Agent error in chart generation: %s", e)
130144
return {"error": "Chart could not be generated from this data. Please ask a different question."}
131145

146+
147+
# async def run_agent():
148+
# chart_data = {"error": "Chart could not be generated."}
149+
# try:
150+
# agent_info = await ChartAgentFactory.get_agent()
151+
# agent = agent_info["agent"]
152+
# client = agent_info["client"]
153+
154+
# thread = client.agents.threads.create()
155+
# client.agents.messages.create(
156+
# thread_id=thread.id,
157+
# role=MessageRole.USER,
158+
# content=combined_input
159+
# )
160+
161+
# print(f"thread with id:{thread.id}",flush=True)
162+
# print(f"agent id:{agent.id}",flush=True)
163+
# print(f"project clinet :{client}",flush=True)
164+
165+
# run = client.agents.runs.create_and_process(
166+
# thread_id=thread.id,
167+
# agent_id=agent.id
168+
# )
169+
170+
# if run.status == "failed":
171+
# print(f"[Chart Agent] Run failed: {run.last_error}")
172+
# return {"error": "Chart could not be generated due to agent failure."}
173+
174+
# chart_json = ""
175+
# messages = client.agents.messages.list(thread_id=thread.id, order=ListSortOrder.ASCENDING)
176+
# for msg in messages:
177+
# if msg.role == MessageRole.AGENT and msg.text_messages:
178+
# chart_json = msg.text_messages[-1].text.value.strip()
179+
# break
180+
181+
# chart_json = chart_json.replace("```json", "").replace("```", "").strip()
182+
# client.agents.threads.delete(thread_id=thread.id)
183+
184+
# chart_data = json.loads(chart_json)
185+
# except Exception as e:
186+
# print(f"[Chart Agent Error]: {e}")
187+
# return chart_data
188+
189+
# # Run the async agent call synchronously
190+
# loop = asyncio.get_event_loop()
191+
# chart_data = loop.run_until_complete(run_agent())
192+
193+
# if not chart_data or "error" in chart_data:
194+
# return {"error": "Chart could not be generated from this data. Please ask a different question."}
195+
196+
# return chart_data
197+
198+
# except Exception as e:
199+
# logger.error("Agent error in chart generation: %s", e)
200+
# return {"error": "Chart could not be generated from this data. Please ask a different question."}
201+
132202
async def stream_openai_text(self, conversation_id: str, query: str) -> StreamingResponse:
133203
"""
134204
Get a streaming text response from OpenAI.
@@ -254,7 +324,7 @@ async def complete_chat_request(self, query, last_rag_response=None):
254324
return {"error": "A previous RAG response is required to generate a chart."}
255325

256326
# Process RAG response to generate chart data
257-
chart_data = self.process_rag_response(last_rag_response, query)
327+
chart_data = await self.process_rag_response(last_rag_response, query)
258328

259329
if not chart_data or "error" in chart_data:
260330
return {

0 commit comments

Comments
 (0)