packages feed

langchain-hs-gemini 0.0.6.0 → 0.0.6.2

raw patch · 4 files changed

+143/−11 lines, 4 filesdep ~langchain-hs-corePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: langchain-hs-core

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Changelog for langchain-hs-gemini +## 0.0.6.2 - 2026-11-14++> **Note**: `0.0.6.1` was skipped globally after `langchain-hs-ollama-v0.0.6.1`.++- Added incremental tool-call emission during SSE streaming in `Langchain.Provider.Gemini` (`ChatModel.stream`).+- Consolidates Interactions v1 Servant bindings (`Gemini.Client.*`) and `Langchain.Embeddings.Gemini` under aligned `0.0.6.2`.+- Updated `langchain-hs-core` dependency bound to `>= 0.0.6.2 && < 0.0.7`.+ ## 0.0.6.0 - 2026-10-02  - Initial release. Split from the monolithic `langchain-hs` package.
langchain-hs-gemini.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           langchain-hs-gemini-version:        0.0.6.0+version:        0.0.6.2 synopsis:       Google Gemini provider for langchain-hs description:    Google Gemini ChatModel provider for langchain-hs. Supports multi-modal content, streaming via Server-Sent Events, and function calling. Does not depend on OpenAI or Ollama packages. category:       AI@@ -51,7 +51,7 @@     , http-client ==0.7.*     , http-client-tls >=0.3 && <0.5     , http-conduit ==2.*-    , langchain-hs-core ==0.0.6.*+    , langchain-hs-core >=0.0.6.2 && <0.0.7     , mtl >=2.2 && <2.4     , resourcet >=1.2 && <1.4     , servant >=0.19 && <0.21
src/Langchain/Provider/Gemini.hs view
@@ -29,14 +29,16 @@   ) where  import Control.Applicative ((<|>))+import Control.Monad (forM_) import Control.Monad.Except (throwError) import Control.Monad.IO.Class (liftIO)-import Data.Aeson (FromJSON, ToJSON, decodeStrict, object, toJSON, (.=))+import Data.Aeson (FromJSON, ToJSON, decodeStrict, eitherDecodeStrict, object, toJSON, (.=)) import Data.Conduit (await, yield, (.|)) import qualified Data.List as List import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe)+import qualified Data.Set as Set import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE@@ -270,31 +272,101 @@       Nothing -> newGeminiClientEnv (apiKey provider)     let req = buildCreateRequest provider msgs mbConfig -    (accumulated, mbCompletedInter) <--      streamGeminiInteraction env req .| consumeStream rId "" Nothing+    (accumulated, pendingCalls, emittedCalls, thoughtSigs, mbCompletedInter) <-+      streamGeminiInteraction env req .| consumeStream rId "" Map.empty [] [] Nothing +    -- Finalize any tool calls whose step.start was not followed by step.stop+    remainingCalls <- traverse finalizePendingCall (Map.elems pendingCalls)+    forM_ remainingCalls $ \tc ->+      yield $ LLMChunk rId "" (Just tc)++    let allEmitted = reverse emittedCalls ++ remainingCalls     case mbCompletedInter of       Just completedInter -> do         let finalMsg = interactionToMessage completedInter             usage = toTokenUsage (interactionUsage completedInter)+            alreadyEmittedIds = Set.fromList [(toolCallId tc, toolCallName tc) | tc <- allEmitted]+            missingChunkCalls =+              filter+                (\tc -> not $ Set.member (toolCallId tc, toolCallName tc) alreadyEmittedIds)+                (fromMaybe [] (messageToolCalls finalMsg))+        forM_ missingChunkCalls $ \tc ->+          yield $ LLMChunk rId "" (Just tc)         yield $ LLMEnd rId finalMsg usage       Nothing -> do-        let finalMsg = assistantMessage accumulated+        let base = assistantMessage accumulated+            withCalls =+              if null allEmitted+                then base+                else base {messageToolCalls = Just allEmitted}+            sigs = reverse thoughtSigs+            finalMsg =+              if null sigs+                then withCalls+                else+                  withCalls+                    { messageMetadata =+                        Map.insert+                          "langchain.gemini.thoughtSignatures"+                          (toJSON sigs)+                          (messageMetadata withCalls)+                    }         yield $ LLMEnd rId finalMsg Nothing     where-      consumeStream rId acc mbCompleted =+      finalizePendingCall (cid, cname, initArgs, argText)+        | T.null (T.strip argText) =+            pure+              ToolCall+                { toolCallId = cid+                , toolCallType = "function"+                , toolCallName = cname+                , toolCallArguments = initArgs+                }+        | otherwise =+            case eitherDecodeStrict (TE.encodeUtf8 argText) of+              Left err ->+                throwError $ llmError ("Invalid JSON arguments in Gemini tool call: " <> T.pack err) Nothing Nothing+              Right parsedArgs ->+                pure+                  ToolCall+                    { toolCallId = cid+                    , toolCallType = "function"+                    , toolCallName = cname+                    , toolCallArguments = parsedArgs+                    }++      consumeStream rId acc pending emitted sigs mbCompleted =         await >>= \case-          Nothing -> pure (acc, mbCompleted)+          Nothing -> pure (acc, pending, emitted, sigs, mbCompleted)+          Just (SseStepStart idx FunctionCallStep {..}) ->+            let pending' = Map.insert idx (callId, callName, callArguments, "") pending+             in consumeStream rId acc pending' emitted sigs mbCompleted           Just (SseStepDelta _ (TextDelta txt)) -> do             yield $ LLMChunk rId txt Nothing-            consumeStream rId (acc <> txt) mbCompleted+            consumeStream rId (acc <> txt) pending emitted sigs mbCompleted+          Just (SseStepDelta idx (ArgumentsDelta argDelta)) ->+            let pending' =+                  Map.adjust+                    (\(cid, cname, initArgs, argTxt) -> (cid, cname, initArgs, argTxt <> argDelta))+                    idx+                    pending+             in consumeStream rId acc pending' emitted sigs mbCompleted+          Just (SseStepDelta _ (ThoughtSignatureDelta sig)) ->+            consumeStream rId acc pending emitted (sig : sigs) mbCompleted+          Just (SseStepStop idx) ->+            case Map.lookup idx pending of+              Nothing -> consumeStream rId acc pending emitted sigs mbCompleted+              Just entry -> do+                tc <- finalizePendingCall entry+                yield $ LLMChunk rId "" (Just tc)+                consumeStream rId acc (Map.delete idx pending) (tc : emitted) sigs mbCompleted           Just (SseInteractionCompleted inter) ->-            consumeStream rId acc (Just inter)+            consumeStream rId acc pending emitted sigs (Just inter)           Just (SseError code msg) ->             throwError $               llmError (code <> ": " <> msg) Nothing Nothing           Just _ ->-            consumeStream rId acc mbCompleted+            consumeStream rId acc pending emitted sigs mbCompleted        toTokenUsage Nothing = Nothing       toTokenUsage (Just Usage {..}) =
test/Test/Langchain/Provider/GeminiSpec.hs view
@@ -163,6 +163,58 @@                   case (events, reverse events) of                     (LLMStart {} : _, LLMEnd {} : _) -> pure ()                     other -> assertFailure ("Unexpected events: " ++ show other)+        , testCase "stream emits toolCallDelta chunks from step.start/step.delta/step.stop" $ do+            let sseBody =+                  "data: {\"event_type\": \"step.delta\", \"index\": 0, \"delta\": {\"type\": \"text\", \"text\": \"Checking \"}}\n\n"+                    <> "data: {\"event_type\": \"step.start\", \"index\": 1, \"step\": {\"type\": \"function_call\", \"id\": \"call_g1\", \"name\": \"get_weather\", \"arguments\": {}}}\n\n"+                    <> "data: {\"event_type\": \"step.delta\", \"index\": 1, \"delta\": {\"type\": \"arguments\", \"arguments\": \"{\\\"city\\\":\\\"Paris\\\"}\"}}\n\n"+                    <> "data: {\"event_type\": \"step.stop\", \"index\": 1}\n\n"+                    <> "data: {\"event_type\": \"interaction.completed\", \"interaction\": {\"id\": \"v1_tc\", \"status\": \"requires_action\", \"steps\": [{\"type\": \"model_output\", \"content\": [{\"type\": \"text\", \"text\": \"Checking \"}]}, {\"type\": \"function_call\", \"id\": \"call_g1\", \"name\": \"get_weather\", \"arguments\": {\"city\": \"Paris\"}}]}}\n\n"+                app _req respond =+                  respond $+                    responseStream status200 [("Content-Type", "text/event-stream")] $ \write flush -> do+                      write (Builder.lazyByteString sseBody)+                      flush+                expectedTc =+                  ToolCall+                    { toolCallId = "call_g1"+                    , toolCallType = "function"+                    , toolCallName = "get_weather"+                    , toolCallArguments = [aesonQQ|{"city": "Paris"}|]+                    }+            withMockServer app $ \url -> do+              let provider = newGemini "fake-key" "gemini-3.6-flash" (Just url)+              eEvents <-+                runResourceT $+                  runExceptT $+                    collectEvents $+                      stream provider [userMessage "Weather in Paris?"] Nothing+              case eEvents of+                Left err -> assertFailure (show err)+                Right events -> do+                  let toolChunks = [tc | LLMChunk _ _ (Just tc) <- events]+                  toolChunks @?= [expectedTc]+                  case reverse events of+                    LLMEnd _ finalMsg _ : _ ->+                      messageToolCalls finalMsg @?= Just [expectedTc]+                    _ -> assertFailure ("Expected LLMEnd, got: " ++ show events)+        , testCase "stream emits toolCallDelta from interaction.completed fallback" $ do+            let sseBody =+                  "data: {\"event_type\": \"interaction.completed\", \"interaction\": {\"id\": \"v1_tc2\", \"status\": \"requires_action\", \"steps\": [{\"type\": \"function_call\", \"id\": \"call_g2\", \"name\": \"ping\", \"arguments\": {}}]}}\n\n"+                app _req respond =+                  respond $+                    responseStream status200 [("Content-Type", "text/event-stream")] $ \write flush -> do+                      write (Builder.lazyByteString sseBody)+                      flush+                expectedTc = ToolCall "call_g2" "function" "ping" [aesonQQ|{}|]+            withMockServer app $ \url -> do+              let provider = newGemini "fake-key" "gemini-3.6-flash" (Just url)+              eEvents <- runResourceT $ runExceptT $ collectEvents $ stream provider [userMessage "Ping"] Nothing+              case eEvents of+                Left err -> assertFailure (show err)+                Right events -> do+                  let toolChunks = [tc | LLMChunk _ _ (Just tc) <- events]+                  toolChunks @?= [expectedTc]         ]     , testGroup         "Embeddings GeminiEmbeddings (Mock)"