Skip to content

Commit 6b6f833

Browse files
authored
Adding the ability to load a session from a saved transcript (#8)
1 parent e9a40a5 commit 6b6f833

11 files changed

Lines changed: 892 additions & 1 deletion

File tree

foundation-models-c/Sources/FoundationModelsCBindings/FoundationModelsCBindings.swift

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,45 @@ public func FMLanguageModelSessionCreateFromSystemLanguageModel(
127127
return FMLanguageModelSessionRef(Unmanaged.passRetained(session).toOpaque())
128128
}
129129

130+
@_cdecl("FMLanguageModelSessionCreateFromTranscript")
131+
public func FMLanguageModelSessionCreateFromTranscript(
132+
transcriptSession: FMLanguageModelSessionRef,
133+
model: UnsafePointer<FMSystemLanguageModelRef>?,
134+
tools: UnsafeMutablePointer<FMBridgedToolRef>?,
135+
toolCount: Int32
136+
) -> FMLanguageModelSessionRef {
137+
// Extract the transcript from the existing session
138+
let existingSession = Unmanaged<LanguageModelSession>.fromOpaque(transcriptSession)
139+
.takeUnretainedValue()
140+
let transcript = existingSession.transcript
141+
142+
// Get the model to use
143+
var modelChoice: SystemLanguageModel
144+
if let model = model {
145+
modelChoice = Unmanaged<SystemLanguageModel>.fromOpaque(model).takeUnretainedValue()
146+
} else {
147+
modelChoice = SystemLanguageModel.default
148+
}
149+
150+
// Convert the C array of tool refs to Swift array of Tool objects
151+
var toolArray: [any Tool] = []
152+
if let tools = tools, toolCount > 0 {
153+
for i in 0..<Int(toolCount) {
154+
let toolRef = tools[i]
155+
let bridgedTool = Unmanaged<BridgedTool>.fromOpaque(toolRef).takeUnretainedValue()
156+
toolArray.append(bridgedTool)
157+
}
158+
}
159+
160+
// Create a new session from the transcript
161+
let session = LanguageModelSession(
162+
model: modelChoice,
163+
tools: toolArray,
164+
transcript: transcript,
165+
)
166+
return FMLanguageModelSessionRef(Unmanaged.passRetained(session).toOpaque())
167+
}
168+
130169
@_cdecl("FMLanguageModelSessionIsResponding")
131170
public func FMLanguageModelSessionIsResponding(session: FMLanguageModelSessionRef) -> Bool {
132171
let session = Unmanaged<LanguageModelSession>.fromOpaque(session).takeUnretainedValue()
@@ -525,6 +564,29 @@ public func FMLanguageModelSessionRespondWithSchemaFromJSON(
525564

526565
// MARK: - Transcript
527566

567+
@_cdecl("FMTranscriptCreateFromJSONString")
568+
public func FMTranscriptCreateFromJSONString(
569+
jsonString: UnsafePointer<CChar>,
570+
outErrorCode: UnsafeMutablePointer<Int32>?,
571+
outErrorDescription: UnsafeMutablePointer<UnsafePointer<CChar>?>?
572+
) -> FMLanguageModelSessionRef? {
573+
let jsonStr = String(cString: jsonString)
574+
575+
do {
576+
let transcript = try JSONDecoder().decode(Transcript.self, from: Data(jsonStr.utf8))
577+
// Create a new session initialized with the transcript
578+
let session = LanguageModelSession(transcript: transcript)
579+
return FMLanguageModelSessionRef(Unmanaged.passRetained(session).toOpaque())
580+
} catch {
581+
let debugDescription = formatErrorDescription(error)
582+
debugDescription.withCString { cString in
583+
outErrorCode?.pointee = StatusCode.decodingFailure.rawValue
584+
outErrorDescription?.pointee = UnsafePointer(strdup(cString))
585+
}
586+
return nil
587+
}
588+
}
589+
528590
/// Returns a JSON string representation of the session transcript.
529591
///
530592
/// - Parameters:

foundation-models-c/Sources/FoundationModelsCBindings/include/FoundationModels.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ FMSystemLanguageModelRef _Nonnull FMSystemLanguageModelCreate(FMSystemLanguageMo
5252
bool FMSystemLanguageModelIsAvailable(FMSystemLanguageModelRef _Nonnull ref, FMSystemLanguageModelUnavailableReason *_Nullable unavailableReason);
5353
FMLanguageModelSessionRef _Nonnull FMLanguageModelSessionCreateDefault();
5454
FMLanguageModelSessionRef _Nonnull FMLanguageModelSessionCreateFromSystemLanguageModel(FMSystemLanguageModelRef _Nullable model, const char *_Nullable instructions, FMBridgedToolRef _Nullable *_Nullable tools, int toolCount);
55+
FMLanguageModelSessionRef _Nonnull FMLanguageModelSessionCreateFromTranscript(FMLanguageModelSessionRef _Nonnull transcriptSession, FMSystemLanguageModelRef _Nullable model, FMBridgedToolRef _Nullable *_Nullable tools, int toolCount);
5556
bool FMLanguageModelSessionIsResponding(FMLanguageModelSessionRef _Nonnull session);
5657
void FMLanguageModelSessionReset(FMLanguageModelSessionRef _Nonnull session);
5758
FMTaskRef FMLanguageModelSessionRespond(FMLanguageModelSessionRef _Nonnull session, const char *_Nonnull prompt, void *_Nullable userInfo, FMLanguageModelSessionResponseCallback callback);
5859
FMLanguageModelSessionResponseStreamRef _Nonnull FMLanguageModelSessionStreamResponse(FMLanguageModelSessionRef _Nonnull session, const char *_Nonnull prompt);
5960
void FMLanguageModelSessionResponseStreamIterate(FMLanguageModelSessionResponseStreamRef _Nonnull stream, void *_Nullable userInfo, FMLanguageModelSessionResponseCallback callback);
6061

6162
// Transcript functions
63+
FMLanguageModelSessionRef _Nullable FMTranscriptCreateFromJSONString(const char *_Nonnull jsonString, int *_Nullable outErrorCode, char *_Nullable *_Nullable outErrorDescription);
6264
char *_Nullable FMLanguageModelSessionGetTranscriptJSONString(FMLanguageModelSessionRef _Nonnull session, int *_Nullable outErrorCode, char *_Nullable *_Nullable outErrorDescription);
6365

6466
// GenerationSchema functions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ backend-path = ["."]
55

66
[project]
77
name = "apple-fm-sdk"
8-
version = "0.1.0"
8+
version = "0.1.1"
99
description = "Python bindings for Apple's Foundation Models Swift framework"
1010
readme = "README.md"
1111
requires-python = ">=3.10"

src/apple_fm_sdk/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from .session import LanguageModelSession
1616

17+
from .transcript import Transcript
18+
1719
from .errors import (
1820
FoundationModelsError,
1921
GenerationError,
@@ -51,6 +53,7 @@
5153
__all__ = [
5254
"SystemLanguageModel",
5355
"LanguageModelSession",
56+
"Transcript",
5457
"SystemLanguageModelUseCase",
5558
"SystemLanguageModelGuardrails",
5659
"SystemLanguageModelUnavailableReason",

src/apple_fm_sdk/session.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,106 @@ def __init__(
168168
super().__init__(ptr)
169169
# This opaque pointer already has 1 ref count by `passRetained`
170170

171+
@classmethod
172+
def from_transcript(
173+
cls,
174+
transcript: Transcript,
175+
model: Optional[SystemLanguageModel] = None,
176+
tools: Optional[list[Tool]] = None,
177+
) -> "LanguageModelSession":
178+
"""Create a new session from an existing transcript.
179+
180+
This method creates a new LanguageModelSession initialized from an existing transcript.
181+
The new session will contain all the conversation history from the transcript, including
182+
user messages, model responses, and tool interactions.
183+
184+
:param transcript: The Transcript instance to initialize the session from.
185+
This transcript contains the complete conversation history.
186+
:type transcript: Transcript
187+
:param model: Optional SystemLanguageModel to use for the new session. If not
188+
provided, uses the default model. This allows you to continue a conversation
189+
with a different model configuration than the original.
190+
:type model: Optional[SystemLanguageModel]
191+
:param tools: **IMPORTANT**: Tool mentions loaded from a Transcript are historical only.
192+
You must **also** pass tool instances here if you want to allow the model to make new
193+
tool calls in this session.
194+
:type tools: Optional[list[Tool]]
195+
:return: A new LanguageModelSession initialized with the transcript's history
196+
:rtype: LanguageModelSession
197+
:raises FoundationModelsError: If session creation fails
198+
199+
Examples:
200+
Resume a conversation from a saved transcript::
201+
202+
import apple_fm_sdk as fm
203+
import json
204+
205+
# Load a saved transcript
206+
with open("transcript.json", "r") as f:
207+
transcript_dict = json.load(f)
208+
209+
transcript = await fm.Transcript.from_dict(transcript_dict)
210+
211+
# Create a new session from the transcript
212+
session = fm.LanguageModelSession.from_transcript(transcript)
213+
214+
# Continue the session
215+
response = await session.respond("Summarize the session so far.")
216+
217+
Resume with tools::
218+
219+
import apple_fm_sdk as fm
220+
from my_tools import CalculatorTool, WeatherTool
221+
222+
# Load transcript that had tool calls
223+
transcript = await fm.Transcript.from_dict(transcript_dict)
224+
225+
# IMPORTANT: You must pass the tool instances explicitly.
226+
# The transcript contains the history of tool calls, but not
227+
# the ability to make new tool calls unless you provide them.
228+
session = fm.LanguageModelSession.from_transcript(
229+
transcript,
230+
tools=[CalculatorTool(), WeatherTool()]
231+
)
232+
233+
# Now the model can make new tool calls
234+
response = await session.respond("Calculate 15 * 24")
235+
236+
Note:
237+
- The transcript contains the session instructions, so you don't need to
238+
pass instructions separately
239+
- The new session shares the transcript object with the original
240+
- Any new interactions will update the transcript
241+
- Tool mentions from the transcript are historical only. To allow the model to
242+
make new tool calls, you must explicitly pass the tool instances in the ``tools`` parameter.
243+
See Also:
244+
- :class:`~apple_fm_sdk.transcript.Transcript`: For working with transcripts
245+
- :meth:`~apple_fm_sdk.transcript.Transcript.from_dict`: For loading transcripts from JSON
246+
- :class:`~apple_fm_sdk.tool.Tool`: For creating custom tools
247+
"""
248+
# Create model pointer
249+
model_ptr = model._ptr if model else None
250+
251+
# Create array of tool pointers
252+
tool_count = len(tools) if tools else 0
253+
tool_refs = (ctypes.c_void_p * tool_count)()
254+
if tools:
255+
for i, tool in enumerate(tools):
256+
tool_refs[i] = tool._ptr
257+
258+
# Create the session via C binding - use the new function that takes a transcript session
259+
ptr = lib.FMLanguageModelSessionCreateFromTranscript(
260+
transcript.session_ptr, model_ptr, tool_refs, tool_count
261+
)
262+
263+
# Update transcript to use the new session pointer
264+
transcript._update_session_ptr(ptr)
265+
266+
# Create session instance
267+
session = cls(_ptr=ptr)
268+
session.transcript = transcript # Use the provided transcript
269+
return session
270+
171271
@property
172272
def is_responding(self) -> bool:
173273
"""Check if the session is currently responding to a request.

src/apple_fm_sdk/transcript.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,113 @@ async def to_dict(self) -> dict:
225225
json_str = str(jsn_string)
226226
result = json.loads(json_str)
227227
return result
228+
229+
@classmethod
230+
async def from_dict(cls, dict: dict) -> "Transcript":
231+
"""Create a Transcript from a dictionary representation.
232+
233+
This method deserializes a transcript dictionary (typically loaded from JSON)
234+
and creates a Transcript instance. This is useful for loading saved session
235+
transcripts and resuming sessions with the full history intact.
236+
237+
:param dict: Dictionary representation of a transcript, typically loaded from
238+
a JSON file. Must match the Foundation Models transcript format.
239+
:type dict: dict
240+
:return: A new Transcript instance initialized with the provided data
241+
:rtype: Transcript
242+
:raises GenerationError: If the dictionary format is invalid or cannot be parsed
243+
244+
.. warning::
245+
**Tools in a transcript:**
246+
247+
The transcript preserves the *history* of tool calls (what was called and
248+
what the results were), but not the *capability* to make new tool calls.
249+
Tool definitions stored in a transcript JSON will appear in the transcript's
250+
content history, but they will **not** be automatically available for the model
251+
to call. That's because the transcript doesn't actually contain the tool
252+
implementations. To allow the model to invoke tools mentioned in the transcript,
253+
implement each tool in Python and then create a session with both the transcript
254+
and tools using :meth:`~apple_fm_sdk.session.LanguageModelSession.from_transcript`
255+
256+
Examples:
257+
Load a transcript from a JSON file::
258+
259+
import apple_fm_sdk as fm
260+
import json
261+
262+
# Load transcript from file
263+
with open("transcript.json", "r") as f:
264+
transcript_dict = json.load(f)
265+
266+
# Create Transcript instance
267+
transcript = await fm.Transcript.from_dict(transcript_dict)
268+
269+
# Now you can create a session starting from this transcript
270+
session = fm.LanguageModelSession.from_transcript(transcript)
271+
272+
Load and resume with tools::
273+
274+
import apple_fm_sdk as fm
275+
import json
276+
from my_tools import CalculatorTool, WeatherTool
277+
278+
# Load transcript that had tool calls
279+
with open("transcript_with_tools.json", "r") as f:
280+
transcript_dict = json.load(f)
281+
282+
transcript = await fm.Transcript.from_dict(transcript_dict)
283+
284+
# IMPORTANT: Tools in the transcript are historical mentions only.
285+
# To allow the model to call a tool, you must explicitly instantiate each
286+
# tool in Python and then pass them to the session initializer.
287+
session = fm.LanguageModelSession.from_transcript(
288+
transcript,
289+
tools=[CalculatorTool(), WeatherTool()]
290+
)
291+
292+
Note:
293+
- The dictionary must follow the Foundation Models transcript format
294+
- Tool definitions in the transcript are for historical reference only
295+
- To use tools with the transcript, pass them to
296+
:meth:`~apple_fm_sdk.session.LanguageModelSession.from_transcript`
297+
298+
See Also:
299+
- :meth:`to_dict`: For converting a Transcript to a dictionary
300+
- :meth:`~apple_fm_sdk.session.LanguageModelSession.from_transcript`: For creating sessions from transcripts
301+
- :class:`~apple_fm_sdk.tool.Tool`: For creating custom tools
302+
"""
303+
error_code = ctypes.c_int32() # C error status code
304+
error_description = ctypes.POINTER(
305+
ctypes.c_char
306+
)() # C error description pointer
307+
308+
# Create a session pointer initialized with the transcript data from the dictionary
309+
# We can't create transcript pointer directly, so we create a new session pointer that
310+
# holds the Transcript.
311+
session_ptr = lib.FMTranscriptCreateFromJSONString(
312+
json.dumps(dict), ctypes.byref(error_code), ctypes.byref(error_description)
313+
)
314+
315+
# Check if we got a valid result or an error
316+
if session_ptr is None:
317+
# An error occurred, raise appropriate exception
318+
err_code, err_desc = _get_error_string(error_code, error_description)
319+
error_msg = "Failed to create transcript from dictionary"
320+
if err_desc:
321+
error_msg = error_msg + ": " + err_desc
322+
raise _status_code_to_exception(err_code or error_code.value, error_msg)
323+
324+
return cls(_ptr=session_ptr)
325+
326+
def _update_session_ptr(self, new_ptr):
327+
"""Update the session pointer associated with this transcript.
328+
329+
This is used internally to keep the transcript's session pointer in sync
330+
with the LanguageModelSession's pointer after interactions that may change it.
331+
332+
Note:
333+
- This method is for internal use only and should not be called directly.
334+
- The transcript shares the session's pointer, so updating it ensures the
335+
transcript reflects the current session state.
336+
"""
337+
self.session_ptr = new_ptr

0 commit comments

Comments
 (0)