llms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +160 -0
  4. data/bin/llms-chat +6 -0
  5. data/bin/llms-test-model-access +4 -0
  6. data/bin/llms-test-model-image-support +4 -0
  7. data/bin/llms-test-model-prompt-caching +4 -0
  8. data/bin/llms-test-model-tool-use +5 -0
  9. data/lib/llms/adapters/anthropic_message_adapter.rb +73 -0
  10. data/lib/llms/adapters/anthropic_tool_call_adapter.rb +20 -0
  11. data/lib/llms/adapters/base_message_adapter.rb +60 -0
  12. data/lib/llms/adapters/google_gemini_message_adapter.rb +72 -0
  13. data/lib/llms/adapters/google_gemini_tool_call_adapter.rb +20 -0
  14. data/lib/llms/adapters/open_ai_compatible_message_adapter.rb +88 -0
  15. data/lib/llms/adapters/open_ai_compatible_tool_call_adapter.rb +67 -0
  16. data/lib/llms/adapters.rb +12 -0
  17. data/lib/llms/apis/google_gemini_api.rb +45 -0
  18. data/lib/llms/apis/open_ai_compatible_api.rb +54 -0
  19. data/lib/llms/cli/base.rb +186 -0
  20. data/lib/llms/cli/chat.rb +92 -0
  21. data/lib/llms/cli/test_access.rb +79 -0
  22. data/lib/llms/cli/test_image_support.rb +92 -0
  23. data/lib/llms/cli/test_prompt_caching.rb +275 -0
  24. data/lib/llms/cli/test_tool_use.rb +108 -0
  25. data/lib/llms/cli.rb +12 -0
  26. data/lib/llms/conversation.rb +100 -0
  27. data/lib/llms/conversation_message.rb +60 -0
  28. data/lib/llms/conversation_tool_call.rb +14 -0
  29. data/lib/llms/conversation_tool_result.rb +15 -0
  30. data/lib/llms/exceptions.rb +33 -0
  31. data/lib/llms/executors/anthropic_executor.rb +247 -0
  32. data/lib/llms/executors/base_executor.rb +144 -0
  33. data/lib/llms/executors/google_gemini_executor.rb +212 -0
  34. data/lib/llms/executors/hugging_face_executor.rb +17 -0
  35. data/lib/llms/executors/open_ai_compatible_executor.rb +209 -0
  36. data/lib/llms/executors.rb +52 -0
  37. data/lib/llms/models/model.rb +86 -0
  38. data/lib/llms/models/provider.rb +48 -0
  39. data/lib/llms/models.rb +187 -0
  40. data/lib/llms/parsers/anthropic_chat_response_stream_parser.rb +184 -0
  41. data/lib/llms/parsers/google_gemini_chat_response_stream_parser.rb +128 -0
  42. data/lib/llms/parsers/open_ai_compatible_chat_response_stream_parser.rb +170 -0
  43. data/lib/llms/parsers/partial_json_parser.rb +77 -0
  44. data/lib/llms/parsers/sse_chat_response_stream_parser.rb +72 -0
  45. data/lib/llms/public_models.json +607 -0
  46. data/lib/llms/stream/event_emitter.rb +48 -0
  47. data/lib/llms/stream/events.rb +104 -0
  48. data/lib/llms/usage/cost_calculator.rb +75 -0
  49. data/lib/llms/usage/usage_data.rb +46 -0
  50. data/lib/llms.rb +16 -0
  51. metadata +243 -0
@@ -0,0 +1,275 @@
1
+ require_relative 'base'
2
+
3
+ module LLMs
4
+ module CLI
5
+ class TestPromptCaching < Base
6
+ protected
7
+
8
+ def default_options
9
+ super.merge({
10
+ max_completion_tokens: 10,
11
+ confirmed: false,
12
+ system_prompt: "You are a book expert. What is the following excerpt from?",
13
+ cache_prompt: true ##TODO caching is automatic for most models now - remove this option
14
+ })
15
+ end
16
+
17
+ def add_custom_options(opts)
18
+ opts.on('-y', '--yes', 'Skip confirmation warning') { @options[:confirmed] = true }
19
+ end
20
+
21
+ def setup
22
+ # No model name required for this command
23
+ true
24
+ end
25
+
26
+ def perform_execution
27
+ unless @options[:confirmed]
28
+ puts "\e[33mWarning: Running this test can be expensive as it sends a long prompt to the model and makes multiple API calls."
29
+ puts "Use -y option to skip this warning and proceed.\e[0m"
30
+ exit 1
31
+ end
32
+
33
+ if @options[:model_name]
34
+ test_single_model(create_executor)
35
+ else
36
+ test_all_models
37
+ end
38
+ end
39
+
40
+ def test_all_models
41
+ models = LLMs::Models.list_model_names(full: true)
42
+ if ARGV[0]
43
+ models = models.select { |name| name.include?(ARGV[0]) }
44
+ end
45
+ models.each do |model_name|
46
+ test_single_model(create_executor({model_name: model_name}))
47
+ puts "-" * 80
48
+ end
49
+ end
50
+
51
+ def test_single_model(executor)
52
+ begin
53
+ puts "First call:"
54
+ response = executor.execute_prompt(prompt_data)
55
+ puts response
56
+ puts "Usage: #{executor.last_usage_data.inspect}"
57
+
58
+ puts "\nSecond call:"
59
+ response = executor.execute_prompt(prompt_data)
60
+ puts "Response: #{response}"
61
+ usage = executor.last_usage_data
62
+ puts "Usage: #{usage.inspect}"
63
+
64
+ if usage&.fetch(:cache_was_read, false)
65
+ puts "\e[32mSUCCESS: Prompt caching detected!\e[0m"
66
+ else
67
+ puts "\e[31mFAILURE: Prompt caching not detected!\e[0m"
68
+ end
69
+
70
+ rescue StandardError => e
71
+ puts "#{executor.model_name}: ERROR - #{e.message}"
72
+ puts e.backtrace if @options[:debug]
73
+ end
74
+ end
75
+
76
+ def prompt_data
77
+ PROMPT_DATA
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ PROMPT_DATA = <<-PROMPT
84
+ "That is very evident."
85
+ "Absurdly commonplace, is it not?"
86
+ "But the boots and the bath?"
87
+ "Equally childish. You are in the habit of doing up your boots in a certain way. I see them on this occasion fastened with an elaborate double bow, which is not your usual method of tying them. You have, therefore, had them off. Who has tied them? A bootmaker—or the boy at the bath. It is unlikely that it is the bootmaker, since your boots are nearly new. Well, what remains? The bath. Absurd, is it not? But, for all that, the Turkish bath has served a purpose."
88
+ "What is that?"
89
+ "You say that you have had it because you need a change. Let me suggest that you take one. How would Lausanne do, my dear Watson—first-class tickets and all expenses paid on a princely scale?"
90
+ "Splendid! But why?"
91
+ Holmes leaned back in his armchair and took his notebook from his pocket.
92
+ "One of the most dangerous classes in the world," said he, "is the drifting and friendless woman. She is the most harmless and often the most useful of mortals, but she is the inevitable inciter of crime in others. She is helpless. She is migratory. She has sufficient means to take her from country to country and from hotel to hotel. She is lost, as often as not, in a maze of obscure pensions and boardinghouses. She is a stray chicken in a world of foxes. When she is gobbled up she is hardly missed. I much fear that some evil has come to the Lady Frances Carfax."
93
+ I was relieved at this sudden descent from the general to the particular. Holmes consulted his notes.
94
+ "Lady Frances," he continued, "is the sole survivor of the direct family of the late Earl of Rufton. The estates went, as you may remember, in the male line. She was left with limited means, but with some very remarkable old Spanish jewellery of silver and curiously cut diamonds to which she was fondly attached—too attached, for she refused to leave them with her banker and always carried them about with her. A rather pathetic figure, the Lady Frances, a beautiful woman, still in fresh middle age, and yet, by a strange change, the last derelict of what only twenty years ago was a goodly fleet."
95
+ "What has happened to her, then?"
96
+ "Ah, what has happened to the Lady Frances? Is she alive or dead? There is our problem. She is a lady of precise habits, and for four years it has been her invariable custom to write every second week to Miss Dobney, her old governess, who has long retired and lives in Camberwell. It is this Miss Dobney who has consulted me. Nearly five weeks have passed without a word. The last letter was from the Hôtel National at Lausanne. Lady Frances seems to have left there and given no address. The family are anxious, and as they are exceedingly wealthy no sum will be spared if we can clear the matter up."
97
+ "Is Miss Dobney the only source of information? Surely she had other correspondents?"
98
+ "There is one correspondent who is a sure draw, Watson. That is the bank. Single ladies must live, and their passbooks are compressed diaries. She banks at Silvester's. I have glanced over her account. The last check but one paid her bill at Lausanne, but it was a large one and probably left her with cash in hand. Only one check has been drawn since."
99
+ "To whom, and where?"
100
+ "To Miss Marie Devine. There is nothing to show where the check was drawn. It was cashed at the Crédit Lyonnais at Montpellier less than three weeks ago. The sum was fifty pounds."
101
+ "And who is Miss Marie Devine?"
102
+ "That also I have been able to discover. Miss Marie Devine was the maid of Lady Frances Carfax. Why she should have paid her this check we have not yet determined. I have no doubt, however, that your researches will soon clear the matter up."
103
+ "My researches!"
104
+ "Hence the health-giving expedition to Lausanne. You know that I cannot possibly leave London while old Abrahams is in such mortal terror of his life. Besides, on general principles it is best that I should not leave the country. Scotland Yard feels lonely without me, and it causes an unhealthy excitement among the criminal classes. Go, then, my dear Watson, and if my humble counsel can ever be valued at so extravagant a rate as two pence a word, it waits your disposal night and day at the end of the Continental wire."
105
+ Two days later found me at the Hôtel National at Lausanne, where I received every courtesy at the hands of M. Moser, the well-known manager. Lady Frances, as he informed me, had stayed there for several weeks. She had been much liked by all who met her. Her age was not more than forty. She was still handsome and bore every sign of having in her youth been a very lovely woman. M. Moser knew nothing of any valuable jewellery, but it had been remarked by the servants that the heavy trunk in the lady's bedroom was always scrupulously locked. Marie Devine, the maid, was as popular as her mistress. She was actually engaged to one of the head waiters in the hotel, and there was no difficulty in getting her address. It was 11, Rue de Trajan, Montpellier. All this I jotted down and felt that Holmes himself could not have been more adroit in collecting his facts.
106
+ Only one corner still remained in the shadow. No light which I possessed could clear up the cause for the lady's sudden departure. She was very happy at Lausanne. There was every reason to believe that she intended to remain for the season in her luxurious rooms overlooking the lake. And yet she had left at a single day's notice, which involved her in the useless payment of a week's rent. Only Jules Vibart, the lover of the maid, had any suggestion to offer. He connected the sudden departure with the visit to the hotel a day or two before of a tall, dark, bearded man. "Un sauvage—un veritable sauvage!" cried Jules Vibart. The man had rooms somewhere in the town. He had been seen talking earnestly to Madame on the promenade by the lake. Then he had called. She had refused to see him. He was English, but of his name there was no record. Madame had left the place immediately afterwards. Jules Vibart, and, what was of more importance, Jules Vibart's sweetheart, thought that this call and the departure were cause and effect. Only one thing Jules would not discuss. That was the reason why Marie had left her mistress. Of that he could or would say nothing. If I wished to know, I must go to Montpellier and ask her.
107
+ So ended the first chapter of my inquiry. The second was devoted to the place which Lady Frances Carfax had sought when she left Lausanne. Concerning this there had been some secrecy, which confirmed the idea that she had gone with the intention of throwing someone off her track. Otherwise why should not her luggage have been openly labelled for Baden? Both she and it reached the Rhenish spa by some circuitous route. This much I gathered from the manager of Cook's local office. So to Baden I went, after dispatching to Holmes an account of all my proceedings and receiving in reply a telegram of half-humorous commendation.
108
+ At Baden the track was not difficult to follow. Lady Frances had stayed at the Englischer Hof for a fortnight. While there she had made the acquaintance of a Dr. Shlessinger and his wife, a missionary from South America. Like most lonely ladies, Lady Frances found her comfort and occupation in religion. Dr. Shlessinger's remarkable personality, his whole hearted devotion, and the fact that he was recovering from a disease contracted in the exercise of his apostolic duties affected her deeply. She had helped Mrs. Shlessinger in the nursing of the convalescent saint. He spent his day, as the manager described it to me, upon a lounge-chair on the veranda, with an attendant lady upon either side of him. He was preparing a map of the Holy Land, with special reference to the kingdom of the Midianites, upon which he was writing a monograph. Finally, having improved much in health, he and his wife had returned to London, and Lady Frances had started thither in their company. This was just three weeks before, and the manager had heard nothing since. As to the maid, Marie, she had gone off some days beforehand in floods of tears, after informing the other maids that she was leaving service forever. Dr. Shlessinger had paid the bill of the whole party before his departure.
109
+ "By the way," said the landlord in conclusion, "you are not the only friend of Lady Frances Carfax who is inquiring after her just now. Only a week or so ago we had a man here upon the same errand."
110
+ "Did he give a name?" I asked.
111
+ "None; but he was an Englishman, though of an unusual type."
112
+ "A savage?" said I, linking my facts after the fashion of my illustrious friend.
113
+ "Exactly. That describes him very well. He is a bulky, bearded, sunburned fellow, who looks as if he would be more at home in a farmers' inn than in a fashionable hotel. A hard, fierce man, I should think, and one whom I should be sorry to offend."
114
+ Already the mystery began to define itself, as figures grow clearer with the lifting of a fog. Here was this good and pious lady pursued from place to place by a sinister and unrelenting figure. She feared him, or she would not have fled from Lausanne. He had still followed. Sooner or later he would overtake her. Had he already overtaken her? Was that the secret of her continued silence? Could the good people who were her companions not screen her from his violence or his blackmail? What horrible purpose, what deep design, lay behind this long pursuit? There was the problem which I had to solve.
115
+ To Holmes I wrote showing how rapidly and surely I had got down to the roots of the matter. In reply I had a telegram asking for a description of Dr. Shlessinger's left ear. Holmes's ideas of humour are strange and occasionally offensive, so I took no notice of his ill-timed jest—indeed, I had already reached Montpellier in my pursuit of the maid, Marie, before his message came.
116
+ I had no difficulty in finding the ex-servant and in learning all that she could tell me. She was a devoted creature, who had only left her mistress because she was sure that she was in good hands, and because her own approaching marriage made a separation inevitable in any case. Her mistress had, as she confessed with distress, shown some irritability of temper towards her during their stay in Baden, and had even questioned her once as if she had suspicions of her honesty, and this had made the parting easier than it would otherwise have been. Lady Frances had given her fifty pounds as a wedding-present. Like me, Marie viewed with deep distrust the stranger who had driven her mistress from Lausanne. With her own eyes she had seen him seize the lady's wrist with great violence on the public promenade by the lake. He was a fierce and terrible man. She believed that it was out of dread of him that Lady Frances had accepted the escort of the Shlessingers to London. She had never spoken to Marie about it, but many little signs had convinced the maid that her mistress lived in a state of continual nervous apprehension. So far she had got in her narrative, when suddenly she sprang from her chair and her face was convulsed with surprise and fear. "See!" she cried. "The miscreant follows still! There is the very man of whom I speak."
117
+ Through the open sitting-room window I saw a huge, swarthy man with a bristling black beard walking slowly down the centre of the street and staring eagerly at the numbers of the houses. It was clear that, like myself, he was on the track of the maid. Acting upon the impulse of the moment, I rushed out and accosted him.
118
+ "You are an Englishman," I said.
119
+ "What if I am?" he asked with a most villainous scowl.
120
+ "May I ask what your name is?"
121
+ "No, you may not," said he with decision.
122
+ The situation was awkward, but the most direct way is often the best.
123
+ "Where is the Lady Frances Carfax?" I asked.
124
+ He stared at me with amazement.
125
+ "What have you done with her? Why have you pursued her? I insist upon an answer!" said I.
126
+ The fellow gave a bellow of anger and sprang upon me like a tiger. I have held my own in many a struggle, but the man had a grip of iron and the fury of a fiend. His hand was on my throat and my senses were nearly gone before an unshaven French ouvrier in a blue blouse darted out from a cabaret opposite, with a cudgel in his hand, and struck my assailant a sharp crack over the forearm, which made him leave go his hold. He stood for an instant fuming with rage and uncertain whether he should not renew his attack. Then, with a snarl of anger, he left me and entered the cottage from which I had just come. I turned to thank my preserver, who stood beside me in the roadway.
127
+ "Well, Watson," said he, "a very pretty hash you have made of it! I rather think you had better come back with me to London by the night express."
128
+ An hour afterwards, Sherlock Holmes, in his usual garb and style, was seated in my private room at the hotel. His explanation of his sudden and opportune appearance was simplicity itself, for, finding that he could get away from London, he determined to head me off at the next obvious point of my travels. In the disguise of a workingman he had sat in the cabaret waiting for my appearance.
129
+ "And a singularly consistent investigation you have made, my dear Watson," said he. "I cannot at the moment recall any possible blunder which you have omitted. The total effect of your proceeding has been to give the alarm everywhere and yet to discover nothing."
130
+ "Perhaps you would have done no better," I answered bitterly.
131
+
132
+
133
+ "That is very evident."
134
+ "Absurdly commonplace, is it not?"
135
+ "But the boots and the bath?"
136
+ "Equally childish. You are in the habit of doing up your boots in a certain way. I see them on this occasion fastened with an elaborate double bow, which is not your usual method of tying them. You have, therefore, had them off. Who has tied them? A bootmaker—or the boy at the bath. It is unlikely that it is the bootmaker, since your boots are nearly new. Well, what remains? The bath. Absurd, is it not? But, for all that, the Turkish bath has served a purpose."
137
+ "What is that?"
138
+ "You say that you have had it because you need a change. Let me suggest that you take one. How would Lausanne do, my dear Watson—first-class tickets and all expenses paid on a princely scale?"
139
+ "Splendid! But why?"
140
+ Holmes leaned back in his armchair and took his notebook from his pocket.
141
+ "One of the most dangerous classes in the world," said he, "is the drifting and friendless woman. She is the most harmless and often the most useful of mortals, but she is the inevitable inciter of crime in others. She is helpless. She is migratory. She has sufficient means to take her from country to country and from hotel to hotel. She is lost, as often as not, in a maze of obscure pensions and boardinghouses. She is a stray chicken in a world of foxes. When she is gobbled up she is hardly missed. I much fear that some evil has come to the Lady Frances Carfax."
142
+ I was relieved at this sudden descent from the general to the particular. Holmes consulted his notes.
143
+ "Lady Frances," he continued, "is the sole survivor of the direct family of the late Earl of Rufton. The estates went, as you may remember, in the male line. She was left with limited means, but with some very remarkable old Spanish jewellery of silver and curiously cut diamonds to which she was fondly attached—too attached, for she refused to leave them with her banker and always carried them about with her. A rather pathetic figure, the Lady Frances, a beautiful woman, still in fresh middle age, and yet, by a strange change, the last derelict of what only twenty years ago was a goodly fleet."
144
+ "What has happened to her, then?"
145
+ "Ah, what has happened to the Lady Frances? Is she alive or dead? There is our problem. She is a lady of precise habits, and for four years it has been her invariable custom to write every second week to Miss Dobney, her old governess, who has long retired and lives in Camberwell. It is this Miss Dobney who has consulted me. Nearly five weeks have passed without a word. The last letter was from the Hôtel National at Lausanne. Lady Frances seems to have left there and given no address. The family are anxious, and as they are exceedingly wealthy no sum will be spared if we can clear the matter up."
146
+ "Is Miss Dobney the only source of information? Surely she had other correspondents?"
147
+ "There is one correspondent who is a sure draw, Watson. That is the bank. Single ladies must live, and their passbooks are compressed diaries. She banks at Silvester's. I have glanced over her account. The last check but one paid her bill at Lausanne, but it was a large one and probably left her with cash in hand. Only one check has been drawn since."
148
+ "To whom, and where?"
149
+ "To Miss Marie Devine. There is nothing to show where the check was drawn. It was cashed at the Crédit Lyonnais at Montpellier less than three weeks ago. The sum was fifty pounds."
150
+ "And who is Miss Marie Devine?"
151
+ "That also I have been able to discover. Miss Marie Devine was the maid of Lady Frances Carfax. Why she should have paid her this check we have not yet determined. I have no doubt, however, that your researches will soon clear the matter up."
152
+ "My researches!"
153
+ "Hence the health-giving expedition to Lausanne. You know that I cannot possibly leave London while old Abrahams is in such mortal terror of his life. Besides, on general principles it is best that I should not leave the country. Scotland Yard feels lonely without me, and it causes an unhealthy excitement among the criminal classes. Go, then, my dear Watson, and if my humble counsel can ever be valued at so extravagant a rate as two pence a word, it waits your disposal night and day at the end of the Continental wire."
154
+ Two days later found me at the Hôtel National at Lausanne, where I received every courtesy at the hands of M. Moser, the well-known manager. Lady Frances, as he informed me, had stayed there for several weeks. She had been much liked by all who met her. Her age was not more than forty. She was still handsome and bore every sign of having in her youth been a very lovely woman. M. Moser knew nothing of any valuable jewellery, but it had been remarked by the servants that the heavy trunk in the lady's bedroom was always scrupulously locked. Marie Devine, the maid, was as popular as her mistress. She was actually engaged to one of the head waiters in the hotel, and there was no difficulty in getting her address. It was 11, Rue de Trajan, Montpellier. All this I jotted down and felt that Holmes himself could not have been more adroit in collecting his facts.
155
+ Only one corner still remained in the shadow. No light which I possessed could clear up the cause for the lady's sudden departure. She was very happy at Lausanne. There was every reason to believe that she intended to remain for the season in her luxurious rooms overlooking the lake. And yet she had left at a single day's notice, which involved her in the useless payment of a week's rent. Only Jules Vibart, the lover of the maid, had any suggestion to offer. He connected the sudden departure with the visit to the hotel a day or two before of a tall, dark, bearded man. "Un sauvage—un veritable sauvage!" cried Jules Vibart. The man had rooms somewhere in the town. He had been seen talking earnestly to Madame on the promenade by the lake. Then he had called. She had refused to see him. He was English, but of his name there was no record. Madame had left the place immediately afterwards. Jules Vibart, and, what was of more importance, Jules Vibart's sweetheart, thought that this call and the departure were cause and effect. Only one thing Jules would not discuss. That was the reason why Marie had left her mistress. Of that he could or would say nothing. If I wished to know, I must go to Montpellier and ask her.
156
+ So ended the first chapter of my inquiry. The second was devoted to the place which Lady Frances Carfax had sought when she left Lausanne. Concerning this there had been some secrecy, which confirmed the idea that she had gone with the intention of throwing someone off her track. Otherwise why should not her luggage have been openly labelled for Baden? Both she and it reached the Rhenish spa by some circuitous route. This much I gathered from the manager of Cook's local office. So to Baden I went, after dispatching to Holmes an account of all my proceedings and receiving in reply a telegram of half-humorous commendation.
157
+ At Baden the track was not difficult to follow. Lady Frances had stayed at the Englischer Hof for a fortnight. While there she had made the acquaintance of a Dr. Shlessinger and his wife, a missionary from South America. Like most lonely ladies, Lady Frances found her comfort and occupation in religion. Dr. Shlessinger's remarkable personality, his whole hearted devotion, and the fact that he was recovering from a disease contracted in the exercise of his apostolic duties affected her deeply. She had helped Mrs. Shlessinger in the nursing of the convalescent saint. He spent his day, as the manager described it to me, upon a lounge-chair on the veranda, with an attendant lady upon either side of him. He was preparing a map of the Holy Land, with special reference to the kingdom of the Midianites, upon which he was writing a monograph. Finally, having improved much in health, he and his wife had returned to London, and Lady Frances had started thither in their company. This was just three weeks before, and the manager had heard nothing since. As to the maid, Marie, she had gone off some days beforehand in floods of tears, after informing the other maids that she was leaving service forever. Dr. Shlessinger had paid the bill of the whole party before his departure.
158
+ "By the way," said the landlord in conclusion, "you are not the only friend of Lady Frances Carfax who is inquiring after her just now. Only a week or so ago we had a man here upon the same errand."
159
+ "Did he give a name?" I asked.
160
+ "None; but he was an Englishman, though of an unusual type."
161
+ "A savage?" said I, linking my facts after the fashion of my illustrious friend.
162
+ "Exactly. That describes him very well. He is a bulky, bearded, sunburned fellow, who looks as if he would be more at home in a farmers' inn than in a fashionable hotel. A hard, fierce man, I should think, and one whom I should be sorry to offend."
163
+ Already the mystery began to define itself, as figures grow clearer with the lifting of a fog. Here was this good and pious lady pursued from place to place by a sinister and unrelenting figure. She feared him, or she would not have fled from Lausanne. He had still followed. Sooner or later he would overtake her. Had he already overtaken her? Was that the secret of her continued silence? Could the good people who were her companions not screen her from his violence or his blackmail? What horrible purpose, what deep design, lay behind this long pursuit? There was the problem which I had to solve.
164
+ To Holmes I wrote showing how rapidly and surely I had got down to the roots of the matter. In reply I had a telegram asking for a description of Dr. Shlessinger's left ear. Holmes's ideas of humour are strange and occasionally offensive, so I took no notice of his ill-timed jest—indeed, I had already reached Montpellier in my pursuit of the maid, Marie, before his message came.
165
+ I had no difficulty in finding the ex-servant and in learning all that she could tell me. She was a devoted creature, who had only left her mistress because she was sure that she was in good hands, and because her own approaching marriage made a separation inevitable in any case. Her mistress had, as she confessed with distress, shown some irritability of temper towards her during their stay in Baden, and had even questioned her once as if she had suspicions of her honesty, and this had made the parting easier than it would otherwise have been. Lady Frances had given her fifty pounds as a wedding-present. Like me, Marie viewed with deep distrust the stranger who had driven her mistress from Lausanne. With her own eyes she had seen him seize the lady's wrist with great violence on the public promenade by the lake. He was a fierce and terrible man. She believed that it was out of dread of him that Lady Frances had accepted the escort of the Shlessingers to London. She had never spoken to Marie about it, but many little signs had convinced the maid that her mistress lived in a state of continual nervous apprehension. So far she had got in her narrative, when suddenly she sprang from her chair and her face was convulsed with surprise and fear. "See!" she cried. "The miscreant follows still! There is the very man of whom I speak."
166
+ Through the open sitting-room window I saw a huge, swarthy man with a bristling black beard walking slowly down the centre of the street and staring eagerly at the numbers of the houses. It was clear that, like myself, he was on the track of the maid. Acting upon the impulse of the moment, I rushed out and accosted him.
167
+ "You are an Englishman," I said.
168
+ "What if I am?" he asked with a most villainous scowl.
169
+ "May I ask what your name is?"
170
+ "No, you may not," said he with decision.
171
+ The situation was awkward, but the most direct way is often the best.
172
+ "Where is the Lady Frances Carfax?" I asked.
173
+ He stared at me with amazement.
174
+ "What have you done with her? Why have you pursued her? I insist upon an answer!" said I.
175
+ The fellow gave a bellow of anger and sprang upon me like a tiger. I have held my own in many a struggle, but the man had a grip of iron and the fury of a fiend. His hand was on my throat and my senses were nearly gone before an unshaven French ouvrier in a blue blouse darted out from a cabaret opposite, with a cudgel in his hand, and struck my assailant a sharp crack over the forearm, which made him leave go his hold. He stood for an instant fuming with rage and uncertain whether he should not renew his attack. Then, with a snarl of anger, he left me and entered the cottage from which I had just come. I turned to thank my preserver, who stood beside me in the roadway.
176
+ "Well, Watson," said he, "a very pretty hash you have made of it! I rather think you had better come back with me to London by the night express."
177
+ An hour afterwards, Sherlock Holmes, in his usual garb and style, was seated in my private room at the hotel. His explanation of his sudden and opportune appearance was simplicity itself, for, finding that he could get away from London, he determined to head me off at the next obvious point of my travels. In the disguise of a workingman he had sat in the cabaret waiting for my appearance.
178
+ "And a singularly consistent investigation you have made, my dear Watson," said he. "I cannot at the moment recall any possible blunder which you have omitted. The total effect of your proceeding has been to give the alarm everywhere and yet to discover nothing."
179
+ "Perhaps you would have done no better," I answered bitterly.
180
+ "That is very evident."
181
+ "Absurdly commonplace, is it not?"
182
+ "But the boots and the bath?"
183
+ "Equally childish. You are in the habit of doing up your boots in a certain way. I see them on this occasion fastened with an elaborate double bow, which is not your usual method of tying them. You have, therefore, had them off. Who has tied them? A bootmaker—or the boy at the bath. It is unlikely that it is the bootmaker, since your boots are nearly new. Well, what remains? The bath. Absurd, is it not? But, for all that, the Turkish bath has served a purpose."
184
+ "What is that?"
185
+ "You say that you have had it because you need a change. Let me suggest that you take one. How would Lausanne do, my dear Watson—first-class tickets and all expenses paid on a princely scale?"
186
+ "Splendid! But why?"
187
+ Holmes leaned back in his armchair and took his notebook from his pocket.
188
+ "One of the most dangerous classes in the world," said he, "is the drifting and friendless woman. She is the most harmless and often the most useful of mortals, but she is the inevitable inciter of crime in others. She is helpless. She is migratory. She has sufficient means to take her from country to country and from hotel to hotel. She is lost, as often as not, in a maze of obscure pensions and boardinghouses. She is a stray chicken in a world of foxes. When she is gobbled up she is hardly missed. I much fear that some evil has come to the Lady Frances Carfax."
189
+ I was relieved at this sudden descent from the general to the particular. Holmes consulted his notes.
190
+ "Lady Frances," he continued, "is the sole survivor of the direct family of the late Earl of Rufton. The estates went, as you may remember, in the male line. She was left with limited means, but with some very remarkable old Spanish jewellery of silver and curiously cut diamonds to which she was fondly attached—too attached, for she refused to leave them with her banker and always carried them about with her. A rather pathetic figure, the Lady Frances, a beautiful woman, still in fresh middle age, and yet, by a strange change, the last derelict of what only twenty years ago was a goodly fleet."
191
+ "What has happened to her, then?"
192
+ "Ah, what has happened to the Lady Frances? Is she alive or dead? There is our problem. She is a lady of precise habits, and for four years it has been her invariable custom to write every second week to Miss Dobney, her old governess, who has long retired and lives in Camberwell. It is this Miss Dobney who has consulted me. Nearly five weeks have passed without a word. The last letter was from the Hôtel National at Lausanne. Lady Frances seems to have left there and given no address. The family are anxious, and as they are exceedingly wealthy no sum will be spared if we can clear the matter up."
193
+ "Is Miss Dobney the only source of information? Surely she had other correspondents?"
194
+ "There is one correspondent who is a sure draw, Watson. That is the bank. Single ladies must live, and their passbooks are compressed diaries. She banks at Silvester's. I have glanced over her account. The last check but one paid her bill at Lausanne, but it was a large one and probably left her with cash in hand. Only one check has been drawn since."
195
+ "To whom, and where?"
196
+ "To Miss Marie Devine. There is nothing to show where the check was drawn. It was cashed at the Crédit Lyonnais at Montpellier less than three weeks ago. The sum was fifty pounds."
197
+ "And who is Miss Marie Devine?"
198
+ "That also I have been able to discover. Miss Marie Devine was the maid of Lady Frances Carfax. Why she should have paid her this check we have not yet determined. I have no doubt, however, that your researches will soon clear the matter up."
199
+ "My researches!"
200
+ "Hence the health-giving expedition to Lausanne. You know that I cannot possibly leave London while old Abrahams is in such mortal terror of his life. Besides, on general principles it is best that I should not leave the country. Scotland Yard feels lonely without me, and it causes an unhealthy excitement among the criminal classes. Go, then, my dear Watson, and if my humble counsel can ever be valued at so extravagant a rate as two pence a word, it waits your disposal night and day at the end of the Continental wire."
201
+ Two days later found me at the Hôtel National at Lausanne, where I received every courtesy at the hands of M. Moser, the well-known manager. Lady Frances, as he informed me, had stayed there for several weeks. She had been much liked by all who met her. Her age was not more than forty. She was still handsome and bore every sign of having in her youth been a very lovely woman. M. Moser knew nothing of any valuable jewellery, but it had been remarked by the servants that the heavy trunk in the lady's bedroom was always scrupulously locked. Marie Devine, the maid, was as popular as her mistress. She was actually engaged to one of the head waiters in the hotel, and there was no difficulty in getting her address. It was 11, Rue de Trajan, Montpellier. All this I jotted down and felt that Holmes himself could not have been more adroit in collecting his facts.
202
+ Only one corner still remained in the shadow. No light which I possessed could clear up the cause for the lady's sudden departure. She was very happy at Lausanne. There was every reason to believe that she intended to remain for the season in her luxurious rooms overlooking the lake. And yet she had left at a single day's notice, which involved her in the useless payment of a week's rent. Only Jules Vibart, the lover of the maid, had any suggestion to offer. He connected the sudden departure with the visit to the hotel a day or two before of a tall, dark, bearded man. "Un sauvage—un veritable sauvage!" cried Jules Vibart. The man had rooms somewhere in the town. He had been seen talking earnestly to Madame on the promenade by the lake. Then he had called. She had refused to see him. He was English, but of his name there was no record. Madame had left the place immediately afterwards. Jules Vibart, and, what was of more importance, Jules Vibart's sweetheart, thought that this call and the departure were cause and effect. Only one thing Jules would not discuss. That was the reason why Marie had left her mistress. Of that he could or would say nothing. If I wished to know, I must go to Montpellier and ask her.
203
+ So ended the first chapter of my inquiry. The second was devoted to the place which Lady Frances Carfax had sought when she left Lausanne. Concerning this there had been some secrecy, which confirmed the idea that she had gone with the intention of throwing someone off her track. Otherwise why should not her luggage have been openly labelled for Baden? Both she and it reached the Rhenish spa by some circuitous route. This much I gathered from the manager of Cook's local office. So to Baden I went, after dispatching to Holmes an account of all my proceedings and receiving in reply a telegram of half-humorous commendation.
204
+ At Baden the track was not difficult to follow. Lady Frances had stayed at the Englischer Hof for a fortnight. While there she had made the acquaintance of a Dr. Shlessinger and his wife, a missionary from South America. Like most lonely ladies, Lady Frances found her comfort and occupation in religion. Dr. Shlessinger's remarkable personality, his whole hearted devotion, and the fact that he was recovering from a disease contracted in the exercise of his apostolic duties affected her deeply. She had helped Mrs. Shlessinger in the nursing of the convalescent saint. He spent his day, as the manager described it to me, upon a lounge-chair on the veranda, with an attendant lady upon either side of him. He was preparing a map of the Holy Land, with special reference to the kingdom of the Midianites, upon which he was writing a monograph. Finally, having improved much in health, he and his wife had returned to London, and Lady Frances had started thither in their company. This was just three weeks before, and the manager had heard nothing since. As to the maid, Marie, she had gone off some days beforehand in floods of tears, after informing the other maids that she was leaving service forever. Dr. Shlessinger had paid the bill of the whole party before his departure.
205
+ "By the way," said the landlord in conclusion, "you are not the only friend of Lady Frances Carfax who is inquiring after her just now. Only a week or so ago we had a man here upon the same errand."
206
+ "Did he give a name?" I asked.
207
+ "None; but he was an Englishman, though of an unusual type."
208
+ "A savage?" said I, linking my facts after the fashion of my illustrious friend.
209
+ "Exactly. That describes him very well. He is a bulky, bearded, sunburned fellow, who looks as if he would be more at home in a farmers' inn than in a fashionable hotel. A hard, fierce man, I should think, and one whom I should be sorry to offend."
210
+ Already the mystery began to define itself, as figures grow clearer with the lifting of a fog. Here was this good and pious lady pursued from place to place by a sinister and unrelenting figure. She feared him, or she would not have fled from Lausanne. He had still followed. Sooner or later he would overtake her. Had he already overtaken her? Was that the secret of her continued silence? Could the good people who were her companions not screen her from his violence or his blackmail? What horrible purpose, what deep design, lay behind this long pursuit? There was the problem which I had to solve.
211
+ To Holmes I wrote showing how rapidly and surely I had got down to the roots of the matter. In reply I had a telegram asking for a description of Dr. Shlessinger's left ear. Holmes's ideas of humour are strange and occasionally offensive, so I took no notice of his ill-timed jest—indeed, I had already reached Montpellier in my pursuit of the maid, Marie, before his message came.
212
+ I had no difficulty in finding the ex-servant and in learning all that she could tell me. She was a devoted creature, who had only left her mistress because she was sure that she was in good hands, and because her own approaching marriage made a separation inevitable in any case. Her mistress had, as she confessed with distress, shown some irritability of temper towards her during their stay in Baden, and had even questioned her once as if she had suspicions of her honesty, and this had made the parting easier than it would otherwise have been. Lady Frances had given her fifty pounds as a wedding-present. Like me, Marie viewed with deep distrust the stranger who had driven her mistress from Lausanne. With her own eyes she had seen him seize the lady's wrist with great violence on the public promenade by the lake. He was a fierce and terrible man. She believed that it was out of dread of him that Lady Frances had accepted the escort of the Shlessingers to London. She had never spoken to Marie about it, but many little signs had convinced the maid that her mistress lived in a state of continual nervous apprehension. So far she had got in her narrative, when suddenly she sprang from her chair and her face was convulsed with surprise and fear. "See!" she cried. "The miscreant follows still! There is the very man of whom I speak."
213
+ Through the open sitting-room window I saw a huge, swarthy man with a bristling black beard walking slowly down the centre of the street and staring eagerly at the numbers of the houses. It was clear that, like myself, he was on the track of the maid. Acting upon the impulse of the moment, I rushed out and accosted him.
214
+ "You are an Englishman," I said.
215
+ "What if I am?" he asked with a most villainous scowl.
216
+ "May I ask what your name is?"
217
+ "No, you may not," said he with decision.
218
+ The situation was awkward, but the most direct way is often the best.
219
+ "Where is the Lady Frances Carfax?" I asked.
220
+ He stared at me with amazement.
221
+ "What have you done with her? Why have you pursued her? I insist upon an answer!" said I.
222
+ The fellow gave a bellow of anger and sprang upon me like a tiger. I have held my own in many a struggle, but the man had a grip of iron and the fury of a fiend. His hand was on my throat and my senses were nearly gone before an unshaven French ouvrier in a blue blouse darted out from a cabaret opposite, with a cudgel in his hand, and struck my assailant a sharp crack over the forearm, which made him leave go his hold. He stood for an instant fuming with rage and uncertain whether he should not renew his attack. Then, with a snarl of anger, he left me and entered the cottage from which I had just come. I turned to thank my preserver, who stood beside me in the roadway.
223
+ "Well, Watson," said he, "a very pretty hash you have made of it! I rather think you had better come back with me to London by the night express."
224
+ An hour afterwards, Sherlock Holmes, in his usual garb and style, was seated in my private room at the hotel. His explanation of his sudden and opportune appearance was simplicity itself, for, finding that he could get away from London, he determined to head me off at the next obvious point of my travels. In the disguise of a workingman he had sat in the cabaret waiting for my appearance.
225
+ "And a singularly consistent investigation you have made, my dear Watson," said he. "I cannot at the moment recall any possible blunder which you have omitted. The total effect of your proceeding has been to give the alarm everywhere and yet to discover nothing."
226
+ "Perhaps you would have done no better," I answered bitterly.
227
+ "That is very evident."
228
+ "Absurdly commonplace, is it not?"
229
+ "But the boots and the bath?"
230
+ "Equally childish. You are in the habit of doing up your boots in a certain way. I see them on this occasion fastened with an elaborate double bow, which is not your usual method of tying them. You have, therefore, had them off. Who has tied them? A bootmaker—or the boy at the bath. It is unlikely that it is the bootmaker, since your boots are nearly new. Well, what remains? The bath. Absurd, is it not? But, for all that, the Turkish bath has served a purpose."
231
+ "What is that?"
232
+ "You say that you have had it because you need a change. Let me suggest that you take one. How would Lausanne do, my dear Watson—first-class tickets and all expenses paid on a princely scale?"
233
+ "Splendid! But why?"
234
+ Holmes leaned back in his armchair and took his notebook from his pocket.
235
+ "One of the most dangerous classes in the world," said he, "is the drifting and friendless woman. She is the most harmless and often the most useful of mortals, but she is the inevitable inciter of crime in others. She is helpless. She is migratory. She has sufficient means to take her from country to country and from hotel to hotel. She is lost, as often as not, in a maze of obscure pensions and boardinghouses. She is a stray chicken in a world of foxes. When she is gobbled up she is hardly missed. I much fear that some evil has come to the Lady Frances Carfax."
236
+ I was relieved at this sudden descent from the general to the particular. Holmes consulted his notes.
237
+ "Lady Frances," he continued, "is the sole survivor of the direct family of the late Earl of Rufton. The estates went, as you may remember, in the male line. She was left with limited means, but with some very remarkable old Spanish jewellery of silver and curiously cut diamonds to which she was fondly attached—too attached, for she refused to leave them with her banker and always carried them about with her. A rather pathetic figure, the Lady Frances, a beautiful woman, still in fresh middle age, and yet, by a strange change, the last derelict of what only twenty years ago was a goodly fleet."
238
+ "What has happened to her, then?"
239
+ "Ah, what has happened to the Lady Frances? Is she alive or dead? There is our problem. She is a lady of precise habits, and for four years it has been her invariable custom to write every second week to Miss Dobney, her old governess, who has long retired and lives in Camberwell. It is this Miss Dobney who has consulted me. Nearly five weeks have passed without a word. The last letter was from the Hôtel National at Lausanne. Lady Frances seems to have left there and given no address. The family are anxious, and as they are exceedingly wealthy no sum will be spared if we can clear the matter up."
240
+ "Is Miss Dobney the only source of information? Surely she had other correspondents?"
241
+ "There is one correspondent who is a sure draw, Watson. That is the bank. Single ladies must live, and their passbooks are compressed diaries. She banks at Silvester's. I have glanced over her account. The last check but one paid her bill at Lausanne, but it was a large one and probably left her with cash in hand. Only one check has been drawn since."
242
+ "To whom, and where?"
243
+ "To Miss Marie Devine. There is nothing to show where the check was drawn. It was cashed at the Crédit Lyonnais at Montpellier less than three weeks ago. The sum was fifty pounds."
244
+ "And who is Miss Marie Devine?"
245
+ "That also I have been able to discover. Miss Marie Devine was the maid of Lady Frances Carfax. Why she should have paid her this check we have not yet determined. I have no doubt, however, that your researches will soon clear the matter up."
246
+ "My researches!"
247
+ "Hence the health-giving expedition to Lausanne. You know that I cannot possibly leave London while old Abrahams is in such mortal terror of his life. Besides, on general principles it is best that I should not leave the country. Scotland Yard feels lonely without me, and it causes an unhealthy excitement among the criminal classes. Go, then, my dear Watson, and if my humble counsel can ever be valued at so extravagant a rate as two pence a word, it waits your disposal night and day at the end of the Continental wire."
248
+ Two days later found me at the Hôtel National at Lausanne, where I received every courtesy at the hands of M. Moser, the well-known manager. Lady Frances, as he informed me, had stayed there for several weeks. She had been much liked by all who met her. Her age was not more than forty. She was still handsome and bore every sign of having in her youth been a very lovely woman. M. Moser knew nothing of any valuable jewellery, but it had been remarked by the servants that the heavy trunk in the lady's bedroom was always scrupulously locked. Marie Devine, the maid, was as popular as her mistress. She was actually engaged to one of the head waiters in the hotel, and there was no difficulty in getting her address. It was 11, Rue de Trajan, Montpellier. All this I jotted down and felt that Holmes himself could not have been more adroit in collecting his facts.
249
+ Only one corner still remained in the shadow. No light which I possessed could clear up the cause for the lady's sudden departure. She was very happy at Lausanne. There was every reason to believe that she intended to remain for the season in her luxurious rooms overlooking the lake. And yet she had left at a single day's notice, which involved her in the useless payment of a week's rent. Only Jules Vibart, the lover of the maid, had any suggestion to offer. He connected the sudden departure with the visit to the hotel a day or two before of a tall, dark, bearded man. "Un sauvage—un veritable sauvage!" cried Jules Vibart. The man had rooms somewhere in the town. He had been seen talking earnestly to Madame on the promenade by the lake. Then he had called. She had refused to see him. He was English, but of his name there was no record. Madame had left the place immediately afterwards. Jules Vibart, and, what was of more importance, Jules Vibart's sweetheart, thought that this call and the departure were cause and effect. Only one thing Jules would not discuss. That was the reason why Marie had left her mistress. Of that he could or would say nothing. If I wished to know, I must go to Montpellier and ask her.
250
+ So ended the first chapter of my inquiry. The second was devoted to the place which Lady Frances Carfax had sought when she left Lausanne. Concerning this there had been some secrecy, which confirmed the idea that she had gone with the intention of throwing someone off her track. Otherwise why should not her luggage have been openly labelled for Baden? Both she and it reached the Rhenish spa by some circuitous route. This much I gathered from the manager of Cook's local office. So to Baden I went, after dispatching to Holmes an account of all my proceedings and receiving in reply a telegram of half-humorous commendation.
251
+ At Baden the track was not difficult to follow. Lady Frances had stayed at the Englischer Hof for a fortnight. While there she had made the acquaintance of a Dr. Shlessinger and his wife, a missionary from South America. Like most lonely ladies, Lady Frances found her comfort and occupation in religion. Dr. Shlessinger's remarkable personality, his whole hearted devotion, and the fact that he was recovering from a disease contracted in the exercise of his apostolic duties affected her deeply. She had helped Mrs. Shlessinger in the nursing of the convalescent saint. He spent his day, as the manager described it to me, upon a lounge-chair on the veranda, with an attendant lady upon either side of him. He was preparing a map of the Holy Land, with special reference to the kingdom of the Midianites, upon which he was writing a monograph. Finally, having improved much in health, he and his wife had returned to London, and Lady Frances had started thither in their company. This was just three weeks before, and the manager had heard nothing since. As to the maid, Marie, she had gone off some days beforehand in floods of tears, after informing the other maids that she was leaving service forever. Dr. Shlessinger had paid the bill of the whole party before his departure.
252
+ "By the way," said the landlord in conclusion, "you are not the only friend of Lady Frances Carfax who is inquiring after her just now. Only a week or so ago we had a man here upon the same errand."
253
+ "Did he give a name?" I asked.
254
+ "None; but he was an Englishman, though of an unusual type."
255
+ "A savage?" said I, linking my facts after the fashion of my illustrious friend.
256
+ "Exactly. That describes him very well. He is a bulky, bearded, sunburned fellow, who looks as if he would be more at home in a farmers' inn than in a fashionable hotel. A hard, fierce man, I should think, and one whom I should be sorry to offend."
257
+ Already the mystery began to define itself, as figures grow clearer with the lifting of a fog. Here was this good and pious lady pursued from place to place by a sinister and unrelenting figure. She feared him, or she would not have fled from Lausanne. He had still followed. Sooner or later he would overtake her. Had he already overtaken her? Was that the secret of her continued silence? Could the good people who were her companions not screen her from his violence or his blackmail? What horrible purpose, what deep design, lay behind this long pursuit? There was the problem which I had to solve.
258
+ To Holmes I wrote showing how rapidly and surely I had got down to the roots of the matter. In reply I had a telegram asking for a description of Dr. Shlessinger's left ear. Holmes's ideas of humour are strange and occasionally offensive, so I took no notice of his ill-timed jest—indeed, I had already reached Montpellier in my pursuit of the maid, Marie, before his message came.
259
+ I had no difficulty in finding the ex-servant and in learning all that she could tell me. She was a devoted creature, who had only left her mistress because she was sure that she was in good hands, and because her own approaching marriage made a separation inevitable in any case. Her mistress had, as she confessed with distress, shown some irritability of temper towards her during their stay in Baden, and had even questioned her once as if she had suspicions of her honesty, and this had made the parting easier than it would otherwise have been. Lady Frances had given her fifty pounds as a wedding-present. Like me, Marie viewed with deep distrust the stranger who had driven her mistress from Lausanne. With her own eyes she had seen him seize the lady's wrist with great violence on the public promenade by the lake. He was a fierce and terrible man. She believed that it was out of dread of him that Lady Frances had accepted the escort of the Shlessingers to London. She had never spoken to Marie about it, but many little signs had convinced the maid that her mistress lived in a state of continual nervous apprehension. So far she had got in her narrative, when suddenly she sprang from her chair and her face was convulsed with surprise and fear. "See!" she cried. "The miscreant follows still! There is the very man of whom I speak."
260
+ Through the open sitting-room window I saw a huge, swarthy man with a bristling black beard walking slowly down the centre of the street and staring eagerly at the numbers of the houses. It was clear that, like myself, he was on the track of the maid. Acting upon the impulse of the moment, I rushed out and accosted him.
261
+ "You are an Englishman," I said.
262
+ "What if I am?" he asked with a most villainous scowl.
263
+ "May I ask what your name is?"
264
+ "No, you may not," said he with decision.
265
+ The situation was awkward, but the most direct way is often the best.
266
+ "Where is the Lady Frances Carfax?" I asked.
267
+ He stared at me with amazement.
268
+ "What have you done with her? Why have you pursued her? I insist upon an answer!" said I.
269
+ The fellow gave a bellow of anger and sprang upon me like a tiger. I have held my own in many a struggle, but the man had a grip of iron and the fury of a fiend. His hand was on my throat and my senses were nearly gone before an unshaven French ouvrier in a blue blouse darted out from a cabaret opposite, with a cudgel in his hand, and struck my assailant a sharp crack over the forearm, which made him leave go his hold. He stood for an instant fuming with rage and uncertain whether he should not renew his attack. Then, with a snarl of anger, he left me and entered the cottage from which I had just come. I turned to thank my preserver, who stood beside me in the roadway.
270
+ "Well, Watson," said he, "a very pretty hash you have made of it! I rather think you had better come back with me to London by the night express."
271
+ An hour afterwards, Sherlock Holmes, in his usual garb and style, was seated in my private room at the hotel. His explanation of his sudden and opportune appearance was simplicity itself, for, finding that he could get away from London, he determined to head me off at the next obvious point of my travels. In the disguise of a workingman he had sat in the cabaret waiting for my appearance.
272
+ "And a singularly consistent investigation you have made, my dear Watson," said he. "I cannot at the moment recall any possible blunder which you have omitted. The total effect of your proceeding has been to give the alarm everywhere and yet to discover nothing."
273
+ "Perhaps you would have done no better," I answered bitterly.
274
+
275
+ PROMPT
@@ -0,0 +1,108 @@
1
+ require_relative 'base'
2
+ require_relative '../conversation'
3
+
4
+ module LLMs
5
+ module CLI
6
+ class TestToolUse < Base
7
+ class CalculatorTool
8
+ def self.tool_schema
9
+ {
10
+ name: 'calculator_tool',
11
+ description: 'Compute a plain-text arithmetic calculation using the operators + - * / and parens (). E.g. (2 + 2) / (3 - 1)',
12
+ parameters: {
13
+ properties: {
14
+ calculation: {
15
+ type: 'string',
16
+ description: 'The arithmetic operation to perform'
17
+ },
18
+ },
19
+ type: 'object',
20
+ }
21
+ }
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def default_options
28
+ super.merge({
29
+ max_completion_tokens: 1000,
30
+ prompt: "2+2=",
31
+ system_prompt: "Always reply with a tool use function call"
32
+ })
33
+ end
34
+
35
+ def add_custom_options(opts)
36
+ opts.on("--prompt PROMPT", "Test prompt to use") do |prompt|
37
+ @options[:prompt] = prompt
38
+ end
39
+ end
40
+
41
+ def setup
42
+ # No model name required for this command
43
+ true
44
+ end
45
+
46
+ def perform_execution
47
+ if @options[:model_name]
48
+ test_single_model(create_executor)
49
+ else
50
+ test_all_models
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def test_single_model(executor)
57
+ begin
58
+ cm = LLMs::Conversation.new
59
+ cm.set_system_message(@options[:system_prompt])
60
+ cm.set_available_tools([CalculatorTool])
61
+ cm.add_user_message(@options[:prompt])
62
+
63
+ if @options[:stream]
64
+ executor.execute_conversation(cm) do |chunk|
65
+ print chunk
66
+ end
67
+ puts
68
+ else
69
+ response_message = executor.execute_conversation(cm)
70
+ puts response_message&.text
71
+ end
72
+
73
+ # Display tool calls if present
74
+ if executor.last_received_message&.tool_calls
75
+ puts 'Tool calls: ' + executor.last_received_message.tool_calls.map { |tc| "#{tc.name}: #{tc.arguments} - #{tc.tool_call_id}" }.join(', ')
76
+ end
77
+
78
+ report_error(executor)
79
+ report_usage(executor)
80
+
81
+ rescue StandardError => e
82
+ puts "#{executor.model_name}: ERROR - #{e.message}"
83
+ puts e.backtrace if @options[:debug]
84
+ end
85
+ end
86
+
87
+ def test_all_models
88
+ models = get_models_to_test
89
+
90
+ models.each do |model_name|
91
+ test_single_model(create_executor({model_name: model_name}))
92
+ puts "-" * 80
93
+ end
94
+ end
95
+
96
+ def get_models_to_test
97
+ models = LLMs::Models.list_model_names(full: true, require_tools: true)
98
+
99
+ # Filter by model name if provided
100
+ if ARGV[0]
101
+ models = models.select { |name| name.include?(ARGV[0]) }
102
+ end
103
+
104
+ models
105
+ end
106
+ end
107
+ end
108
+ end
data/lib/llms/cli.rb ADDED
@@ -0,0 +1,12 @@
1
+ require_relative '../llms'
2
+ require_relative 'cli/base'
3
+ require_relative 'cli/chat'
4
+ require_relative 'cli/test_access'
5
+ require_relative 'cli/test_tool_use'
6
+ require_relative 'cli/test_image_support'
7
+ require_relative 'cli/test_prompt_caching'
8
+
9
+ module LLMs
10
+ module CLI
11
+ end
12
+ end
@@ -0,0 +1,100 @@
1
+ require_relative './conversation_message'
2
+ require_relative './conversation_tool_call'
3
+ require_relative './conversation_tool_result'
4
+
5
+ module LLMs
6
+ class Conversation
7
+
8
+ attr_reader :system_message
9
+
10
+ def initialize
11
+ @messages = []
12
+ @system_message = nil
13
+ @available_tools = nil
14
+ end
15
+
16
+ def pending?
17
+ !!@messages.last&.user?
18
+ end
19
+
20
+ def set_system_message(content)
21
+ raise "content is not a String" unless content.is_a?(String)
22
+ @system_message = content
23
+ end
24
+
25
+ def set_available_tools(tools)
26
+ @available_tools = tools
27
+ end
28
+
29
+ def add_user_message(content, tool_results = nil)
30
+ if content.is_a?(LLMs::ConversationMessage)
31
+ unless tool_results.nil?
32
+ raise "tool_results argument not allowed when adding a ConversationMessage"
33
+ end
34
+ unless content.user?
35
+ raise "message role must be 'user' when calling add_user_message with a ConversationMessage"
36
+ end
37
+ add_conversation_message(content)
38
+ else
39
+ unless tool_results.nil? || tool_results.all? { |tr| tr.is_a?(LLMs::ConversationToolResult) }
40
+ raise "tool_results argument must be an array of ConversationToolResult"
41
+ end
42
+ add_conversation_message(LLMs::ConversationMessage.new("user", content, nil, tool_results))
43
+ end
44
+ end
45
+
46
+ def add_assistant_message(content, tool_calls = nil)
47
+ if content.is_a?(LLMs::ConversationMessage)
48
+ unless tool_calls.nil?
49
+ raise "tool_calls argument not allowed when adding a ConversationMessage"
50
+ end
51
+ unless content.assistant?
52
+ raise "message role must be 'assistant' when calling add_assistant_message with a ConversationMessage"
53
+ end
54
+ add_conversation_message(content)
55
+ else
56
+ unless tool_calls.nil? || tool_calls.all? { |tc| tc.is_a?(LLMs::ConversationToolCall) }
57
+ raise "tool_calls argument must be an array of ConversationToolCall"
58
+ end
59
+ add_conversation_message(LLMs::ConversationMessage.new("assistant", content, tool_calls, nil))
60
+ end
61
+ end
62
+
63
+ def add_conversation_message(message)
64
+ raise "message is not a ConversationMessage" unless message.is_a?(LLMs::ConversationMessage)
65
+ @messages << message
66
+ end
67
+
68
+ def system_message
69
+ @system_message&.dup
70
+ end
71
+
72
+ def available_tools
73
+ @available_tools&.dup
74
+ end
75
+
76
+ def messages(include_system_message: false)
77
+ m = @messages.dup
78
+ if include_system_message && @system_message
79
+ m.unshift(LLMs::ConversationMessage.new("system", @system_message, nil, nil))
80
+ end
81
+ m
82
+ end
83
+
84
+
85
+
86
+ def last_message
87
+ @messages.last&.dup
88
+ end
89
+
90
+ def find_tool_call(tool_call_id)
91
+ @messages.each do |message|
92
+ message.tool_calls&.each do |tool_call|
93
+ return tool_call if tool_call.tool_call_id == tool_call_id
94
+ end
95
+ end
96
+ nil
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,60 @@
1
+ require_relative 'conversation_tool_call'
2
+ require_relative 'conversation_tool_result'
3
+
4
+ module LLMs
5
+ class ConversationMessage
6
+ USER_ROLE = 'user'
7
+ ASSISTANT_ROLE = 'assistant'
8
+ SYSTEM_ROLE = 'system'
9
+
10
+ attr_reader :role, :parts, :tool_calls, :tool_results
11
+
12
+ def initialize(role, content, tool_calls = nil, tool_results = nil)
13
+ raise "role (#{role}) is not one of the allowed values" unless role == USER_ROLE || role == ASSISTANT_ROLE || role == SYSTEM_ROLE
14
+ @role = role
15
+ @parts = if content.is_a?(String)
16
+ [{ text: content }]
17
+ elsif content.is_a?(Array)
18
+ content
19
+ else
20
+ raise "content (#{content}) is not a String or Array"
21
+ end
22
+ @tool_calls = tool_calls
23
+ @tool_results = tool_results
24
+ end
25
+
26
+ def text
27
+ if !@parts.nil?
28
+ text_parts = @parts.map{ |part| part[:text] }.compact
29
+ if !text_parts.empty?
30
+ text_parts.join
31
+ end
32
+ end
33
+ end
34
+
35
+ def images
36
+ if !@parts.nil?
37
+ @parts.map{ |part| part[:image] }.compact
38
+ end
39
+ end
40
+
41
+ def empty?
42
+ (@parts.nil? || @parts.empty? || self.text.nil? || self.text.empty?) &&
43
+ (@tool_calls.nil? || @tool_calls.empty?) &&
44
+ (@tool_results.nil? || @tool_results.empty?)
45
+ end
46
+
47
+ def user?
48
+ @role == USER_ROLE
49
+ end
50
+
51
+ def assistant?
52
+ @role == ASSISTANT_ROLE
53
+ end
54
+
55
+ ## Only for OpenAI compatible APIs
56
+ def system?
57
+ @role == SYSTEM_ROLE
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,14 @@
1
+ module LLMs
2
+ class ConversationToolCall
3
+ attr_reader :index, :tool_call_id, :tool_call_type, :name, :arguments
4
+
5
+ def initialize(index, tool_call_id, tool_call_type, name, arguments)
6
+ raise "index is nil" if index.nil?
7
+ @index = index
8
+ @tool_call_id = tool_call_id
9
+ @tool_call_type = tool_call_type
10
+ @name = name
11
+ @arguments = arguments
12
+ end
13
+ end
14
+ end