1use headless_lms_chatbot::{
2 azure_chatbot::InputItem, cms_ai_suggestion::USER_PROMPT_PREFIX,
3 llm_utils::AzureCompletionRequest, message_suggestion::USER_PROMPT,
4};
5use regex::Regex;
6
7use crate::prelude::*;
8
9fn get_response(base_url: String) -> Result<String, ControllerError> {
10 let url1 = format!("{base_url}/api/v0/mock-document-storage/test/documents/document1");
11 let url2 = format!("{base_url}/api/v0/mock-document-storage/test/documents/document2");
12 let url3 = format!("{base_url}/api/v0/mock-document-storage/test/documents/document3");
13
14 let re = Regex::new(r"!URLS!")
15 .map_err(|e| controller_err!(InternalServerError, "Error in mock azure endpoint: ", e))?;
16 let res = re
17 .replace_all(
18 RESPONSE,
19 &format!("\\\"{url1}\\\",\\\"{url2}\\\",\\\"{url3}\\\""),
20 )
21 .to_string();
22
23 Ok(res)
24}
25
26const RESPONSE: &str = r#"
27event: response.created
28data: {"type": "response.created","response": {"id": "resp_0","object": "response","created_at": 1774260901,"status": "in_progress","background": false,"completed_at": null,"content_filters": null,"error": null,"frequency_penalty": 0.0,"incomplete_details": null,"instructions": null,"max_output_tokens": null,"max_tool_calls": null,"model": "mock-gpt","output": [],"parallel_tool_calls": true,"presence_penalty": 0.0,"previous_response_id": null,"prompt_cache_key": null,"prompt_cache_retention": null,"reasoning": {"effort": "medium","summary": null},"safety_identifier": null,"service_tier": "auto","store": true,"temperature": 1.0,"text": {"format": {"type": "text"},"verbosity": "medium"},"tool_choice": null,"tools": [{"type": "azure_ai_search","azure_ai_search": {"indexes": [{"project_connection_id": "connection-id","index_name": "mock-index","query_type": "semantic","top_k": 5}]}}],"top_logprobs": 0,"top_logprobs": 0,"top_p": 0.85,"truncation": "disabled","usage": null,"user": null,"metadata": {}},"sequence_number": 0}
29
30event: response.in_progress
31data: {"type": "response.in_progress","response": {"id": "resp_0","object": "response","created_at": 1774260901,"status": "in_progress","background": false,"completed_at": null,"content_filters": null,"error": null,"frequency_penalty": 0.0,"incomplete_details": null,"instructions": null,"max_output_tokens": null,"max_tool_calls": null,"model": "mock-gpt","output": [],"parallel_tool_calls": true,"presence_penalty": 0.0,"previous_response_id": null,"prompt_cache_key": null,"prompt_cache_retention": null,"reasoning": {"effort": "medium","summary": null},"safety_identifier": null,"service_tier": "auto","store": true,"temperature": 1.0,"text": {"format": {"type": "text"},"verbosity": "medium"},"tool_choice": null,"tools": [{"type": "azure_ai_search","azure_ai_search": {"indexes": [{"project_connection_id": "connection-id","index_name": "mock-index","query_type": "semantic","top_k": 5}]}}],"top_logprobs": 0,"top_logprobs": 0,"top_p": 0.85,"truncation": "disabled","usage": null,"user": null,"metadata": {}},"sequence_number": 1}
32
33event: response.output_item.added
34data: {"type": "response.output_item.added","item": {"type": "reasoning","id": "rs_0","response_id": "resp_0","summary": []},"output_index": 0,"sequence_number": 2}
35
36event: response.output_item.done
37data: {"type": "response.output_item.done","item": {"type": "reasoning","id": "rs_0","response_id": "resp_0","summary": []},"output_index": 0,"sequence_number": 3}
38
39event: response.output_item.added
40data: {"type": "response.output_item.added","item": {"type": "azure_ai_search_call","id": "fc_0","response_id": "resp_0","call_id": "call_0","arguments": "","status": "in_progress"},"output_index": 1,"sequence_number": 4}
41
42event: response.output_item.done
43data: {"type": "response.output_item.done","item": {"type": "azure_ai_search_call","id": "fc_0","response_id": "resp_0","call_id": "call_0","arguments": "{\"query\":\"tell me more\"}","status": "completed"},"output_index": 1,"sequence_number": 5}
44
45event: response.output_item.added
46data: {"type": "response.output_item.added","item": {"type": "azure_ai_search_call_output","id": "fco_0","response_id": "resp_0","call_id": "call_0","output": "[]","status": "in_progress"},"output_index": 2,"sequence_number": 6}
47
48event: response.output_item.done
49data: {"type": "response.output_item.done","item": {"type": "azure_ai_search_call_output","id": "fco_0","response_id": "resp_0","call_id": "call_0","output": "{\"documents\": [{\"id\": \"doc1\", \"content\": \"This chunk is a snippet from page {} of the course {}. Mock test page content This is test content blah\", \"filepath\": \"document1\", \"title\": \"Cited course page\", \"url\": \"\",\"score\": 0.016666668, \"knowledgeSourceIndex\": 0},{\"id\": \"doc2\",\"content\": \"Mock test page content 2 This is another test page.\",\"filepath\": \"document2\",\"title\": \"Cited course page 2\",\"url\": \"\",\"score\": 0.016666668,\"knowledgeSourceIndex\": 0},{\"id\": \"doc3\",\"content\": \"More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long.\",\"filepath\": \"document1\",\"title\": \"Cited course page\",\"url\": \"\",\"score\": 0.016666668,\"knowledgeSourceIndex\": 0}],\"get_urls\": [!URLS!]}","status": "completed"},"output_index": 2,"sequence_number": 7}
50
51event: response.output_item.added
52data: {"type": "response.output_item.added","item": {"type": "message","id": "msg_0","response_id": "resp_0","role": "assistant","content": [],"status": "in_progress"},"output_index": 3,"sequence_number": 8}
53
54event: response.content_part.added
55data: {"type": "response.content_part.added","content_index": 0,"item_id": "msg_0","output_index": 3,"part": {"type": "output_text","annotations": [],"logprobs": [],"text": ""},"sequence_number": 9}
56
57event: response.output_text.delta
58data: {"type": "response.output_text.delta","content_index": 0,"delta": "Hello","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 10}
59
60event: response.output_text.delta
61data: {"type": "response.output_text.delta","content_index": 0,"delta": "!","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 11}
62
63event: response.output_text.delta
64data: {"type": "response.output_text.delta","content_index": 0,"delta": " How","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 12}
65
66event: response.output_text.delta
67data: {"type": "response.output_text.delta","content_index": 0,"delta": " can","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 13}
68
69event: response.output_text.delta
70data: {"type": "response.output_text.delta","content_index": 0,"delta": " I","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 14}
71
72event: response.output_text.delta
73data: {"type": "response.output_text.delta","content_index": 0,"delta": " assist","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 15}
74
75event: response.output_text.delta
76data: {"type": "response.output_text.delta","content_index": 0,"delta": " 【0:3†source】","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 16}
77
78event: response.output_text.delta
79data: {"type": "response.output_text.delta","content_index": 0,"delta": " you","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 17}
80
81event: response.output_text.delta
82data: {"type": "response.output_text.delta","content_index": 0,"delta": " 【0:2†source】","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 18}
83
84event: response.output_text.delta
85data: {"type": "response.output_text.delta","content_index": 0,"delta": " today","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 19}
86
87event: response.output_text.delta
88data: {"type": "response.output_text.delta","content_index": 0,"delta": "?","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 20}
89
90event: response.output_text.delta
91data: {"type": "response.output_text.delta","content_index": 0,"delta": "【0:3†source】","item_id": "msg_0","logprobs": [],"obfuscation": "","output_index": 3,"sequence_number": 21}
92
93event: response.output_text.done
94data: {"type": "response.output_text.done","content_index": 0,"item_id": "msg_0","logprobs": [],"output_index": 3,"sequence_number": 22,"text": "Hello! How can I assist 【0:3†source】 you 【0:2†source】 today? 【0:3†source】"}
95
96event: response.content_part.done
97data: {"type": "response.content_part.done","content_index": 0,"item_id": "msg_0","output_index": 3,"part": {"type": "output_text","annotations": [],"logprobs": [],"text": "Hello! How can I assist 【0:3†source】 you 【0:2†source】 today? 【0:3†source】"},"sequence_number": 23}
98
99event: response.output_item.done
100data: {"type": "response.output_item.done","item": {"type": "message","id": "msg_0","response_id": "resp_0","role": "assistant","content": [{"type": "output_text","text": "Hello! How can I assist 【0:3†source】 you 【0:2†source】 today? 【0:3†source】","annotations": [],"logprobs": []}],"status": "completed"},"output_index": 3,"sequence_number": 24}
101
102event: response.completed
103data: {"type": "response.completed","response": {"id": "resp_0","object": "response","created_at": 1774422684,"status": "completed","background": false,"completed_at": 1774422685,"content_filters": [{"blocked": false,"source_type": "prompt","content_filter_raw": [],"content_filter_results": {"jailbreak": {"filtered": false,"detected": false},"self_harm": {"filtered": false,"severity": "safe"},"hate": {"filtered": false,"severity": "safe"},"violence": {"filtered": false,"severity": "safe"},"sexual": {"filtered": false,"severity": "safe"}},"content_filter_offsets": {"start_offset": 918,"end_offset": 930,"check_offset": 0}}],"error": null,"frequency_penalty": 0.0,"incomplete_details": null,"instructions": null,"max_output_tokens": null,"max_tool_calls": null,"model": "gpt-5.4-nano","output": [{"type": "reasoning","id": "rs_0","response_id": "resp_0","summary": []},{"type": "azure_ai_search_call","id": "fc_0","response_id": "resp_0","call_id": "call_0","arguments": "{\"query\":\"tell me more\"}","status": "completed"},{"type": "azure_ai_search_call_output","id": "fco_0","response_id": "resp_0","call_id": "call_0","output": "{\"documents\":[{\"id\": \"doc1\",\"content\": \"This chunk is a snippet from page {} of the course {}. ,|||,Mock test page content\n This is test content blah\",\"filepath\": \"document1\",\"title\": \"Cited course page\",\"url\": \"\",\"score\": 0.016666668,\"knowledgeSourceIndex\": 0},{\"id\": \"doc2\",\"content\": \"Mock test page content 2\n This is another test page.\",\"filepath\": \"document2\",\"title\": \"Cited course page 2\",\"url\": \"\",\"score\": 0.016666668,\"knowledgeSourceIndex\": 0},{\"id\": \"doc3\",\"content\": \"More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long. More content on the same mock course page. Another snippet. Long.\",\"filepath\": \"document1\",\"title\": \"Cited course page\",\"url\": \"\",\"score\": 0.016666668,\"knowledgeSourceIndex\": 0},],\"get_urls\":[!URLS!]}","status": "completed"},{"type": "message","id": "msg_0","response_id": "resp_0","phase": "final_answer","role": "assistant","content": [{"type": "output_text","text": "Hello! How can I assist 【0:3†source】 you 【0:2†source】 today? 【0:3†source】","annotations": [],"logprobs": []}],"status": "completed"}],"parallel_tool_calls": true,"presence_penalty": 0.0,"previous_response_id": null,"prompt_cache_key": null,"prompt_cache_retention": null,"reasoning": {"effort": "high","summary": null},"safety_identifier": null,"service_tier": "auto","store": true,"temperature": 1.0,"text": {"format": {"type": "text"},"verbosity": "medium"},"tool_choice": "required","tools": [{"type": "azure_ai_search","azure_ai_search": {"indexes": [{"project_connection_id": "connection-id","index_name": "mock-index","query_type": "semantic","top_k": 5}]}}],"top_logprobs": 0,"top_logprobs": 0,"top_p": 0.85,"truncation": "disabled","usage": {"input_tokens": 38,"input_tokens_details": {"cached_tokens": 0},"output_tokens": 79,"output_tokens_details": {"reasoning_tokens": 64},"total_tokens": 117},"user": null,"metadata": {}},"sequence_number": 25}
104"#;
105
106const SUGGESTION: &str = r#"{"metadata": {},"top_logprobs": 0,"temperature": 1,"top_p": 0.98,"service_tier": "default","model": "mock-gpt","reasoning": {"effort": "medium","summary": "detailed"},"background": false,"text": {"format": {"type": "text"},"verbosity": "medium"},"tools": [],"tool_choice": "auto","truncation": "disabled","id": "resp_0","object": "response","status": "completed","created_at": 1776144780,"completed_at": 1776144781,"error": null,"incomplete_details": null,"output": [{"type": "message","id": "msg_0","response_id": "resp_0","phase": "final_answer","role": "assistant","content": [{"type": "output_text","text": "{\"suggestions\":[\"Can you pls help me?\",\"Nice weather we're having.\",\"Hello?\"]}","annotations": [],"logprobs": []}],"status": "completed"}],"instructions": null,"usage": {"input_tokens": 30,"input_tokens_details": {"cached_tokens": 0},"output_tokens": 15,"output_tokens_details": {"reasoning_tokens": 0},"total_tokens": 45},"parallel_tool_calls": true,"agent_reference": null}
107"#;
108
109const CMS_SUGGESTION: &str = r#"{"metadata": {},"top_logprobs": 0,"temperature": 1,"top_p": 0.98,"service_tier": "default","model": "mock-gpt","reasoning": {"effort": "medium","summary": "detailed"},"background": false,"text": {"format": {"type": "text"},"verbosity": "medium"},"tools": [],"tool_choice": "auto","truncation": "disabled","id": "resp_0","object": "response","status": "completed","created_at": 1776144780,"completed_at": 1776144781,"error": null,"incomplete_details": null,"output": [{"type": "message","id": "msg_0","response_id": "resp_0","phase": "final_answer","role": "assistant","content": [{"type": "output_text","text": "{\"suggestions\":[\"Mock suggestion 1: The paragraph has been improved.\",\"Mock suggestion 2: Here is an alternative version of the paragraph.\",\"Mock suggestion 3: A third distinct rewrite of the paragraph.\"]}","annotations": [],"logprobs": []}],"status": "completed"}],"instructions": null,"usage": {"input_tokens": 30,"input_tokens_details": {"cached_tokens": 0},"output_tokens": 15,"output_tokens_details": {"reasoning_tokens": 0},"total_tokens": 45},"parallel_tool_calls": true,"agent_reference": null}"#;
110
111async fn mock_azure_chat_responses(
114 app_conf: web::Data<ApplicationConfiguration>,
115 payload: web::Json<AzureCompletionRequest>,
116) -> ControllerResult<String> {
117 assert!(app_conf.test_chatbot && app_conf.test_mode);
118 let message_suggestion_user_prompt = USER_PROMPT;
119
120 let message_kind = &payload
121 .base
122 .input
123 .last()
124 .ok_or(ControllerError::new(
125 ControllerErrorType::BadRequest,
126 "No messages in request, there should be at least one.",
127 None,
128 ))?
129 .message_type;
130 let message = match message_kind {
131 InputItem::Message {
132 role: _role,
133 content,
134 } => Ok(content.to_owned()),
135 _ => Err(ControllerError::new(
136 ControllerErrorType::BadRequest,
137 "The request had a tool call or tool response. This shouldn't happen when using message suggestion LLM.",
138 None,
139 )),
140 }?.get_content_text();
141
142 let suggest_prompt_match = message
143 .matches(message_suggestion_user_prompt)
144 .collect::<Vec<&str>>();
145 let cms_suggest_match = message.contains(USER_PROMPT_PREFIX);
146 let res = if !suggest_prompt_match.is_empty() {
147 SUGGESTION.to_string()
148 } else if cms_suggest_match {
149 CMS_SUGGESTION.to_string()
150 } else {
151 get_response(app_conf.base_url.clone())?
152 };
153 let token = skip_authorize();
154 token.authorized_ok(res)
155}
156
157pub fn _add_routes(cfg: &mut ServiceConfig) {
158 cfg.route(
159 "/test/v1/responses",
160 web::get().to(mock_azure_chat_responses),
161 )
162 .route(
163 "/test/v1/responses",
164 web::post().to(mock_azure_chat_responses),
165 );
166}