this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Switch Grace prompting to Letta streaming.

This keeps the prompt examples, docs, and generated fixtures aligned with the new Letta-backed chat path.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta Code <noreply@letta.com>

nandi a3fbc4d3 34b8daf7

+428 -88
+3 -3
dependencies/openai.nix
··· 6 6 }: 7 7 mkDerivation { 8 8 pname = "openai"; 9 - version = "2.2.1"; 9 + version = "2.5.3"; 10 10 src = fetchgit { 11 11 url = "https://github.com/MercuryTechnologies/openai.git"; 12 - sha256 = "1zs9iq9rcxrm1k1b57v3bwbg9iwj0dzp3psjwjmj0pla76xpp6sp"; 13 - rev = "07415dedd588c38740be4692e1aaa2309efe3982"; 12 + sha256 = "sha256-wl3TukDMzn19Mrt1PgsEXJo6amdF57NYjDaA/MyG9UQ="; 13 + rev = "e22221c2264ee16c3fc039b4e51dda2233394b59"; 14 14 fetchSubmodules = true; 15 15 }; 16 16 isLibrary = true;
+2 -2
examples/chaining.ffg
··· 1 - \{ "OpenAI API key" } -> 1 + \{ "Letta API key" } -> 2 2 3 - let key = .'OpenAI API key' 3 + let key = .'Letta API key' 4 4 5 5 let question = prompt 6 6 { key
+2 -2
examples/code.ffg
··· 1 - \{ "OpenAI API key" } -> 1 + \{ "Letta API key" } -> 2 2 3 - let key = .'OpenAI API key' 3 + let key = .'Letta API key' 4 4 5 5 in import prompt{ key, text: "Uppercase the input" } "abc" : Text
+2 -2
examples/emotion-wheel.ffg
··· 1 1 # Ask ChatGPT how it feels using this emotion wheel: 2 2 # 3 3 # https://www.betterup.com/hs-fs/hubfs/Emotion-Wheel-I.jpg?width=900&height=915&name=Emotion-Wheel-I.jpg 4 - \{ "OpenAI API key" } -> 4 + \{ "Letta API key" } -> 5 5 6 - let key = .'OpenAI API key' 6 + let key = .'Letta API key' 7 7 8 8 in fold 9 9 { "Uncomfortable emotions" { }: fold
+1 -1
examples/history.ffg
··· 1 1 \arguments -> 2 2 3 - let key = arguments."OpenAI API key" in 3 + let key = arguments."Letta API key" in 4 4 5 5 prompt 6 6 { key
+2 -2
examples/poem.ffg
··· 1 - \{ "OpenAI API key" } -> 1 + \{ "Letta API key" } -> 2 2 3 - let key = .'OpenAI API key' 3 + let key = .'Letta API key' 4 4 5 5 let concatSep = import github 6 6 { owner: "Gabriella439"
+2 -2
examples/prompt.ffg
··· 1 - \{ "OpenAI API key" } -> 1 + \{ "Letta API key" } -> 2 2 3 - let key = .'OpenAI API key' 3 + let key = .'Letta API key' 4 4 5 5 let { x, y } = prompt{ key, text: "Give me two numbers" } 6 6
+2 -2
examples/tools.ffg
··· 1 - \{ "OpenAI API key" } -> 1 + \{ "Letta API key" } -> 2 2 3 - let key = .'OpenAI API key' 3 + let key = .'Letta API key' 4 4 5 5 let concatSep = import github 6 6 { owner: "Gabriella439"
+2 -2
examples/transform-text.ffg
··· 1 - \{ "OpenAI API key", passage, code } -> 1 + \{ "Letta API key", passage, code } -> 2 2 3 3 prompt 4 - { key: .'OpenAI API key' 4 + { key: .'Letta API key' 5 5 , text: " 6 6 Take this passage: 7 7
+1 -1
examples/tutorial/coding.ffg
··· 1 1 \arguments -> 2 2 3 - let key = arguments."OpenAI API key" in 3 + let key = arguments."Letta API key" in 4 4 5 5 # What do you think this code will do? Run it to test your guess: 6 6 import prompt{ key }
+5 -7
examples/tutorial/prompting.ffg
··· 1 1 # Grace provides built-in language support for LLMs using the `prompt` function. 2 - # To run these examples you will need to provide an OpenAI API key below and. 3 - # and then click "Submit". 2 + # To run these examples you will need to provide a Letta API key below, then click "Submit". 4 3 \arguments -> 5 4 6 - let key = arguments."OpenAI API key" in 5 + let key = arguments."Letta API key" in 7 6 8 - { # You can prompt a model with `Text`, which will (by default) return `Text`: 7 + { # You can prompt a Letta agent with `Text`, which returns `Text`: 9 8 names: prompt{ key, text: "Give me a list of names" } 10 9 11 10 , # You can request structured output with a type annotation, like this: ··· 20 19 , # In fact, that type is descriptive enough that we can just omit the prompt: 21 20 tacitFullNames: prompt{ key } : List { firstName: Text, lastName: Text } 22 21 23 - , # By default the `prompt` keyword selects the `gpt-5-mini` model, but you can 24 - # specify other models using the `model` argument: 22 + , # The `model` argument selects which Letta agent to call, so pass the agent id there: 25 23 differentModel: 26 - prompt{ key, model: "gpt-5-nano" } : List { firstName: Text, lastName: Text } 24 + prompt{ key, model: "agent-YOUR_AGENT_ID" } : List { firstName: Text, lastName: Text } 27 25 } 28 26 29 27 # Try switching to the "Code" tab below to view the code for the result, then
+383 -35
ghc/Grace/HTTP.hs
··· 14 14 , Grace.HTTP.createChatCompletion 15 15 ) where 16 16 17 + import Control.Applicative ((<|>)) 18 + import Control.Concurrent (threadDelay) 17 19 import Control.Concurrent.MVar (MVar) 18 - import Control.Exception.Safe (Exception(..), Handler(..)) 20 + import Control.Exception.Safe (Exception(..)) 21 + import Data.Map.Strict (Map) 22 + import Data.Maybe (fromMaybe) 19 23 import Data.Text (Text) 24 + import Numeric.Natural (Natural) 20 25 import Data.Text.Encoding.Error (UnicodeException) 21 - import OpenAI.V1 (Methods(..)) 22 - import OpenAI.V1.Chat.Completions (ChatCompletionObject, CreateChatCompletion) 23 - import Servant.Client.Core.ClientError (ClientError(..)) 24 - import Servant.Client.Core.Response (ResponseF(..)) 26 + import Data.Time.Clock.POSIX (POSIXTime) 27 + import qualified Data.ByteString as ByteString 28 + import OpenAI.V1.Chat.Completions 29 + ( ChatCompletionObject(..) 30 + , Choice(..) 31 + , Content(..) 32 + , CreateChatCompletion(..) 33 + , Message(..) 34 + , LogProbs 35 + , ServiceTier 36 + , _CreateChatCompletion 37 + ) 38 + import OpenAI.V1.Models (Model) 39 + import OpenAI.V1.Usage 40 + ( CompletionTokensDetails(..) 41 + , PromptTokensDetails(..) 42 + , Usage(..) 43 + ) 25 44 26 45 import Grace.HTTP.Type 27 46 ( Header(..) ··· 42 61 43 62 import qualified Control.Concurrent.MVar as MVar 44 63 import qualified Control.Exception.Safe as Exception 45 - import qualified Control.Retry as Retry 46 64 import qualified Data.Aeson as Aeson 65 + import qualified Data.Map.Strict as Map 47 66 import qualified Data.Text as Text 48 67 import qualified Data.Text.Encoding as Encoding 49 68 import qualified Data.Text.Lazy as Text.Lazy 50 69 import qualified Data.Text.Lazy.Encoding as Lazy.Encoding 70 + import qualified Data.Vector as Vector 51 71 import qualified Network.HTTP.Types.Status as Status 52 72 import qualified Network.HTTP.Client as HTTP 53 73 import qualified Network.HTTP.Client.TLS as TLS 54 74 import qualified Network.HTTP.Types as HTTP.Types 55 - import qualified OpenAI.V1 as OpenAI 56 - import qualified Servant.Client as Client 75 + import qualified OpenAI.V1.Chat.Completions.Stream as Stream 57 76 import qualified System.IO.Unsafe as Unsafe 58 77 59 78 -- | Exception type thrown by `fetch` in the event of any failure ··· 70 89 {-# NOINLINE managerMVar #-} 71 90 72 91 retry :: IO a -> IO a 73 - retry io = 74 - Retry.recovering 75 - retryPolicy 76 - [ \_ -> Handler handler 77 - ] 78 - (\_ -> io) 92 + retry io = go 0 io 79 93 where 80 - retryPolicy = Retry.fullJitterBackoff 1000000 <> Retry.limitRetries 3 94 + go :: Int -> IO b -> IO b 95 + go attempt action = 96 + Exception.catch action (handler attempt action) 81 97 82 - handler (FailureResponse _ Response{ responseStatusCode }) = 83 - return (Status.statusIsServerError responseStatusCode) 84 - handler (ConnectionError _) = 85 - return True 86 - handler _ = 87 - return False 98 + handler :: Int -> IO b -> HTTP.HttpException -> IO b 99 + handler attempt action exception = 100 + if attempt < 3 then do 101 + threadDelay 1000000 102 + go (attempt + 1) action 103 + else 104 + Exception.throwIO exception 88 105 89 106 -- | Acquire a new `Manager` 90 107 -- ··· 96 113 Nothing -> do 97 114 TLS.newTlsManagerWith TLS.tlsManagerSettings 98 115 { managerResponseTimeout = HTTP.responseTimeoutNone 99 - , managerRetryableException = \exception -> 100 - case Exception.fromException exception of 101 - Just (FailureResponse _ Response{ responseStatusCode }) -> 102 - Status.statusIsServerError responseStatusCode 103 - Just (ConnectionError _) -> 104 - True 105 - _ -> 106 - False 116 + , managerRetryableException = \_ -> True 107 117 } 108 118 109 119 Just manager -> do ··· 259 269 \" <> Text.pack (displayException unicodeException) 260 270 261 271 -- | Initialize API for prompting 272 + type Methods = Text 273 + 262 274 getMethods :: IO (Text -> Methods) 263 - getMethods = do 264 - baseUrl <- Client.parseBaseUrl "https://api.openai.com" 275 + getMethods = 276 + return (\key -> key) 277 + 278 + data StreamState = StreamState 279 + { streamStateId :: Maybe Text 280 + , streamStateCreated :: Maybe POSIXTime 281 + , streamStateModel :: Maybe Model 282 + , streamStateServiceTier :: Maybe ServiceTier 283 + , streamStateSystemFingerprint :: Maybe Text 284 + , streamStateUsage :: Maybe (Usage CompletionTokensDetails PromptTokensDetails) 285 + , streamStateChoices :: Map Natural ChoiceState 286 + } 287 + 288 + data ChoiceState = ChoiceState 289 + { choiceStateContent :: Text 290 + , choiceStateRefusal :: Text 291 + , choiceStateFinishReason :: Maybe Text 292 + , choiceStateLogProbs :: Maybe LogProbs 293 + } 294 + 295 + emptyStreamState :: StreamState 296 + emptyStreamState = StreamState 297 + { streamStateId = Nothing 298 + , streamStateCreated = Nothing 299 + , streamStateModel = Nothing 300 + , streamStateServiceTier = Nothing 301 + , streamStateSystemFingerprint = Nothing 302 + , streamStateUsage = Nothing 303 + , streamStateChoices = Map.empty 304 + } 305 + 306 + zeroUsage :: Usage CompletionTokensDetails PromptTokensDetails 307 + zeroUsage = Usage 308 + { completion_tokens = 0 309 + , prompt_tokens = 0 310 + , total_tokens = 0 311 + , completion_tokens_details = Nothing 312 + , prompt_tokens_details = Nothing 313 + } 265 314 266 - manager <- newManager 315 + stepStreamState :: StreamState -> Stream.ChatCompletionChunk -> StreamState 316 + stepStreamState state Stream.ChatCompletionChunk 317 + { id 318 + , choices 319 + , created 320 + , model 321 + , service_tier 322 + , system_fingerprint 323 + , object = _ 324 + , usage 325 + } = 326 + state 327 + { streamStateId = Just id 328 + , streamStateCreated = Just created 329 + , streamStateModel = Just model 330 + , streamStateServiceTier = service_tier <|> streamStateServiceTier state 331 + , streamStateSystemFingerprint = system_fingerprint <|> streamStateSystemFingerprint state 332 + , streamStateUsage = usage <|> streamStateUsage state 333 + , streamStateChoices = Vector.foldl' stepChoiceState (streamStateChoices state) choices 334 + } 335 + where 336 + stepChoiceState :: Map Natural ChoiceState -> Stream.ChunkChoice -> Map Natural ChoiceState 337 + stepChoiceState choiceStates Stream.ChunkChoice 338 + { delta = Stream.Delta 339 + { delta_content 340 + , delta_refusal 341 + , delta_role = _ 342 + , delta_tool_calls = _ 343 + } 344 + , finish_reason 345 + , index 346 + , logprobs 347 + } = 348 + Map.insertWith combine index chunkState choiceStates 349 + where 350 + chunkState :: ChoiceState 351 + chunkState = ChoiceState 352 + { choiceStateContent = fromMaybe "" delta_content 353 + , choiceStateRefusal = fromMaybe "" delta_refusal 354 + , choiceStateFinishReason = finish_reason 355 + , choiceStateLogProbs = logprobs 356 + } 267 357 268 - let clientEnv = Client.mkClientEnv manager baseUrl 358 + combine :: ChoiceState -> ChoiceState -> ChoiceState 359 + combine newer older = ChoiceState 360 + { choiceStateContent = choiceStateContent older <> choiceStateContent newer 361 + , choiceStateRefusal = choiceStateRefusal older <> choiceStateRefusal newer 362 + , choiceStateFinishReason = choiceStateFinishReason newer <|> choiceStateFinishReason older 363 + , choiceStateLogProbs = choiceStateLogProbs newer <|> choiceStateLogProbs older 364 + } 269 365 270 - return (\key -> OpenAI.makeMethods clientEnv key organization Nothing) 366 + buildChatCompletion 367 + :: CreateChatCompletion 368 + -> StreamState 369 + -> IO ChatCompletionObject 370 + buildChatCompletion request@CreateChatCompletion 371 + { reasoning_effort = requestReasoningEffort 372 + } StreamState 373 + { streamStateId 374 + , streamStateCreated 375 + , streamStateModel 376 + , streamStateServiceTier 377 + , streamStateSystemFingerprint 378 + , streamStateUsage 379 + , streamStateChoices 380 + } = do 381 + id <- require "The chat completion stream did not provide an id." 382 + streamStateId 383 + 384 + created <- require "The chat completion stream did not provide a created timestamp." 385 + streamStateCreated 386 + 387 + model <- require "The chat completion stream did not provide a model." 388 + streamStateModel 389 + 390 + let buildChoice (index, ChoiceState 391 + { choiceStateContent 392 + , choiceStateRefusal 393 + , choiceStateFinishReason 394 + , choiceStateLogProbs 395 + }) = 396 + Choice 397 + { finish_reason = fromMaybe "stop" choiceStateFinishReason 398 + , index 399 + , message = Assistant 400 + { assistant_content = 401 + if Text.null choiceStateContent 402 + then Nothing 403 + else Just choiceStateContent 404 + , refusal = 405 + if Text.null choiceStateRefusal 406 + then Nothing 407 + else Just choiceStateRefusal 408 + , name = Nothing 409 + , assistant_audio = Nothing 410 + , tool_calls = Nothing 411 + } 412 + , logprobs = choiceStateLogProbs 413 + } 414 + 415 + return ChatCompletionObject 416 + { id 417 + , choices = Vector.fromList (fmap buildChoice (Map.toAscList streamStateChoices)) 418 + , created 419 + , model 420 + , reasoning_effort = requestReasoningEffort 421 + , service_tier = streamStateServiceTier 422 + , system_fingerprint = streamStateSystemFingerprint 423 + , object = "chat.completion" 424 + , usage = fromMaybe zeroUsage streamStateUsage 425 + } 426 + where 427 + require :: String -> Maybe a -> IO a 428 + require message = maybe (Exception.throwString message) pure 271 429 272 430 -- | This powers the @prompt@ keyword 273 431 createChatCompletion 274 432 :: Methods 275 433 -> CreateChatCompletion 276 434 -> IO ChatCompletionObject 277 - createChatCompletion Methods{ createChatCompletion = c } x = retry (c x) 435 + createChatCompletion key request = 436 + retry do 437 + manager <- newManager 438 + 439 + request₀ <- HTTP.parseUrlThrow "https://api.letta.com/v1/chat/completions" 440 + 441 + let body = RequestBodyLBS (Aeson.encode request{ stream = Just True }) 442 + 443 + let organizationHeader = case organization of 444 + Nothing -> [ ] 445 + Just o -> 446 + [ ( "OpenAI-Organization" 447 + , Encoding.encodeUtf8 o 448 + ) 449 + ] 450 + 451 + let request₁ = request₀ 452 + { method = HTTP.Types.methodPost 453 + , requestHeaders = 454 + [ ( "Content-Type" 455 + , "application/json" 456 + ) 457 + , ( "Authorization" 458 + , "Bearer " <> Encoding.encodeUtf8 key 459 + ) 460 + ] <> organizationHeader 461 + , requestBody = body 462 + } 463 + 464 + let handler :: HTTP.HttpException -> IO a 465 + handler httpException = Exception.throwIO (HttpException httpException) 466 + 467 + Exception.handle handler do 468 + HTTP.withResponse request₁ manager $ \response -> do 469 + if not (Status.statusIsSuccessful (HTTP.responseStatus response)) then do 470 + body' <- readBody (HTTP.responseBody response) 471 + 472 + Exception.throwIO 473 + ( HttpException 474 + ( HTTP.HttpExceptionRequest 475 + request₁ 476 + (StatusCodeException (fmap (const ()) response) body') 477 + ) 478 + ) 479 + else do 480 + finalState <- readStream (HTTP.responseBody response) emptyStreamParseState 481 + buildChatCompletion request (streamParseStreamState finalState) 482 + 483 + readBody :: HTTP.BodyReader -> IO ByteString.ByteString 484 + readBody bodyReader = go [ ] 485 + where 486 + go chunks = do 487 + chunk <- HTTP.brRead bodyReader 488 + 489 + if ByteString.null chunk then 490 + return (ByteString.concat (reverse chunks)) 491 + else 492 + go (chunk : chunks) 493 + 494 + data StreamParseState = StreamParseState 495 + { streamParseBuffer :: ByteString.ByteString 496 + , streamParseDataLines :: [ByteString.ByteString] 497 + , streamParseDone :: Bool 498 + , streamParseStreamState :: StreamState 499 + } 500 + 501 + emptyStreamParseState :: StreamParseState 502 + emptyStreamParseState = StreamParseState 503 + { streamParseBuffer = ByteString.empty 504 + , streamParseDataLines = [ ] 505 + , streamParseDone = False 506 + , streamParseStreamState = emptyStreamState 507 + } 508 + 509 + readStream :: HTTP.BodyReader -> StreamParseState -> IO StreamParseState 510 + readStream bodyReader state = 511 + if streamParseDone state then 512 + return state 513 + else do 514 + chunk <- HTTP.brRead bodyReader 515 + 516 + if ByteString.null chunk then 517 + flushStreamParseState state 518 + else do 519 + state' <- processStreamChunk state chunk 520 + 521 + if streamParseDone state' then 522 + return state' 523 + else 524 + readStream bodyReader state' 525 + 526 + processStreamChunk :: StreamParseState -> ByteString.ByteString -> IO StreamParseState 527 + processStreamChunk state chunk = 528 + consumeBufferedLines state 529 + { streamParseBuffer = streamParseBuffer state <> chunk 530 + } 531 + where 532 + consumeBufferedLines currentState = 533 + case ByteString.break (== 10) (streamParseBuffer currentState) of 534 + (line, rest) 535 + | ByteString.null rest -> 536 + return currentState 537 + { streamParseBuffer = line 538 + } 539 + | otherwise -> do 540 + let nextState = currentState 541 + { streamParseBuffer = ByteString.drop 1 rest 542 + } 543 + 544 + nextState' <- processStreamLine nextState line 545 + 546 + if streamParseDone nextState' then 547 + return nextState' 548 + else 549 + consumeBufferedLines nextState' 550 + 551 + processStreamLine :: StreamParseState -> ByteString.ByteString -> IO StreamParseState 552 + processStreamLine state rawLine = 553 + case stripCR rawLine of 554 + line 555 + | ByteString.null line -> 556 + emitStreamEvent state 557 + | ByteString.isPrefixOf dataPrefix line -> 558 + let payload = ByteString.dropWhile (== 32) (ByteString.drop (ByteString.length dataPrefix) line) 559 + in return state 560 + { streamParseDataLines = payload : streamParseDataLines state 561 + } 562 + | otherwise -> 563 + return state 564 + where 565 + dataPrefix = Encoding.encodeUtf8 "data:" 566 + 567 + stripCR :: ByteString.ByteString -> ByteString.ByteString 568 + stripCR line 569 + | ByteString.null line = line 570 + | ByteString.last line == 13 = ByteString.init line 571 + | otherwise = line 572 + 573 + emitStreamEvent :: StreamParseState -> IO StreamParseState 574 + emitStreamEvent state = 575 + case streamParseDataLines state of 576 + [ ] -> 577 + return state 578 + { streamParseDataLines = [ ] 579 + } 580 + dataLines -> do 581 + let payloadBytes = ByteString.intercalate (ByteString.singleton 10) (reverse dataLines) 582 + 583 + payloadText <- case Encoding.decodeUtf8' payloadBytes of 584 + Left exception -> Exception.throwIO (NotUTF8 exception) 585 + Right text -> return text 586 + 587 + if Text.strip payloadText == "[DONE]" then 588 + return state 589 + { streamParseDataLines = [ ] 590 + , streamParseDone = True 591 + } 592 + else do 593 + chunk <- case Aeson.eitherDecodeStrict' payloadBytes of 594 + Left message -> 595 + Exception.throwString 596 + ( "Could not decode SSE chunk: " 597 + <> message 598 + <> "\n" 599 + <> Text.unpack payloadText 600 + ) 601 + Right chunk' -> 602 + return chunk' 603 + 604 + return state 605 + { streamParseDataLines = [ ] 606 + , streamParseStreamState = 607 + stepStreamState (streamParseStreamState state) chunk 608 + } 609 + 610 + flushStreamParseState :: StreamParseState -> IO StreamParseState 611 + flushStreamParseState state = do 612 + state' <- 613 + if ByteString.null (streamParseBuffer state) then 614 + return state 615 + else 616 + processStreamLine 617 + state 618 + { streamParseBuffer = ByteString.empty 619 + } 620 + (streamParseBuffer state) 621 + 622 + if streamParseDone state' then 623 + return state' 624 + else 625 + emitStreamEvent state'
+2 -2
ghcjs/Grace/HTTP.hs
··· 130 130 renderError :: HttpException -> Text 131 131 renderError = Text.pack . displayException 132 132 133 - -- | The GHCJS implementation of OpenAI bindings just stores the API key 133 + -- | The GHCJS implementation of the chat-completions bindings just stores the API key 134 134 type Methods = Text 135 135 136 136 -- | Initialize API for prompting ··· 154 154 Just o -> [("OpenAI-Organization", Encoding.encodeUtf8 o)] 155 155 156 156 let request = Request 157 - { reqUrl = "https://api.openai.com/v1/chat/completions" 157 + { reqUrl = "https://api.letta.com/v1/chat/completions" 158 158 , reqOptions = Fetch.defaultRequestOptions 159 159 { reqOptMethod = "POST" 160 160 , reqOptHeaders =
+3 -3
prompts/inference.md
··· 265 265 266 266 ```bash 267 267 $ grace repl 268 - >>> prompt{ key: ./openai.key, text: "Give me a first and last name" } : { first: Text, last: Text } 268 + >>> prompt{ key: ./letta.key, text: "Give me a first and last name" } : { first: Text, last: Text } 269 269 { "first": "Emily", "last": "Johnson" } 270 - >>> prompt{ key: ./openai.key, text: "Give me a list of names" } : List Text 270 + >>> prompt{ key: ./letta.key, text: "Give me a list of names" } : List Text 271 271 [ "Alice" 272 272 , "Bob" 273 273 , "Charlie" ··· 384 384 We can explore this idea of using the schema to drive the prompt instead of prose using an example like this: 385 385 386 386 ```haskell 387 - prompt{ key: ./openai.key, text: "Generate some characters for a story" } 387 + prompt{ key: ./letta.key, text: "Generate some characters for a story" } 388 388 : List 389 389 { "The character's name": Text 390 390 , "The most memorable thing about the character": Text
+11 -17
src/Grace/Prompt.hs
··· 38 38 , CreateChatCompletion(..) 39 39 , Message(..) 40 40 , ReasoningEffort(..) 41 - , WebSearchOptions(..) 42 41 , _CreateChatCompletion 43 42 ) 44 43 ··· 215 214 (Aeson.object 216 215 [ ("type", "number") 217 216 -- , ("minimum", Aeson.toJSON (0 :: Int)) 218 - -- ^ Not supported by OpenAI 217 + -- ^ Not supported by the current chat completions API 219 218 ] 220 219 ) 221 220 loop Type.Scalar{ scalar = Monotype.Text } = ··· 251 250 252 251 let methods = keyToMethods (Text.strip key) 253 252 254 - let defaultedSearch = case search of 255 - Just s -> s 256 - Nothing -> False 257 - 258 - let web_search_options 259 - | defaultedSearch = Just WebSearchOptions 260 - { search_context_size = Nothing 261 - , user_location = Nothing 262 - } 263 - | otherwise = Nothing 253 + case search of 254 + Just True -> 255 + Exception.throwString "Grace's Letta backend does not support the `search` flag yet; use a Letta agent with search tools instead." 256 + _ -> 257 + return () 264 258 265 - let defaultedModel = case model of 266 - Just m -> m 267 - _ | defaultedSearch -> "gpt-5-search-api" 268 - | otherwise -> "gpt-5-mini" 259 + defaultedModel <- case model of 260 + Just m -> return m 261 + Nothing -> 262 + Exception.throwString "Grace's Letta backend requires a `model` value with the Letta agent id, for example `model: \"agent-...\"`." 269 263 270 264 let reasoning_effort = do 271 265 e <- effort ··· 469 463 HTTP.createChatCompletion methods _CreateChatCompletion 470 464 { messages 471 465 , model = Model defaultedModel 472 - , web_search_options 466 + , web_search_options = Nothing 473 467 , reasoning_effort 474 468 } 475 469
+3 -3
tasty/data/complex/examples-output.ffg
··· 50 50 } 51 51 , "prompting": 52 52 \arguments -> 53 - let key = arguments."OpenAI API key" 53 + let key = arguments."Letta API key" 54 54 55 55 in { "names": 56 56 prompt ··· 121 121 { "key": 122 122 key 123 123 , "model": 124 - some "gpt-5-nano" 124 + some "agent-YOUR_AGENT_ID" 125 125 , "text": 126 126 null 127 127 , "history": ··· 682 682 } 683 683 , "coding": 684 684 \arguments -> 685 - let key = arguments."OpenAI API key" 685 + let key = arguments."Letta API key" 686 686 687 687 in import prompt 688 688 { "key":
+2 -2
tasty/data/complex/examples-type.ffg
··· 45 45 } 46 46 } 47 47 , prompting: 48 - { "OpenAI API key": Key, q } -> 48 + { "Letta API key": Key, q } -> 49 49 { names: 50 50 p 51 51 , structuredNames: ··· 110 110 Bool -> Bool 111 111 } 112 112 , coding: 113 - { "OpenAI API key": Key, l } -> 113 + { "Letta API key": Key, l } -> 114 114 { "Job Description": Text } -> 115 115 { "Is Finance?": Bool, "Rationale": Text } 116 116 , conclusion: