@@ -35,6 +35,7 @@ def __init__(
35
35
model : str = None ,
36
36
):
37
37
self .document_service = document_service
38
+ self .sources = {}
38
39
# Load settings
39
40
self .settings = get_settings ()
40
41
self .model = model or self .settings .AGENT_MODEL
@@ -56,7 +57,6 @@ def __init__(
56
57
}
57
58
)
58
59
59
- # TODO: Evaluate and improve the prompt here please!
60
60
# System prompt
61
61
self .system_prompt = """
62
62
You are Morphik, an intelligent research assistant. You can use the following tools to help answer user queries:
@@ -68,20 +68,70 @@ def __init__(
68
68
- list_graphs: list available knowledge graphs
69
69
- save_to_memory: save important information to persistent memory
70
70
- list_documents: list documents accessible to you
71
+
71
72
Use function calls to invoke these tools when needed. When you have gathered all necessary information,
72
- provide a clear, concise final answer. Include all relevant details and cite your sources.
73
- Always use markdown formatting.
73
+ instead of providing a direct text response, you must return a structured response with display objects.
74
+
75
+ Your response should be a JSON array of display objects, each with:
76
+ 1. "type": either "text" or "image"
77
+ 2. "content": for text objects, this is markdown content; for image objects, this is a base64-encoded image
78
+ 3. "source": the source ID of the chunk where you found this information
79
+
80
+ Example response format:
81
+ ```json
82
+ [
83
+ {
84
+ "type": "text",
85
+ "content": "## Introduction to the Topic\n Here is some detailed information...",
86
+ "source": "doc123-chunk1"
87
+ },
88
+ {
89
+ "type": "text",
90
+ "content": "This analysis shows that...",
91
+ "source": "doc456-chunk2"
92
+ }
93
+ ]
94
+ ```
95
+
96
+ When you use retrieve_chunks, you'll get source IDs for each chunk. Use these IDs in your response.
97
+ For example, if you see "Source ID: doc123-chunk4" for important information, attribute it in your response.
98
+
99
+ Always attribute the information to its specific source. Break your response into multiple display objects
100
+ when citing different sources. Use markdown formatting for text content to improve readability.
74
101
""" .strip ()
75
102
76
103
async def _execute_tool (self , name : str , args : dict , auth : AuthContext ):
77
104
"""Dispatch tool calls, injecting document_service and auth."""
78
105
match name :
79
106
case "retrieve_chunks" :
80
- return await retrieve_chunks (document_service = self .document_service , auth = auth , ** args )
107
+ content , sources = await retrieve_chunks (document_service = self .document_service , auth = auth , ** args )
108
+ self .sources .update (sources )
109
+ return content
81
110
case "retrieve_document" :
82
- return await retrieve_document (document_service = self .document_service , auth = auth , ** args )
111
+ result = await retrieve_document (document_service = self .document_service , auth = auth , ** args )
112
+ # Add document as a source if it's a successful retrieval
113
+ if isinstance (result , str ) and not result .startswith ("Document" ) and not result .startswith ("Error" ):
114
+ doc_id = args .get ("document_id" , "unknown" )
115
+ source_id = f"doc{ doc_id } -full"
116
+ self .sources [source_id ] = {
117
+ "document_id" : doc_id ,
118
+ "document_name" : f"Full Document { doc_id } " ,
119
+ "chunk_number" : "full" ,
120
+ }
121
+ return result
83
122
case "document_analyzer" :
84
- return await document_analyzer (document_service = self .document_service , auth = auth , ** args )
123
+ result = await document_analyzer (document_service = self .document_service , auth = auth , ** args )
124
+ # Track document being analyzed as a source
125
+ if args .get ("document_id" ):
126
+ doc_id = args .get ("document_id" )
127
+ analysis_type = args .get ("analysis_type" , "analysis" )
128
+ source_id = f"doc{ doc_id } -{ analysis_type } "
129
+ self .sources [source_id ] = {
130
+ "document_id" : doc_id ,
131
+ "document_name" : f"Document { doc_id } ({ analysis_type } )" ,
132
+ "analysis_type" : analysis_type ,
133
+ }
134
+ return result
85
135
case "execute_code" :
86
136
res = await execute_code (** args )
87
137
return res ["content" ]
@@ -133,8 +183,125 @@ async def run(self, query: str, auth: AuthContext) -> str:
133
183
# If no tool call, return final content
134
184
if not getattr (msg , "tool_calls" , None ):
135
185
logger .info ("No tool calls detected, returning final content" )
136
- # Return final content and the history
137
- return msg .content , tool_history
186
+
187
+ # Parse the response as display objects if possible
188
+ display_objects = []
189
+ default_text = ""
190
+
191
+ try :
192
+ # Check if the response is JSON formatted
193
+ import re
194
+
195
+ # Try to extract JSON content if present using a regex pattern for common JSON formats
196
+ json_pattern = r'\[\s*{.*}\s*\]|\{\s*".*"\s*:.*\}'
197
+ json_match = re .search (json_pattern , msg .content , re .DOTALL )
198
+
199
+ if json_match :
200
+ potential_json = json_match .group (0 )
201
+ parsed_content = json .loads (potential_json )
202
+
203
+ # Handle both array and object formats
204
+ if isinstance (parsed_content , list ):
205
+ for item in parsed_content :
206
+ if isinstance (item , dict ) and "type" in item and "content" in item :
207
+ # Convert to standardized display object format
208
+ display_obj = {
209
+ "type" : item .get ("type" , "text" ),
210
+ "content" : item .get ("content" , "" ),
211
+ "source" : item .get ("source" , "agent-response" ),
212
+ }
213
+ if "caption" in item and item ["type" ] == "image" :
214
+ display_obj ["caption" ] = item ["caption" ]
215
+ if item ["type" ] == "image" :
216
+ display_obj ["content" ] = self .sources [item ["source" ]]["content" ]
217
+ display_objects .append (display_obj )
218
+ elif (
219
+ isinstance (parsed_content , dict )
220
+ and "type" in parsed_content
221
+ and "content" in parsed_content
222
+ ):
223
+ # Single display object
224
+ display_obj = {
225
+ "type" : parsed_content .get ("type" , "text" ),
226
+ "content" : parsed_content .get ("content" , "" ),
227
+ "source" : parsed_content .get ("source" , "agent-response" ),
228
+ }
229
+ if "caption" in parsed_content and parsed_content ["type" ] == "image" :
230
+ display_obj ["caption" ] = parsed_content ["caption" ]
231
+ if item ["type" ] == "image" :
232
+ display_obj ["content" ] = self .sources [item ["source" ]]["content" ]
233
+ display_objects .append (display_obj )
234
+
235
+ # If no display objects were created, treat the entire content as text
236
+ if not display_objects :
237
+ default_text = msg .content
238
+ except (json .JSONDecodeError , ValueError ) as e :
239
+ logger .warning (f"Failed to parse response as JSON: { e } " )
240
+ default_text = msg .content
241
+
242
+ # If no structured display objects were found, create a default text object
243
+ if not display_objects and default_text :
244
+ display_objects .append ({"type" : "text" , "content" : default_text , "source" : "agent-response" })
245
+
246
+ # Create sources from the collected source IDs in display objects
247
+ sources = []
248
+ seen_source_ids = set ()
249
+
250
+ for obj in display_objects :
251
+ source_id = obj .get ("source" )
252
+ if source_id and source_id != "agent-response" and source_id not in seen_source_ids :
253
+ seen_source_ids .add (source_id )
254
+ # Extract document info from source ID if available
255
+ if "-" in source_id :
256
+ parts = source_id .split ("-" , 1 )
257
+ doc_id = parts [0 ].replace ("doc" , "" )
258
+ sources .append (
259
+ {
260
+ "sourceId" : source_id ,
261
+ "documentName" : f"Document { doc_id } " ,
262
+ "documentId" : doc_id ,
263
+ "content" : self .sources .get (source_id , {"content" : "" })["content" ],
264
+ }
265
+ )
266
+ else :
267
+ sources .append (
268
+ {
269
+ "sourceId" : source_id ,
270
+ "documentName" : "Referenced Source" ,
271
+ "documentId" : "unknown" ,
272
+ "content" : self .sources .get (source_id , {"content" : "" })["content" ],
273
+ }
274
+ )
275
+
276
+ # Add agent response source if not already included
277
+ if "agent-response" not in seen_source_ids :
278
+ sources .append (
279
+ {
280
+ "sourceId" : "agent-response" ,
281
+ "documentName" : "Agent Response" ,
282
+ "documentId" : "system" ,
283
+ "content" : msg .content ,
284
+ }
285
+ )
286
+
287
+ # Add sources from document chunks used during the session
288
+ for source_id , source_info in self .sources .items ():
289
+ if source_id not in seen_source_ids :
290
+ sources .append (
291
+ {
292
+ "sourceId" : source_id ,
293
+ "documentName" : source_info .get ("document_name" , "Unknown Document" ),
294
+ "documentId" : source_info .get ("document_id" , "unknown" ),
295
+ }
296
+ )
297
+
298
+ # Return final content, tool history, display objects and sources
299
+ return {
300
+ "response" : msg .content ,
301
+ "tool_history" : tool_history ,
302
+ "display_objects" : display_objects ,
303
+ "sources" : sources ,
304
+ }
138
305
139
306
call = msg .tool_calls [0 ]
140
307
name = call .function .name
0 commit comments