|
| 1 | +import Web.Scotty |
| 2 | +import Network.HTTP.Client.TLS (tlsManagerSettings) |
| 3 | +import Network.HTTP.Client |
| 4 | +import Network.HTTP.Types.Status (status500) |
| 5 | +import qualified Data.ByteString.Char8 as BS |
| 6 | +import qualified Data.Text.Lazy as TL |
| 7 | +import qualified Data.Aeson as Aeson |
| 8 | +import Data.Aeson (FromJSON, ToJSON) |
| 9 | +import GHC.Generics |
| 10 | +import qualified Data.Text as T |
| 11 | +import System.IO (hFlush, stdout) |
| 12 | +import System.Environment (getEnv) |
| 13 | +import qualified Data.Vector as V |
| 14 | + |
| 15 | +data GeminiRequest = GeminiRequest |
| 16 | + { prompt :: String |
| 17 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 18 | + |
| 19 | +data GeminiResponse = GeminiResponse |
| 20 | + { candidates :: [Candidate] |
| 21 | + , promptFeedback :: Maybe PromptFeedback |
| 22 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 23 | + |
| 24 | +data Candidate = Candidate |
| 25 | + { content :: Content2 |
| 26 | + , finishReason :: Maybe String |
| 27 | + , index :: Maybe Int |
| 28 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 29 | + |
| 30 | +data Content2 = Content2 |
| 31 | + { parts :: [Part] |
| 32 | + , role :: Maybe String |
| 33 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 34 | + |
| 35 | +data Part = Part |
| 36 | + { text :: String |
| 37 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 38 | + |
| 39 | +data PromptFeedback = PromptFeedback |
| 40 | + { blockReason :: Maybe String |
| 41 | + , safetyRatings :: Maybe [SafetyRating] |
| 42 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 43 | + |
| 44 | +data SafetyRating = SafetyRating |
| 45 | + { category :: String |
| 46 | + , probability :: String |
| 47 | + } deriving (Show, Generic, FromJSON, ToJSON) |
| 48 | + |
| 49 | + |
| 50 | +main :: IO () |
| 51 | +main = do |
| 52 | + apiKey <- getEnv "GOOGLE_API_KEY" |
| 53 | + scotty 3000 $ do |
| 54 | + get "/" $ do |
| 55 | + html $ TL.pack |
| 56 | + "<!DOCTYPE html>\ |
| 57 | + \<html>\ |
| 58 | + \<head>\ |
| 59 | + \<title>Gemini Chat</title>\ |
| 60 | + \</head>\ |
| 61 | + \<body>\ |
| 62 | + \ <h1>Gemini Chat</h1>\ |
| 63 | + \ <form id='chat-form'>\ |
| 64 | + \ <input type='text' id='prompt' name='prompt' placeholder='Enter your prompt'\ |
| 65 | + \ style='width: 70%; padding: 10px; font-size: 16px;'>\ |
| 66 | + \ <button type='submit'>Send</button>\ |
| 67 | + \ </form><br/><br/><h4>Response:</h4>\ |
| 68 | + \ <div id='response'></div>\ |
| 69 | + \ <script>\ |
| 70 | + \ const form = document.getElementById('chat-form');\ |
| 71 | + \ const responseDiv = document.getElementById('response');\ |
| 72 | + \ form.addEventListener('submit', async (event) => {\ |
| 73 | + \ event.preventDefault();\ |
| 74 | + \ const prompt = document.getElementById('prompt').value;\ |
| 75 | + \ try {\ |
| 76 | + \ const response = await fetch('/chat', {\ |
| 77 | + \ method: 'POST',\ |
| 78 | + \ headers: { 'Content-Type': 'application/json' },\ |
| 79 | + \ body: JSON.stringify({ prompt: prompt })\ |
| 80 | + \ });\ |
| 81 | + \ const data = await response.json();\ |
| 82 | + \ responseDiv.innerText = data.text;\ |
| 83 | + \ } catch (error) {\ |
| 84 | + \ console.error('Error:', error);\ |
| 85 | + \ responseDiv.innerText = 'Error occurred while fetching response';\ |
| 86 | + \ }\ |
| 87 | + \ });\ |
| 88 | + \ </script>\ |
| 89 | + \</body>\ |
| 90 | + \</html>" |
| 91 | + |
| 92 | + post "/chat" $ do |
| 93 | + req <- jsonData :: ActionM GeminiRequest |
| 94 | + liftIO $ putStrLn $ "Received request: " ++ show req |
| 95 | + liftIO $ hFlush stdout |
| 96 | + |
| 97 | + manager <- liftIO $ newManager tlsManagerSettings |
| 98 | + |
| 99 | + initialRequest <- liftIO $ parseRequest |
| 100 | + "https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent" |
| 101 | + |
| 102 | + let geminiRequestBody = Aeson.object |
| 103 | + [ ("contents", Aeson.Array $ V.singleton $ Aeson.object |
| 104 | + [ ("parts", Aeson.Array $ V.singleton $ Aeson.object |
| 105 | + [ ("text", Aeson.String $ T.pack $ prompt req) |
| 106 | + ] |
| 107 | + ) |
| 108 | + ] |
| 109 | + ) |
| 110 | + , ("generationConfig", Aeson.object |
| 111 | + [ ("temperature", Aeson.Number 0.1) |
| 112 | + , ("maxOutputTokens", Aeson.Number 800) |
| 113 | + ] |
| 114 | + ) |
| 115 | + ] |
| 116 | + |
| 117 | + let request2 = initialRequest |
| 118 | + { requestHeaders = |
| 119 | + [ ("Content-Type", "application/json") |
| 120 | + , ("x-goog-api-key", BS.pack apiKey) |
| 121 | + ] |
| 122 | + , method = "POST" |
| 123 | + , requestBody = RequestBodyLBS $ Aeson.encode geminiRequestBody |
| 124 | + } |
| 125 | + |
| 126 | + liftIO $ putStrLn $ "Request body: " ++ show (Aeson.encode geminiRequestBody) |
| 127 | + liftIO $ hFlush stdout |
| 128 | + |
| 129 | + response2 <- liftIO $ httpLbs request2 manager |
| 130 | + liftIO $ do |
| 131 | + putStrLn $ "Response status: " ++ show (responseStatus response2) |
| 132 | + putStrLn $ "Response headers: " ++ show (responseHeaders response2) |
| 133 | + putStrLn $ "Raw response: " ++ show (responseBody response2) |
| 134 | + hFlush stdout |
| 135 | + |
| 136 | + let maybeGeminiResponse = Aeson.decode (responseBody response2) :: Maybe GeminiResponse |
| 137 | + |
| 138 | + liftIO $ putStrLn $ "Parsed response: " ++ show maybeGeminiResponse -- Debug print |
| 139 | + liftIO $ hFlush stdout |
| 140 | + |
| 141 | + case maybeGeminiResponse of |
| 142 | + Just geminiResponse -> do |
| 143 | + case candidates geminiResponse of |
| 144 | + (candidate:_) -> do |
| 145 | + case parts (content candidate) of |
| 146 | + (part:_) -> do |
| 147 | + liftIO $ putStrLn $ "Sending response: " ++ show part |
| 148 | + liftIO $ hFlush stdout |
| 149 | + json part |
| 150 | + [] -> do |
| 151 | + liftIO $ putStrLn "No parts in response" |
| 152 | + status status500 >> Web.Scotty.text "No content in response" |
| 153 | + [] -> do |
| 154 | + liftIO $ putStrLn "No candidates in response" |
| 155 | + status status500 >> Web.Scotty.text "No candidates in response" |
| 156 | + Nothing -> do |
| 157 | + liftIO $ putStrLn "Failed to parse response" |
| 158 | + status status500 >> Web.Scotty.text "Failed to parse Gemini response" |
0 commit comments