Managing IPWorks LLM Conversations
Requirements:
IPWorks LLM
Introduction
The LLMClient component in IPWorks LLM maintains conversation history across requests. This guide covers continuing and resetting conversations, streaming replies, saving and restoring conversation history, switching providers, and handling unsuccessful replies.
Tip: Download the latest version of IPWorks LLM to follow along. For a basic request example, see Getting Started with IPWorks LLM.
The LLMClient component keeps a conversation in two collections:
- Messages stores each message's identifier and role.
- MessageParts stores the content belonging to those messages, including text, images, files, reasoning, tool calls, and tool results.
Generate sends the current conversation and appends an assistant message and its parts when the reply contains content. ResponseText contains only the text of the latest reply; inspect MessageParts when the reply includes reasoning or tool calls.
Step 1: Continue or Reset a Conversation
Start with a question, then add a follow-up and call Generate again:
using nsoftware.IPWorksLLM;
LLMClient llmclient = new LLMClient();
llmclient.ServiceProvider = LLMClientServiceProviders.spOpenAI;
llmclient.APIKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY");
llmclient.Model = "gpt-4o-mini";
llmclient.AddMessage("user", "What is the capital of France?");
llmclient.Generate();
Console.WriteLine(llmclient.ResponseText);
llmclient.AddMessage("user", "What language is primarily spoken there?");
llmclient.Generate();
Console.WriteLine(llmclient.ResponseText);
The second request includes the first exchange, so the model can interpret "there" as France. To start over, call Reset:
llmclient.Reset();
Reset clears messages, parts, tools, and reply state. It keeps configuration such as ServiceProvider, APIKey, Model, ServerURL, and SystemPrompt. Re-add tools needed by the new conversation.
Step 2: Stream Replies
Set Stream to true and handle ContentDelta to display text as it arrives:
llmclient.Stream = true;
llmclient.OnContentDelta += (s, e) =>
{
if (e.PartType == (int)PartTypes.ptText)
Console.Write(e.Delta);
};
llmclient.AddMessage("user", "Explain photosynthesis in two sentences.");
llmclient.Generate();
Console.WriteLine();
The event can also report reasoning fragments as ptReasoning. The filter above excludes them from displayed text. After the stream completes, the component appends the assembled reply to the conversation and sets ResponseText to its text content.
Step 3: Persist a Conversation
To resume a conversation later, save the messages and their parts in their existing order. If the conversation uses tools, save their definitions as well. Each part's MessageId must still identify a saved message. Save these fields for every item:
| Collection | Fields to Save |
|---|---|
| Messages | Id, Role |
| MessageParts | MessageId, PartType, Text, DataB, URL, MediaType, FileName, ToolCallId, ToolName, ArgumentsJson, IsError, ProviderData |
| Tools | Name, Description, InputSchema |
Saving is application-defined: copy these fields into your own records and write them to storage. The example below assumes your application has loaded the saved messages, parts, and tools into a state object. Add messages before their parts so each MessageId refers to an existing message:
llmclient.Reset();
foreach (var saved in state.Messages)
{
llmclient.Messages.Add(new LLMMessage
{
Id = saved.Id,
Role = saved.Role
});
}
foreach (var saved in state.Parts)
{
llmclient.MessageParts.Add(new LLMMessagePart
{
MessageId = saved.MessageId,
PartType = saved.PartType,
Text = saved.Text,
DataB = saved.DataB,
URL = saved.URL,
MediaType = saved.MediaType,
FileName = saved.FileName,
ToolCallId = saved.ToolCallId,
ToolName = saved.ToolName,
ArgumentsJson = saved.ArgumentsJson,
IsError = saved.IsError,
ProviderData = saved.ProviderData
});
}
foreach (var saved in state.Tools)
{
llmclient.Tools.Add(new LLMTool
{
Name = saved.Name,
Description = saved.Description,
InputSchema = saved.InputSchema
});
}
Encode binary DataB for the storage format, for example as base64 in JSON. Preserve ProviderData when resuming with the same provider; it can contain signatures that the provider needs on a later request. If the saved conversation ends with tool calls, add their results before the next Generate.
The collections do not include configuration. Restore the saved provider and relevant settings before generating again, including the model, system prompt, server URL, and output limit. A custom provider value, such as spCustomOpenAI, has no default endpoint, so Generate fails with error 802 unless ServerURL is restored with it. Load API keys through the application's normal credential handling rather than storing them with the conversation.
Step 4: Switch Providers
Changing ServiceProvider preserves messages, parts, and tools, but clears ProviderData from each part. Provider-only data cannot be reused by another provider. The component leaves APIKey, Model, ServerURL, and MaxOutputTokens unchanged.
Update the provider-dependent settings together:
llmclient.ServiceProvider = LLMClientServiceProviders.spAnthropic;
llmclient.APIKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY");
llmclient.Model = "anthropic-model";
llmclient.ServerURL = ""; // Use the component's default Anthropic endpoint.
llmclient.MaxOutputTokens = 1024;
Model identifiers and credentials are provider-specific. spAnthropic and spCustomAnthropic require a positive MaxOutputTokens. Review message roles, media types, and tool history before sending an existing conversation to a different provider.
Step 5: Handle Errors and Blocked Replies
LLMClient reports the following component error groups:
| Error | Meaning | Typical Response |
|---|---|---|
| 801 | The server returned an error. | Report the provider message. Retry only transient failures, using a bounded delay. |
| 802 | A required setting or argument is missing or invalid, such as an empty Model, or an empty ServerURL with a custom provider value. | Correct the request; retrying it unchanged will not help. |
| 803 | The response could not be parsed or lacked expected content. | Record the response details and verify endpoint compatibility. |
| 804 | The selected provider does not support the operation or setting. | Check provider support, then change the operation or setting. |
Handle exceptions from Generate as part of the application's request flow. If a stream fails, ContentDelta may already have displayed text, but an incomplete assistant reply is not appended to the conversation. Mark the displayed text as incomplete. Decide whether to retry based on the provider error and whether another attempt is appropriate for the application.
A provider-reported safety block can return normally with FinishReason set to frContentFilter. A blocked prompt may append no assistant message. When the provider supplies its own finish reason, it is available through Config("ProviderFinishReason"):
llmclient.Generate();
if (llmclient.FinishReason == LLMClientFinishReasons.frContentFilter)
{
Console.WriteLine("The provider blocked the request or reply: " +
llmclient.Config("ProviderFinishReason"));
}
Conclusion
The history used for follow-up requests can also be saved for a later session. Restore Messages, MessageParts, provider settings, and any needed tools before calling Generate again. For information on handling tool calls and returning results to the model, see Tool Calling with IPWorks LLM.
We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.