Skip to content

Chatbot switching to v1 responses API#1709

Open
HeljaeRaeisaenen wants to merge 56 commits into
masterfrom
chatbot-new-api
Open

Chatbot switching to v1 responses API#1709
HeljaeRaeisaenen wants to merge 56 commits into
masterfrom
chatbot-new-api

Conversation

@HeljaeRaeisaenen
Copy link
Copy Markdown
Contributor

@HeljaeRaeisaenen HeljaeRaeisaenen commented May 6, 2026

The chatbot now uses the v1 responses API. All models can use tools and course material search. Different kinds of reasoning models are supported, but not all. Models that definitely work include GPT 5.2, GPT 5.4, GPT 4o. Models from other providers can be supported in the future.

Indicating the LLM is performing reasoning can now be implemented in the future, as well as indicating tool use.

Summary by CodeRabbit

  • New Features

    • Azure AI Search integration for retrieving relevant course materials and generating automatic citations
    • Support for multiple LLM model types, including reasoning-capable models for enhanced responses
  • Improvements

    • Unified token and output configuration with streamlined max_output_tokens setting
    • Enhanced message architecture for improved reasoning and tool interaction support

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
services/headless-lms/models/src/chatbot_conversation_message_tool_calls.rs (1)

115-142: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

get_by_message_id still returns at most one row.

Multiple insert calls for the same chatbot_conversation_message_id are still possible (an LLM response can produce parallel tool calls), but this function uses fetch_optional and Option<ChatbotConversationMessageToolCall>, which silently drops all but one row when several exist. Either rename to make the singular contract explicit (and add LIMIT 1), or return Vec<ChatbotConversationMessageToolCall> with fetch_all.

Suggested fix (return all matches)
 pub async fn get_by_message_id(
     conn: &mut PgConnection,
     msg_id: Uuid,
-) -> ModelResult<Option<ChatbotConversationMessageToolCall>> {
+) -> ModelResult<Vec<ChatbotConversationMessageToolCall>> {
     let res = sqlx::query_as!(
         ChatbotConversationMessageToolCall,
         r#"
 SELECT
     id,
     created_at,
     updated_at,
     deleted_at,
     chatbot_conversation_message_id,
     tool_name,
     tool_arguments,
     tool_call_id,
     tool_kind as "tool_kind: ToolKind",
     response_id
 FROM chatbot_conversation_message_tool_calls
 WHERE chatbot_conversation_message_id = $1
   AND deleted_at IS NULL
         "#,
         msg_id
     )
-    .fetch_optional(conn)
+    .fetch_all(conn)
     .await?;
     Ok(res)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/headless-lms/models/src/chatbot_conversation_message_tool_calls.rs`
around lines 115 - 142, The function get_by_message_id currently returns
Option<ChatbotConversationMessageToolCall> and uses fetch_optional, which
silently drops extra rows; change it to return
ModelResult<Vec<ChatbotConversationMessageToolCall>> and replace
fetch_optional(conn).await? with fetch_all(conn).await? so all rows for
chatbot_conversation_message_id are returned; update the function signature and
any callers expecting an Option to handle a Vec instead (alternatively, if you
want a single row contract, explicitly add LIMIT 1 to the SQL and keep the
Option return type).
services/headless-lms/chatbot/src/llm_utils.rs (1)

405-435: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

tool_kind is still not set explicitly, and the error message is outdated.

Two leftover issues from the previous review:

  1. Both Ok(...) arms (lines 413–418, 423–428) use ..Default::default(), which sets tool_kind to the struct's default (ToolKind::Function). The AzureAiSearchCallOutput branch therefore writes an Azure AI Search output back to the DB with tool_kind = Function, diverging from the sibling From impl above and from to_chatbot_conversation_message.
  2. The fallback error string on line 431 still says "APIMessage" (old type name) and "type is not OutputItem::FunctionCallOutput", but the match now also handles AzureAiSearchCallOutput.
Suggested fix
 impl TryFrom<APIOutputMessage> for ChatbotConversationMessageToolOutput {
     type Error = ChatbotError;
     fn try_from(value: APIOutputMessage) -> ChatbotResult<Self> {
         match value.message_type {
             OutputItem::FunctionCallOutput {
                 call_id,
                 output,
                 response_id,
             } => Ok(ChatbotConversationMessageToolOutput {
                 output,
                 tool_call_id: call_id,
                 response_id,
+                tool_kind: ToolKind::Function,
                 ..Default::default()
             }),
             OutputItem::AzureAiSearchCallOutput {
                 response_id,
                 call_id,
                 output,
             } => Ok(ChatbotConversationMessageToolOutput {
                 output,
                 tool_call_id: call_id,
                 response_id,
+                tool_kind: ToolKind::AzureAiSearch,
                 ..Default::default()
             }),
             _ => Err(chatbot_err!(
                 Other,
-                "Can't convert APIMessage to ChatbotConversationMessageToolOutput: APIMessage type is not OutputItem::FunctionCallOutput"
+                "Can't convert APIOutputMessage to ChatbotConversationMessageToolOutput: expected a FunctionCallOutput or AzureAiSearchCallOutput variant"
             )),
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/headless-lms/chatbot/src/llm_utils.rs` around lines 405 - 435, In
the TryFrom<APIOutputMessage> for ChatbotConversationMessageToolOutput
implementation, explicitly set the tool_kind field instead of relying on
..Default::default(): set tool_kind = ToolKind::Function in the
OutputItem::FunctionCallOutput arm and tool_kind = ToolKind::AzureAiSearch in
the OutputItem::AzureAiSearchCallOutput arm (keep output, tool_call_id,
response_id as before), and update the Err message to reference APIOutputMessage
and list both unhandled variants (e.g., "Can't convert APIOutputMessage to
ChatbotConversationMessageToolOutput: message type is not FunctionCallOutput or
AzureAiSearchCallOutput") to reflect the current match arms.
🧹 Nitpick comments (3)
services/main-frontend/src/app/manage/course-instances/[id]/certificates/page.tsx (1)

111-120: ⚡ Quick win

Clarify the relationship between the file field and bodySerializer parameter.

There's an apparent inconsistency in how files are handled:

  • Line 111-112: files array may contain both overlaySvg and backgroundSvg (0-2 files after filtering)
  • Line 118: The file field only includes firstFile (the first of potentially two files) or an empty placeholder
  • Line 120: The bodySerializer receives the full files array

If the bodySerializer performs the actual serialization and uses the files parameter, then the file field in the body might exist only to satisfy type requirements. However, this creates confusion about which data structure is actually sent to the API.

Additionally, the empty File fallback (new File([], "empty-certificate-upload")) suggests the API requires a non-empty array even when no files are uploaded, which may warrant a comment explaining this requirement.

Could you verify:

  1. Does the API now require file to be an array type?
  2. Does bodySerializer override the body completely, or does it use body.file?
  3. Should the file field contain all files (matching the files parameter), or is the current single-file approach correct?

Consider adding a brief comment explaining the relationship between these fields if the current behavior is intentional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/main-frontend/src/app/manage/course-instances/`[id]/certificates/page.tsx
around lines 111 - 120, The code currently builds files and firstFile and passes
files into bodySerializer (createCertificateConfigurationFormData) while the
body.file only contains firstFile or an empty placeholder; verify whether the
API expects an array under the file key and whether
updateCertificateConfiguration's bodySerializer fully replaces the body or reads
body.file; if the API expects all uploaded files, change the body.file to mirror
the files array (or populate placeholders to match required length) and/or
update createCertificateConfigurationFormData to only use body.file
consistently; if the current behavior is intentional, add a short inline comment
near files, firstFile, updateCertificateConfiguration and
createCertificateConfigurationFormData explaining that bodySerializer will be
used for actual multipart serialization and why body.file contains only a single
placeholder to satisfy typing/API requirements.
services/headless-lms/chatbot/src/message_suggestion.rs (1)

194-228: ⚡ Quick win

Dead branches in the cutoff loop: Message::ToolOutput and _ are unreachable.

conversation (lines 184–190) is pre-filtered to Message::Text only, so the Message::ToolOutput branch on lines 205–209 and the fallback _ arm on lines 210–214 can never execute. The ToolOutput token estimation, the "skip this element" fallback, and the // todo: which tool call was it note are all dead code that only adds noise.

Either drop the unreachable arms (simplest) or — if the intent is to also account for tool-call/tool-output tokens in the budget and include them in the rendered context — extend the pre-filter to include those variants and adjust create_msg_string callers accordingly. Right now the budget math silently ignores tool-call/output tokens entirely.

Suggested simplification (drop unreachable arms)
     let cutoff = conversation
         .iter()
         .enumerate()
         // we want to take messages starting from the newest (=last)
         .rev()
         .map_while(|(idx, el)| {
-            match el.message.to_owned()  {
-                Message::Text(msg) => {
-                // add this message's tokens and extra 5 tokens for newline and
-                // tag to used_tokens
-                used_tokens += msg.used_tokens + 5;}
-                Message::ToolOutput(o) => {
-                // add the tokens needed for the tool info to used_tokens
-                let s = format!("Output:\n{}\n\n", o.output); // todo: which tool call was it
-                used_tokens += estimate_tokens(&s);
-                }
-                _ => {
-                    // if there is no message or tool output, skip this element.
-                    // putting in conv_len won't affect the truncation later as we take min.
-                    return Some(conv_len);
-                }
-            }
+            let Message::Text(msg) = &el.message else {
+                // `conversation` is pre-filtered to Text only; this is unreachable.
+                return Some(conv_len);
+            };
+            // +5 for the role tag and surrounding newlines added by create_msg_string
+            used_tokens += msg.used_tokens + 5;

             if used_tokens > token_budget {
                 return None;
             }
             // include this element's index as a potential cutoff point
             Some(idx)
         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/headless-lms/chatbot/src/message_suggestion.rs` around lines 194 -
228, The cutoff loop over conversation contains unreachable arms because
conversation is pre-filtered to only Message::Text; either remove the dead
branches (Message::ToolOutput and the fallback _) and their
token-estimation/comment noise so used_tokens/token_budget math only handles
Message::Text, or update the pre-filtering that builds conversation to include
Message::ToolOutput (and any other variants you want accounted for) and then
update create_msg_string callers to accept and render those variants; locate the
cutoff logic around the variable conversation, the match on Message::{Text,
ToolOutput, _}, the used_tokens and token_budget variables, and either delete
the ToolOutput/_ arms or expand upstream filtering and downstream
create_msg_string handling so token accounting includes tool outputs.
services/headless-lms/chatbot/src/llm_utils.rs (1)

720-731: 💤 Low value

Hard-coded "gpt-5.2-chat" string special-cases a single model.

This bypasses model.model_type and configuration.reasoning_effort for one specific deployment name, which is fragile (typos, deployment-name changes, multiple gpt-5.2 variants all silently fall through to the default branch). If gpt-5.2-chat really needs effort = Medium and summary = Detailed, it would be more robust to either (a) drive this from model.model_type (e.g., a new ModelType variant or a configurable force_medium_effort flag on ChatbotConfigurationModel), or (b) compare against a defined constant rather than an inline literal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/headless-lms/chatbot/src/llm_utils.rs` around lines 720 - 731, The
get_params_for_model function currently special-cases the literal
"gpt-5.2-chat"; instead, change it to use a stable discriminator (either check
model.model_type for a new ModelType::Gpt5_2 variant or add a boolean flag on
ChatbotConfigurationModel such as force_medium_effort) or compare against a
shared constant (e.g., GPT_5_2_CHAT_DEPLOYMENT) rather than an inline string;
update the branch that returns LLMRequestParams::GPTThinking(ThinkingParams {
reasoning: ... }) to read the decision from model.model_type or the new flag (or
constant) and preserve respect for configuration.reasoning_effort when the
special-case is not set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/headless-lms/chatbot/src/llm_utils.rs`:
- Around line 113-142: The Azure AI Search branch in the Message::ToolOutput
match deserializes tool_output.output with serde_json::from_str, causing
asymmetric behavior versus ToolKind::Function; update the AzureAiSearch arm in
the Message::ToolOutput match so it assigns output: tool_output.output (same as
the Function branch) and remove the from_str call, leaving any JSON
deserialization to downstream consumers (e.g., where AiSearchOutput is parsed in
azure_chatbot.rs).

In
`@services/main-frontend/src/app/manage/course-instances/`[id]/certificates/page.tsx:
- Line 118: The certificate file payload was changed to send an array (see the
expression file: [firstFile ?? new File([], "empty-certificate-upload")] in
page.tsx) which looks unrelated to the chatbot v1 responses API migration;
confirm intent by either (A) keeping this change in the current PR only if you
can point to the related API/migration task and update the PR description/tests
to document the certificate API change and its backward compatibility, or (B)
revert this line to the previous single-file shape and move the array-based
certificate payload into a dedicated PR that updates the certificate upload API
and its consumers (update page.tsx, any form handlers, and backend API contract
together). Ensure you reference page.tsx and the form handler that consumes
"firstFile" when making the change so all call sites remain consistent.

---

Duplicate comments:
In `@services/headless-lms/chatbot/src/llm_utils.rs`:
- Around line 405-435: In the TryFrom<APIOutputMessage> for
ChatbotConversationMessageToolOutput implementation, explicitly set the
tool_kind field instead of relying on ..Default::default(): set tool_kind =
ToolKind::Function in the OutputItem::FunctionCallOutput arm and tool_kind =
ToolKind::AzureAiSearch in the OutputItem::AzureAiSearchCallOutput arm (keep
output, tool_call_id, response_id as before), and update the Err message to
reference APIOutputMessage and list both unhandled variants (e.g., "Can't
convert APIOutputMessage to ChatbotConversationMessageToolOutput: message type
is not FunctionCallOutput or AzureAiSearchCallOutput") to reflect the current
match arms.

In `@services/headless-lms/models/src/chatbot_conversation_message_tool_calls.rs`:
- Around line 115-142: The function get_by_message_id currently returns
Option<ChatbotConversationMessageToolCall> and uses fetch_optional, which
silently drops extra rows; change it to return
ModelResult<Vec<ChatbotConversationMessageToolCall>> and replace
fetch_optional(conn).await? with fetch_all(conn).await? so all rows for
chatbot_conversation_message_id are returned; update the function signature and
any callers expecting an Option to handle a Vec instead (alternatively, if you
want a single row contract, explicitly add LIMIT 1 to the SQL and keep the
Option return type).

---

Nitpick comments:
In `@services/headless-lms/chatbot/src/llm_utils.rs`:
- Around line 720-731: The get_params_for_model function currently special-cases
the literal "gpt-5.2-chat"; instead, change it to use a stable discriminator
(either check model.model_type for a new ModelType::Gpt5_2 variant or add a
boolean flag on ChatbotConfigurationModel such as force_medium_effort) or
compare against a shared constant (e.g., GPT_5_2_CHAT_DEPLOYMENT) rather than an
inline string; update the branch that returns
LLMRequestParams::GPTThinking(ThinkingParams { reasoning: ... }) to read the
decision from model.model_type or the new flag (or constant) and preserve
respect for configuration.reasoning_effort when the special-case is not set.

In `@services/headless-lms/chatbot/src/message_suggestion.rs`:
- Around line 194-228: The cutoff loop over conversation contains unreachable
arms because conversation is pre-filtered to only Message::Text; either remove
the dead branches (Message::ToolOutput and the fallback _) and their
token-estimation/comment noise so used_tokens/token_budget math only handles
Message::Text, or update the pre-filtering that builds conversation to include
Message::ToolOutput (and any other variants you want accounted for) and then
update create_msg_string callers to accept and render those variants; locate the
cutoff logic around the variable conversation, the match on Message::{Text,
ToolOutput, _}, the used_tokens and token_budget variables, and either delete
the ToolOutput/_ arms or expand upstream filtering and downstream
create_msg_string handling so token accounting includes tool outputs.

In
`@services/main-frontend/src/app/manage/course-instances/`[id]/certificates/page.tsx:
- Around line 111-120: The code currently builds files and firstFile and passes
files into bodySerializer (createCertificateConfigurationFormData) while the
body.file only contains firstFile or an empty placeholder; verify whether the
API expects an array under the file key and whether
updateCertificateConfiguration's bodySerializer fully replaces the body or reads
body.file; if the API expects all uploaded files, change the body.file to mirror
the files array (or populate placeholders to match required length) and/or
update createCertificateConfigurationFormData to only use body.file
consistently; if the current behavior is intentional, add a short inline comment
near files, firstFile, updateCertificateConfiguration and
createCertificateConfigurationFormData explaining that bodySerializer will be
used for actual multipart serialization and why body.file contains only a single
placeholder to satisfy typing/API requirements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fb912fdb-9e0c-4625-83b1-274306b62cd8

📥 Commits

Reviewing files that changed from the base of the PR and between 898e3da and fd969e1.

📒 Files selected for processing (19)
  • services/cms/Dockerfile.production.slim.dockerfile
  • services/example-exercise/Dockerfile.production.slim.dockerfile
  • services/headless-lms/Dockerfile.production.slim.dockerfile
  • services/headless-lms/chatbot/src/azure_chatbot.rs
  • services/headless-lms/chatbot/src/chatbot_tools/azure_ai_search.rs
  • services/headless-lms/chatbot/src/cms_ai_suggestion.rs
  • services/headless-lms/chatbot/src/content_cleaner.rs
  • services/headless-lms/chatbot/src/llm_utils.rs
  • services/headless-lms/chatbot/src/message_suggestion.rs
  • services/headless-lms/models/.sqlx/query-78dd0f61529b6c663bf4238ab900e22b20389cc416449368fa5010c38eee5ff5.json
  • services/headless-lms/models/.sqlx/query-9178bbe940ca7098383e31554c19b99698ded2a88ece5e20be1dec616c4e5857.json
  • services/headless-lms/models/.sqlx/query-e5846a7aac2d0349da510598d023fc965c33926e6637fe469064987cc9de4760.json
  • services/headless-lms/models/.sqlx/query-fc016c003a9005b0dbc87337502807a65bd0abd4e35a9e32899bf7333aad8d46.json
  • services/headless-lms/models/src/chatbot_conversation_message_tool_calls.rs
  • services/main-frontend/src/app/manage/course-instances/[id]/certificates/page.tsx
  • services/main-frontend/src/components/course-material/chatbot/shared/ChatbotChatBody.tsx
  • services/main-frontend/src/utils/course-material/createChatbotTranscript.ts
  • services/quizzes/Dockerfile.production.slim.dockerfile
  • services/tmc/Dockerfile.production.slim.dockerfile
✅ Files skipped from review due to trivial changes (8)
  • services/cms/Dockerfile.production.slim.dockerfile
  • services/quizzes/Dockerfile.production.slim.dockerfile
  • services/headless-lms/Dockerfile.production.slim.dockerfile
  • services/tmc/Dockerfile.production.slim.dockerfile
  • services/example-exercise/Dockerfile.production.slim.dockerfile
  • services/headless-lms/models/.sqlx/query-9178bbe940ca7098383e31554c19b99698ded2a88ece5e20be1dec616c4e5857.json
  • services/headless-lms/models/.sqlx/query-fc016c003a9005b0dbc87337502807a65bd0abd4e35a9e32899bf7333aad8d46.json
  • services/headless-lms/models/.sqlx/query-e5846a7aac2d0349da510598d023fc965c33926e6637fe469064987cc9de4760.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • services/main-frontend/src/components/course-material/chatbot/shared/ChatbotChatBody.tsx
  • services/main-frontend/src/utils/course-material/createChatbotTranscript.ts
  • services/headless-lms/chatbot/src/cms_ai_suggestion.rs
  • services/headless-lms/chatbot/src/chatbot_tools/azure_ai_search.rs
  • services/headless-lms/chatbot/src/content_cleaner.rs
  • services/headless-lms/chatbot/src/azure_chatbot.rs

Comment thread services/headless-lms/chatbot/src/llm_utils.rs
Comment thread services/main-frontend/src/app/manage/course-instances/[id]/certificates/page.tsx Outdated
@github-actions github-actions Bot added the docs label May 13, 2026
Copy link
Copy Markdown
Member

@nygrenh nygrenh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🈹

Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs Outdated
Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs Outdated
Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs Outdated
@@ -559,11 +529,12 @@ impl Drop for RequestCancelledGuard {
}

pub async fn make_request_and_stream<'a>(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add doc comment explaining the return values

Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs
}
Some(Result::Ok(line)) => {
if !line.starts_with("data: ") {
if line.starts_with("event: ") {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CHeck later if (event_type, data_line) tuple would work here

Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs Outdated
/// For saving output items that are not text messages or function calls, i.e. that
/// don't need further processing and are not streamed to the user.
/// Saves reasoning and Azure AI items.
pub async fn process_output_item(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check how many queries calling this in loop makes

Comment thread services/headless-lms/chatbot/src/azure_chatbot.rs Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants