groq 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8d1461971dedb839a98ceba16edeec695a3fbc48216295314e6c319e5976f621
4
- data.tar.gz: ac0437a0a14d79c9faab3c88054100928970606e90997187c6e908e67a67dc8c
3
+ metadata.gz: af8963c428e4a7760f76a17a48d6c92cdae50363c96d6e97eef21293a3321beb
4
+ data.tar.gz: 92d619e893e9fa727c76f42c85f095cc1e874ba78474840f7c6b1d139d691077
5
5
  SHA512:
6
- metadata.gz: 422b5c160196127928397e568aa15e76dc4f63d1388391bce9cae4ad4d6d0b0fb4063fb52126f04a5b32667179532ea62af96f64acb785ffffed875d4c0646cb
7
- data.tar.gz: a537f489dedaa533e9fdb444c6e6d3007dab7164c8306260b0409cd5d2b8bf8802373320d7a79016f04a8e71295845f2504162aad8d7f4997db30c2cad7e32f5
6
+ metadata.gz: bc25feb5cfdaf37955932e59f079568ce4f135dcd61eade7d13bf1d62777fec08642f246bbe2b90d4b0c0ffaaa28e9bc18092814cd19b2da3f9c06486d713417
7
+ data.tar.gz: 2c436ded0ab2152c5625a7b0690942b7d24b71cb36aef56a95984a5faa31ee8b959230f7f5389c4941ed613f19608971a00c415c22b7b6199d04f6a8674f2a14
data/README.md CHANGED
@@ -60,16 +60,12 @@ JSON.parse(response["content"])
60
60
 
61
61
  Install the gem and add to the application's Gemfile by executing:
62
62
 
63
- ```plain
64
- bundle add groq
63
+ > bundle add groq
65
64
  ```
66
-
67
65
  If bundler is not being used to manage dependencies, install the gem by executing:
68
66
 
69
- ```plain
70
- gem install groq
67
+ > gem install groq
71
68
  ```
72
-
73
69
  ## Usage
74
70
 
75
71
  - Get your API key from [console.groq.com/keys](https://console.groq.com/keys)
@@ -105,10 +101,8 @@ client.chat([
105
101
 
106
102
  ### Interactive console (IRb)
107
103
 
108
- ```plain
109
- bin/console
104
+ > bin/console
110
105
  ```
111
-
112
106
  This repository has a `bin/console` script to start an interactive console to play with the Groq API. The `@client` variable is setup using `$GROQ_API_KEY` environment variable; and the `U`, `A`, `T` helpers are already included.
113
107
 
114
108
  ```ruby
@@ -190,9 +184,8 @@ end
190
184
  The output might looks similar to:
191
185
 
192
186
  ```plain
193
- User message: Hello, world!
187
+ > User message: Hello, world!
194
188
  Assistant reply with model llama3-8b-8192:
195
- {"role"=>"assistant", "content"=>"Hello, world! It's great to meet you! Is there something I can help you with, or would you like to chat?"}
196
189
  Assistant reply with model llama3-70b-8192:
197
190
  {"role"=>"assistant", "content"=>"The classic \"Hello, world!\" It's great to see you here! Is there something I can help you with, or would you like to just chat?"}
198
191
  Assistant reply with model llama2-70b-4096:
@@ -227,6 +220,33 @@ JSON.parse(response["content"])
227
220
  # => {"number"=>7}
228
221
  ```
229
222
 
223
+ ### Using dry-schema with JSON mode
224
+
225
+ As a bonus, the `S` or `System` helper can take a `json_schema:` argument and the system message will include the `JSON` keyword and the formatted schema in its content.
226
+
227
+ For example, if you're using [dry-schema](https://dry-rb.org/gems/dry-schema/1.13/extensions/json_schema/) with its `:json_schema` extension you can use Ruby to describe JSON schema.
228
+
229
+ ```ruby
230
+ require "dry-schema"
231
+ Dry::Schema.load_extensions(:json_schema)
232
+
233
+ person_schema_defn = Dry::Schema.JSON do
234
+ required(:name).filled(:string)
235
+ optional(:age).filled(:integer)
236
+ optional(:email).filled(:string)
237
+ end
238
+ person_schema = person_schema_defn.json_schema
239
+
240
+ response = @client.chat([
241
+ S("You're excellent at extracting personal information", json_schema: person_schema),
242
+ U("I'm Dr Nic and I'm almost 50.")
243
+ ], json: true)
244
+ JSON.parse(response["content"])
245
+ # => {"name"=>"Dr Nic", "age"=>49}
246
+ ```
247
+
248
+ NOTE: `bin/console` already loads the `dry-schema` library and the `json_schema` extension because its handy.
249
+
230
250
  ### Tools/Functions
231
251
 
232
252
  LLMs are increasingly supporting deferring to tools or functions to fetch data, perform calculations, or store structured data. Groq Cloud in turn then supports their tool implementations through its API.
@@ -309,6 +329,273 @@ end
309
329
  @client.chat("Hello, world!", max_tokens: 512, temperature: 0.5)
310
330
  ```
311
331
 
332
+ ### Debugging API calls
333
+
334
+ The underlying HTTP library being used is faraday, and you can enabled debugging, or configure other faraday internals by passing a block to the `Groq::Client.new` constructor.
335
+
336
+ ```ruby
337
+ require 'logger'
338
+
339
+ # Create a logger instance
340
+ logger = Logger.new(STDOUT)
341
+ logger.level = Logger::DEBUG
342
+
343
+ @client = Groq::Client.new do |faraday|
344
+ # Log request and response bodies
345
+ faraday.response :logger, logger, bodies: true
346
+ end
347
+ ```
348
+
349
+ If you pass `--debug` to `bin/console` you will have this logger setup for you.
350
+
351
+ ```plain
352
+ bin/console --debug
353
+ ```
354
+
355
+ ### Streaming
356
+
357
+ If your AI assistant responses are being telecast live to a human, then that human might want some progressive responses. The Groq API supports streaming responses.
358
+
359
+ Pass a block to `chat()` with either one or two arguments.
360
+
361
+ 1. The first argument is the string content chunk of the response.
362
+ 2. The optional second argument is the full response object from the API containing extra metadata.
363
+
364
+ The final block call will be the last chunk of the response:
365
+
366
+ 1. The first argument will be `nil`
367
+ 2. The optional second argument, the full response object, contains a summary of the Groq API usage, such as prompt tokens, prompt time, etc.
368
+
369
+ ```ruby
370
+ puts "🍕 "
371
+ messages = [
372
+ S("You are a pizza sales person."),
373
+ U("What do you sell?")
374
+ ]
375
+ @client.chat(messages) do |content|
376
+ print content
377
+ end
378
+ puts
379
+ ```
380
+
381
+ Each chunk of the response will be printed to the console as it is received. It will look pretty.
382
+
383
+ The default `llama3-7b-8192` model is very very fast and you might not see any streaming. Try a slower model like `llama3-70b-8192` or `mixtral-8x7b-32768`.
384
+
385
+ ```ruby
386
+ @client = Groq::Client.new(model_id: "llama3-70b-8192")
387
+ @client.chat("Write a long poem about patience") do |content|
388
+ print content
389
+ end
390
+ puts
391
+ ```
392
+
393
+ You can pass in a second argument to get the full response JSON object:
394
+
395
+ ```ruby
396
+ @client.chat("Write a long poem about patience") do |content, response|
397
+ pp content
398
+ pp response
399
+ end
400
+ ```
401
+
402
+ Alternately, you can pass a `Proc` or any object that responds to `call` via a `stream:` keyword argument:
403
+
404
+ ```ruby
405
+ @client.chat("Write a long poem about patience", stream: ->(content) { print content })
406
+ ```
407
+
408
+ You could use a class with a `call` method with either one or two arguments, like the `Proc` discussion above.
409
+
410
+ ```ruby
411
+ class MessageBits
412
+ def initialize(emoji)
413
+ print "#{emoji} "
414
+ @bits = []
415
+ end
416
+
417
+ def call(content)
418
+ if content.nil?
419
+ puts
420
+ else
421
+ print(content)
422
+ @bits << content
423
+ end
424
+ end
425
+
426
+ def to_s
427
+ @bits.join("")
428
+ end
429
+
430
+ def to_assistant_message
431
+ Assistant(to_s)
432
+ end
433
+ end
434
+
435
+ bits = MessageBits.new("🍕")
436
+ @client.chat("Write a long poem about pizza", stream: bits)
437
+ ```
438
+
439
+ ## Examples
440
+
441
+ Here are some example uses of Groq, of the `groq` gem and its syntax.
442
+
443
+ Also, see the [`examples/`](examples/) folder for more example apps.
444
+
445
+ ### Pizzeria agent
446
+
447
+ Talking with a pizzeria.
448
+
449
+ Our pizzeria agent can be as simple as a function that combines a system message and the current messages array:
450
+
451
+ ```ruby
452
+ @agent_message = <<~EOS
453
+ You are an employee at a pizza store.
454
+
455
+ You sell hawaiian, and pepperoni pizzas; in small and large sizes for $10, and $20 respectively.
456
+
457
+ Pick up only in. Ready in 10 mins. Cash on pickup.
458
+ EOS
459
+
460
+ def chat_pizza_agent(messages)
461
+ @client.chat([
462
+ System(@agent_message),
463
+ *messages
464
+ ])
465
+ end
466
+ ```
467
+
468
+ Now for our first interaction:
469
+
470
+ ```ruby
471
+ messages = [U("Is this the pizza shop? Do you sell hawaiian?")]
472
+
473
+ response = chat_pizza_agent(messages)
474
+ puts response["content"]
475
+ ```
476
+
477
+ The output might be:
478
+
479
+ > Yeah! This is the place! Yes, we sell Hawaiian pizzas here! We've got both small and large sizes available for you. The small Hawaiian pizza is $10, and the large one is $20. Plus, because we're all about getting you your pizza fast, our pick-up time is only 10 minutes! So, what can I get for you today? Would you like to order a small or large Hawaiian pizza?
480
+
481
+ Continue with user's reply.
482
+
483
+ Note, we build the `messages` array with the previous user and assistant messages and the new user message:
484
+
485
+ ```ruby
486
+ messages << response << U("Yep, give me a large.")
487
+ response = chat_pizza_agent(messages)
488
+ puts response["content"]
489
+ ```
490
+
491
+ Response:
492
+
493
+ > I'll get that ready for you. So, to confirm, you'd like to order a large Hawaiian pizza for $20, and I'll have it ready for you in 10 minutes. When you come to pick it up, please have the cash ready as we're a cash-only transaction. See you in 10!
494
+
495
+ Making a change:
496
+
497
+ ```ruby
498
+ messages << response << U("Actually, make it two smalls.")
499
+ response = chat_pizza_agent(messages)
500
+ puts response["content"]
501
+ ```
502
+
503
+ Response:
504
+
505
+ > I've got it! Two small Hawaiian pizzas on the way! That'll be $20 for two small pizzas. Same deal, come back in 10 minutes to pick them up, and bring cash for the payment. See you soon!
506
+
507
+ ### Pizza customer agent
508
+
509
+ Oh my. Let's also have an agent that represents the customer.
510
+
511
+ ```ruby
512
+ @customer_message = <<~EOS
513
+ You are a customer at a pizza store.
514
+
515
+ You want to order a pizza. You can ask about the menu, prices, sizes, and pickup times.
516
+
517
+ You'll agree with the price and terms of the pizza order.
518
+
519
+ You'll make a choice of the available options.
520
+
521
+ If you're first in the conversation, you'll say hello and ask about the menu.
522
+ EOS
523
+
524
+ def chat_pizza_customer(messages)
525
+ @client.chat([
526
+ System(@customer_message),
527
+ *messages
528
+ ])
529
+ end
530
+ ```
531
+
532
+ First interaction starts with no user or assistant messages. We're generating the customer's first message:
533
+
534
+ ```ruby
535
+ customer_messages = []
536
+ response = chat_pizza_customer(customer_messages)
537
+ puts response["content"]
538
+ ```
539
+
540
+ Customer's first message:
541
+
542
+ > Hello! I'd like to order a pizza. Could you tell me more about the menu and prices? What kind of pizzas do you have available?
543
+
544
+ Now we need to pass this to the pizzeria agent:
545
+
546
+ ```ruby
547
+ customer_message = response["content"]
548
+ pizzeria_messages = [U(customer_message)]
549
+ response = chat_pizza_agent(pizzeria_messages)
550
+ puts response["content"]
551
+ ```
552
+
553
+ Pizzeria agent response:
554
+
555
+ > Hi there! Yeah, sure thing! We've got two delicious options to choose from: Hawaiian and Pepperoni. Both come in small and large sizes. The small pizzas are $10 and the large pizzas are $20.
556
+ >
557
+ > Our Hawaiian pizza features fresh ham and pineapple on a bed of melted mozzarella. And if you're in the mood for something classic, our Pepperoni pizza is loaded with plenty of sliced pepperoni and melted mozzarella cheese.
558
+
559
+ Now let's add this response to the customer agent's message array, and generate the customer's next response to the pizzera:
560
+
561
+ ```ruby
562
+ customer_messages << U(response["content"])
563
+ response = chat_pizza_customer(customer_messages)
564
+ puts response["content"]
565
+ ```
566
+
567
+ Customer agent response:
568
+
569
+ > Wow, those both sound delicious! I'm intrigued by the Hawaiian combo, I never thought of putting ham and pineapple on a pizza before. How would you recommend I customize it? Can I add any extra toppings or keep it as is? And do you have any recommendations for the size? Small or large?
570
+
571
+ Add this to the pizzeria agent's message array, and generate the pizzeria's response:
572
+
573
+ ```ruby
574
+ pizzeria_messages << U(response["content"])
575
+ response = chat_pizza_agent(pizzeria_messages)
576
+ puts response["content"]
577
+ ```
578
+
579
+ Pizzeria agent response:
580
+
581
+ > The Hawaiian pizza is definitely a unique twist on traditional toppings! You can definitely customize it to your liking. We allow two extra toppings of your choice for an additional $1 each. If you want to add any other toppings beyond that, it's $2 per topping.
582
+ >
583
+ > As for recommends, I'd say the small size is a great starting point, especially if you're trying something new like the Hawaiian pizza. The small size is $10 and it's a great bite-sized portion. But if you're looking for a bigger pie, the large size is $20 and would be a great option if you're feeding a crowd or want leftovers.
584
+ >
585
+ > Keep in mind that our pizzas are cooked fresh in 10 minutes, so it's ready when it's ready! Would you like to place an order now?
586
+
587
+ Will the customer actually buy anything now?
588
+
589
+ > I think I'd like to go with the Hawaiian pizza in the small size, so the total would be $10. And I'll take advantage of the extra topping option. I think I'll add some mushrooms to it. So, that's an extra $1 for the mushroom topping. Would that be $11 total? And do you have a pickup time available soon?
590
+
591
+ OMG, the customer bought something.
592
+
593
+ Pizzeria agent response:
594
+
595
+ > That sounds like a great choice! Yeah, the total would be $11, the small Hawaiian pizza with mushrooms. And yes, we do have pickup available shortly. It'll be ready in about 10 minutes. Cash on pickup, okay? Would you like to pay when you pick up your pizza?
596
+
597
+ Maybe these two do not know how to stop talking. The Halting Problem exists in pizza shops too.
598
+
312
599
  ## Development
313
600
 
314
601
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -0,0 +1,61 @@
1
+ # Examples
2
+
3
+ ## User Chat
4
+
5
+ Chat with a pre-defined agent using the following command:
6
+
7
+ ```bash
8
+ bundle exec examples/groq-user-chat.rb
9
+ # or
10
+ bundle exec examples/groq-user-chat.rb --agent-prompt examples/agent-prompts/helloworld.yml
11
+ ```
12
+
13
+ There are two example agent prompts available:
14
+
15
+ - `examples/agent-prompts/helloworld.yml` (the default)
16
+ - `examples/agent-prompts/pizzeria-sales.yml`
17
+
18
+ At the prompt, either talk to the AI agent, or some special commands:
19
+
20
+ - `exit` to exit the conversation
21
+ - `summary` to get a summary of the conversation so far
22
+
23
+ ### Streaming
24
+
25
+ There is also an example of streaming the conversation to terminal as it is received from Groq API.
26
+
27
+ It defaults to the slower `llama3-70b-8192` model so that the streaming is more noticable.
28
+
29
+ ```bash
30
+ bundle exec examples/groq-user-chat-streaming.rb --agent-prompt examples/agent-prompts/pizzeria-sales.yml
31
+ ```
32
+
33
+ ### Pizzeria
34
+
35
+ Run the pizzeria example with the following command:
36
+
37
+ ```bash
38
+ bundle exec examples/groq-user-chat.rb --agent-prompt examples/agent-prompts/pizzeria-sales.yml
39
+ ```
40
+
41
+ > 🍕 Hello! Thank you for calling our pizzeria. I'm happy to help you with your inquiry. Do you have a question about our menu or would you like to place an order?
42
+ >
43
+ > 😋 What's your cheapest?
44
+ >
45
+ > 🍕 Our cheapest menu item is the Garlic Knots (6 pieces), which is served with marinara sauce and priced at $5. Would you like to add it to your order or is there something else that catches your attention?
46
+ >
47
+ > 😋 Yes. And a cheap pizza.
48
+ >
49
+ > 🍕 Our cheapest pizza option is the Veggie Pizza. It comes with tomato sauce, mozzarella, and a mix of bell peppers, onions, and mushrooms, all for $13. Would you like to order the Veggie Pizza and the Garlic Knots together?
50
+ >
51
+ > 😋 Yep
52
+ >
53
+ > 🍕 I've taken note of your order. You're ordering a Veggie Pizza and 6 Garlic Knots. Your total comes out to be $18. Would you like to pay by credit card, cash, or take any other payment method?
54
+ >
55
+ > 😋 summary
56
+ >
57
+ > Here is a summary of the current conversation:
58
+ >
59
+ > The conversation started with a customer calling the pizzeria and speaking with an AI assistant. The assistant offered to help with menu inquiries or taking an order. The customer was considering ordering from the menu. The assistant presented the cheapest menu item, Garlic Knots, and asked if the customer wanted to add it to their order. The customer may have been interested in other options as well. The assistant then presented the cheapest pizza option, the Veggie Pizza, and asked if the customer wanted to order it along with the Garlic Knots. The customer agreed, and the assistant took note of the order, which consisted of a Veggie Pizza and 6 Garlic Knots for a total of $18. The assistant asked how the customer would like to pay for their order.
60
+ >
61
+ > 😋 exit
@@ -0,0 +1,6 @@
1
+ ---
2
+ system: |-
3
+ I am a friendly agent who always replies to any prompt
4
+ with a pleasant "Hello" and wishing them well.
5
+ agent_emoji: "🤖"
6
+ user_emoji: "👤"
@@ -0,0 +1,19 @@
1
+ ---
2
+ system: |-
3
+ You are a phone operator at a busy pizzeria. Your responsibilities include answering calls and online chats from customers who may ask about the menu, wish to place or change orders, or inquire about opening hours.
4
+
5
+ Here are some of our popular menu items:
6
+
7
+ <menu>
8
+ Margherita Pizza: Classic with tomato sauce, mozzarella, and basil - $12
9
+ Pepperoni Pizza: Tomato sauce, mozzarella, and a generous layer of pepperoni - $14
10
+ Veggie Pizza: Tomato sauce, mozzarella, and a mix of bell peppers, onions, and mushrooms - $13
11
+ BBQ Chicken Pizza: BBQ sauce, chicken, onions, and cilantro - $15
12
+ Garlic Knots (6 pieces): Served with marinara sauce - $5
13
+ Cannoli: Classic Sicilian dessert filled with sweet ricotta cream - $4 each
14
+ </menu>
15
+
16
+ Your goal is to provide accurate information, confirm order details, and ensure a pleasant customer experience. Please maintain a polite and professional tone, be prompt in your responses, and ensure accuracy in order transmission.
17
+ agent_emoji: "🍕"
18
+ user_emoji: "😋"
19
+ can_go_first: true
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "groq"
5
+ require "yaml"
6
+
7
+ include Groq::Helpers
8
+
9
+ @options = {
10
+ model: "llama3-70b-8192",
11
+ agent_prompt_path: File.join(File.dirname(__FILE__), "agent-prompts/helloworld.yml"),
12
+ timeout: 20
13
+ }
14
+ OptionParser.new do |opts|
15
+ opts.banner = "Usage: ruby script.rb [options]"
16
+
17
+ opts.on("-m", "--model MODEL", "Model name") do |v|
18
+ @options[:model] = v
19
+ end
20
+
21
+ opts.on("-a", "--agent-prompt PATH", "Path to agent prompt file") do |v|
22
+ @options[:agent_prompt_path] = v
23
+ end
24
+
25
+ opts.on("-t", "--timeout TIMEOUT", "Timeout in seconds") do |v|
26
+ @options[:timeout] = v.to_i
27
+ end
28
+
29
+ opts.on("-d", "--debug", "Enable debug mode") do |v|
30
+ @options[:debug] = v
31
+ end
32
+ end.parse!
33
+
34
+ raise "Missing --model option" if @options[:model].nil?
35
+ raise "Missing --agent-prompt option" if @options[:agent_prompt_path].nil?
36
+
37
+ def debug?
38
+ @options[:debug]
39
+ end
40
+
41
+ # Read the agent prompt from the file
42
+ agent_prompt = YAML.load_file(@options[:agent_prompt_path])
43
+ user_emoji = agent_prompt["user_emoji"]
44
+ agent_emoji = agent_prompt["agent_emoji"]
45
+ system_prompt = agent_prompt["system_prompt"] || agent_prompt["system"]
46
+ can_go_first = agent_prompt["can_go_first"]
47
+
48
+ # Initialize the Groq client
49
+ @client = Groq::Client.new(model_id: @options[:model], request_timeout: @options[:timeout]) do |f|
50
+ if debug?
51
+ require "logger"
52
+
53
+ # Create a logger instance
54
+ logger = Logger.new($stdout)
55
+ logger.level = Logger::DEBUG
56
+
57
+ f.response :logger, logger, bodies: true # Log request and response bodies
58
+ end
59
+ end
60
+
61
+ puts "Welcome to the AI assistant! I'll respond to your queries."
62
+ puts "You can quit by typing 'exit'."
63
+
64
+ def produce_summary(messages)
65
+ combined = messages.map do |message|
66
+ if message["role"] == "user"
67
+ "User: #{message["content"]}"
68
+ else
69
+ "Assistant: #{message["content"]}"
70
+ end
71
+ end.join("\n")
72
+ response = @client.chat([
73
+ S("You are excellent at reading a discourse between a human and an AI assistant and summarising the current conversation."),
74
+ U("Here is the current conversation:\n\n------\n\n#{combined}")
75
+ ])
76
+ puts response["content"]
77
+ end
78
+
79
+ messages = [S(system_prompt)]
80
+
81
+ if can_go_first
82
+ print "#{agent_emoji} "
83
+ message_bits = []
84
+ response = @client.chat(messages) do |content|
85
+ # content == nil on last message; and "" on first message
86
+ next unless content
87
+ print(content)
88
+ message_bits << content
89
+ end
90
+ puts
91
+ messages << A(message_bits.join(""))
92
+ end
93
+
94
+ class MessageBits
95
+ def initialize(emoji)
96
+ print "#{emoji} "
97
+ @bits = []
98
+ end
99
+
100
+ def call(content)
101
+ if content.nil?
102
+ puts
103
+ else
104
+ print(content)
105
+ @bits << content
106
+ end
107
+ end
108
+
109
+ def to_assistant_message
110
+ Assistant(@bits.join(""))
111
+ end
112
+ end
113
+
114
+ loop do
115
+ print "#{user_emoji} "
116
+ user_input = gets.chomp
117
+
118
+ break if user_input.downcase == "exit"
119
+
120
+ # produce summary
121
+ if user_input.downcase == "summary"
122
+ produce_summary(messages)
123
+ next
124
+ end
125
+
126
+ messages << U(user_input)
127
+
128
+ # Use Groq to generate a response
129
+ message_bits = MessageBits.new(agent_emoji)
130
+ @client.chat(messages, stream: message_bits)
131
+ messages << message_bits.to_assistant_message
132
+ end
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "groq"
5
+ require "yaml"
6
+
7
+ include Groq::Helpers
8
+
9
+ @options = {
10
+ model: "llama3-8b-8192",
11
+ # model: "llama3-70b-8192",
12
+ agent_prompt_path: File.join(File.dirname(__FILE__), "agent-prompts/helloworld.yml"),
13
+ timeout: 20
14
+ }
15
+ OptionParser.new do |opts|
16
+ opts.banner = "Usage: ruby script.rb [options]"
17
+
18
+ opts.on("-m", "--model MODEL", "Model name") do |v|
19
+ @options[:model] = v
20
+ end
21
+
22
+ opts.on("-a", "--agent-prompt PATH", "Path to agent prompt file") do |v|
23
+ @options[:agent_prompt_path] = v
24
+ end
25
+
26
+ opts.on("-t", "--timeout TIMEOUT", "Timeout in seconds") do |v|
27
+ @options[:timeout] = v.to_i
28
+ end
29
+
30
+ opts.on("-d", "--debug", "Enable debug mode") do |v|
31
+ @options[:debug] = v
32
+ end
33
+ end.parse!
34
+
35
+ raise "Missing --model option" if @options[:model].nil?
36
+ raise "Missing --agent-prompt option" if @options[:agent_prompt_path].nil?
37
+
38
+ def debug?
39
+ @options[:debug]
40
+ end
41
+
42
+ # Read the agent prompt from the file
43
+ agent_prompt = YAML.load_file(@options[:agent_prompt_path])
44
+ user_emoji = agent_prompt["user_emoji"]
45
+ agent_emoji = agent_prompt["agent_emoji"]
46
+ system_prompt = agent_prompt["system_prompt"] || agent_prompt["system"]
47
+ can_go_first = agent_prompt["can_go_first"]
48
+
49
+ # Initialize the Groq client
50
+ @client = Groq::Client.new(model_id: @options[:model], request_timeout: @options[:timeout]) do |f|
51
+ if debug?
52
+ require "logger"
53
+
54
+ # Create a logger instance
55
+ logger = Logger.new($stdout)
56
+ logger.level = Logger::DEBUG
57
+
58
+ f.response :logger, logger, bodies: true # Log request and response bodies
59
+ end
60
+ end
61
+
62
+ puts "Welcome to the AI assistant! I'll respond to your queries."
63
+ puts "You can quit by typing 'exit'."
64
+
65
+ def produce_summary(messages)
66
+ combined = messages.map do |message|
67
+ if message["role"] == "user"
68
+ "User: #{message["content"]}"
69
+ else
70
+ "Assistant: #{message["content"]}"
71
+ end
72
+ end.join("\n")
73
+ response = @client.chat([
74
+ S("You are excellent at reading a discourse between a human and an AI assistant and summarising the current conversation."),
75
+ U("Here is the current conversation:\n\n------\n\n#{combined}")
76
+ ])
77
+ puts response["content"]
78
+ end
79
+
80
+ messages = [S(system_prompt)]
81
+
82
+ if can_go_first
83
+ response = @client.chat(messages)
84
+ puts "#{agent_emoji} #{response["content"]}"
85
+ messages << response
86
+ end
87
+
88
+ loop do
89
+ print "#{user_emoji} "
90
+ user_input = gets.chomp
91
+
92
+ break if user_input.downcase == "exit"
93
+
94
+ # produce summary
95
+ if user_input.downcase == "summary"
96
+ produce_summary(messages)
97
+ next
98
+ end
99
+
100
+ messages << U(user_input)
101
+
102
+ # Use Groq to generate a response
103
+ response = @client.chat(messages)
104
+
105
+ message = response.dig("content")
106
+ puts "#{agent_emoji} #{message}"
107
+
108
+ messages << response
109
+ end
data/lib/groq/client.rb CHANGED
@@ -7,6 +7,7 @@ class Groq::Client
7
7
  model_id
8
8
  max_tokens
9
9
  temperature
10
+ request_timeout
10
11
  ].freeze
11
12
  attr_reader(*CONFIG_KEYS, :faraday_middleware)
12
13
 
@@ -21,8 +22,7 @@ class Groq::Client
21
22
  @faraday_middleware = faraday_middleware
22
23
  end
23
24
 
24
- # TODO: support stream: true; or &stream block
25
- def chat(messages, model_id: nil, tools: nil, max_tokens: nil, temperature: nil, json: false)
25
+ def chat(messages, model_id: nil, tools: nil, tool_choice: nil, max_tokens: nil, temperature: nil, json: false, stream: nil, &stream_chunk)
26
26
  unless messages.is_a?(Array) || messages.is_a?(String)
27
27
  raise ArgumentError, "require messages to be an Array or String"
28
28
  end
@@ -33,45 +33,128 @@ class Groq::Client
33
33
 
34
34
  model_id ||= @model_id
35
35
 
36
+ if stream_chunk ||= stream
37
+ require "event_stream_parser"
38
+ end
39
+
36
40
  body = {
37
41
  model: model_id,
38
42
  messages: messages,
39
43
  tools: tools,
44
+ tool_choice: tool_choice,
40
45
  max_tokens: max_tokens || @max_tokens,
41
46
  temperature: temperature || @temperature,
42
- response_format: json ? {type: "json_object"} : nil
47
+ response_format: json ? {type: "json_object"} : nil,
48
+ stream_chunk: stream_chunk
43
49
  }.compact
44
50
  response = post(path: "/openai/v1/chat/completions", body: body)
45
- if response.status == 200
46
- response.body.dig("choices", 0, "message")
47
- else
48
- # TODO: send the response.body back in Error object
49
- puts "Error: #{response.status}"
50
- pp response.body
51
- raise Error, "Request failed with status #{response.status}: #{response.body}"
51
+ # Configured to raise exceptions on 4xx/5xx responses
52
+ if response.body.is_a?(Hash)
53
+ return response.body.dig("choices", 0, "message")
52
54
  end
55
+ response.body
53
56
  end
54
57
 
55
58
  def get(path:)
56
- client.get do |req|
57
- req.url path
58
- req.headers["Authorization"] = "Bearer #{@api_key}"
59
+ client.get(path) do |req|
60
+ req.headers = headers
59
61
  end
60
62
  end
61
63
 
62
64
  def post(path:, body:)
63
- client.post do |req|
64
- req.url path
65
- req.headers["Authorization"] = "Bearer #{@api_key}"
66
- req.body = body
65
+ client.post(path) do |req|
66
+ configure_json_post_request(req, body)
67
67
  end
68
68
  end
69
69
 
70
70
  def client
71
- @client ||= Faraday.new(url: @api_url) do |f|
72
- f.request :json # automatically encode the request body as JSON
73
- f.response :json # automatically decode JSON responses
74
- f.adapter Faraday.default_adapter
71
+ @client ||= begin
72
+ connection = Faraday.new(url: @api_url) do |f|
73
+ f.request :json # automatically encode the request body as JSON
74
+ f.response :json # automatically decode JSON responses
75
+ f.response :raise_error # raise exceptions on 4xx/5xx responses
76
+
77
+ f.adapter Faraday.default_adapter
78
+ f.options[:timeout] = request_timeout
79
+ end
80
+ @faraday_middleware&.call(connection)
81
+
82
+ connection
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def headers
89
+ {
90
+ "Authorization" => "Bearer #{@api_key}",
91
+ "User-Agent" => "groq-ruby/#{Groq::VERSION}"
92
+ }
93
+ end
94
+
95
+ #
96
+ # Code/ideas borrowed from lib/openai/http.rb in https://github.com/alexrudall/ruby-openai/
97
+ #
98
+
99
+ def configure_json_post_request(req, body)
100
+ req_body = body.dup
101
+
102
+ if body[:stream_chunk].respond_to?(:call)
103
+ req.options.on_data = to_json_stream(user_proc: body[:stream_chunk])
104
+ req_body[:stream] = true # Tell Groq to stream
105
+ req_body.delete(:stream_chunk)
106
+ elsif body[:stream_chunk]
107
+ raise ArgumentError, "The stream_chunk parameter must be a Proc or have a #call method"
108
+ end
109
+
110
+ req.headers = headers
111
+ req.body = req_body
112
+ end
113
+
114
+ # Given a proc, returns an outer proc that can be used to iterate over a JSON stream of chunks.
115
+ # For each chunk, the inner user_proc is called giving it the JSON object. The JSON object could
116
+ # be a data object or an error object as described in the OpenAI API documentation.
117
+ #
118
+ # @param user_proc [Proc] The inner proc to call for each JSON object in the chunk.
119
+ # @return [Proc] An outer proc that iterates over a raw stream, converting it to JSON.
120
+ def to_json_stream(user_proc:)
121
+ parser = EventStreamParser::Parser.new
122
+
123
+ proc do |chunk, _bytes, env|
124
+ if env && env.status != 200
125
+ raise_error = Faraday::Response::RaiseError.new
126
+ raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
127
+ end
128
+
129
+ parser.feed(chunk) do |_type, data|
130
+ next if data == "[DONE]"
131
+ chunk = JSON.parse(data)
132
+ delta = chunk.dig("choices", 0, "delta")
133
+ content = delta.dig("content")
134
+ if user_proc.is_a?(Proc)
135
+ # if user_proc takes one argument, pass the content
136
+ if user_proc.arity == 1
137
+ user_proc.call(content)
138
+ else
139
+ user_proc.call(content, chunk)
140
+ end
141
+ elsif user_proc.respond_to?(:call)
142
+ # if call method takes one argument, pass the content
143
+ if user_proc.method(:call).arity == 1
144
+ user_proc.call(content)
145
+ else
146
+ user_proc.call(content, chunk)
147
+ end
148
+ else
149
+ raise ArgumentError, "The stream_chunk parameter must be a Proc or have a #call method"
150
+ end
151
+ end
75
152
  end
76
153
  end
154
+
155
+ def try_parse_json(maybe_json)
156
+ JSON.parse(maybe_json)
157
+ rescue JSON::ParserError
158
+ maybe_json
159
+ end
77
160
  end
data/lib/groq/helpers.rb CHANGED
@@ -13,7 +13,10 @@ module Groq::Helpers
13
13
  end
14
14
  alias_method :Assistant, :A
15
15
 
16
- def S(content)
16
+ def S(content, json_schema: nil)
17
+ if json_schema
18
+ content += "\nJSON must use schema: #{json_schema}"
19
+ end
17
20
  {role: "system", content: content}
18
21
  end
19
22
  alias_method :System, :S
data/lib/groq/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Groq
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: groq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dr Nic Williams
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-20 00:00:00.000000000 Z
11
+ date: 2024-04-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: event_stream_parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: vcr
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +94,20 @@ dependencies:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
96
  version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dry-schema
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.13'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.13'
83
111
  description: Client library for Groq API for fast LLM inference.
84
112
  email:
85
113
  - drnicwilliams@gmail.com
@@ -94,6 +122,11 @@ files:
94
122
  - README.md
95
123
  - Rakefile
96
124
  - docs/images/groq-speed-price-20240421.png
125
+ - examples/README.md
126
+ - examples/agent-prompts/helloworld.yml
127
+ - examples/agent-prompts/pizzeria-sales.yml
128
+ - examples/groq-user-chat-streaming.rb
129
+ - examples/groq-user-chat.rb
97
130
  - lib/groq-ruby.rb
98
131
  - lib/groq.rb
99
132
  - lib/groq/client.rb