Claudette’s source

This is the ‘literate’ source code for Claudette. You can view the fully rendered version of the notebook here, or you can clone the git repo and run the interactive notebook in Jupyter. The notebook is converted the Python module claudette/core.py using nbdev. The goal of this source code is to both create the Python module, and also to teach the reader how it is created, without assuming much existing knowledge about Claude’s API.

Most of the time you’ll see that we write some source code first, and then a description or discussion of it afterwards.

Setup

import os
# os.environ['ANTHROPIC_LOG'] = 'debug'

To print every HTTP request and response in full, uncomment the above line. This functionality is provided by Anthropic’s SDK.

Tip

If you’re reading the rendered version of this notebook, you’ll see an “Exported source” collapsible widget below. If you’re reading the source notebook directly, you’ll see #| exports at the top of the cell. These show that this piece of code will be exported into the python module that this notebook creates. No other code will be included – any other code in this notebook is just for demonstration, documentation, and testing.

You can toggle expanding/collapsing the source code of all exported sections by using the </> Code menu in the top right of the rendered notebook page.

Exported source
models = 'claude-3-opus-20240229','claude-3-5-sonnet-20240620','claude-3-haiku-20240307'

These are the current versions of Anthropic’s model at the time of writing.

model = models[1]

For examples, we’ll use Sonnet 3.5, since it’s awesome.

Antropic SDK

cli = Anthropic()

This is what Anthropic’s SDK provides for interacting with Python. To use it, pass it a list of messages, with content and a role. The roles should alternate between user and assistant.

Tip

After the code below you’ll see an indented section with an orange vertical line on the left. This is used to show the result of running the code above. Because the code is running in a Jupyter Notebook, we don’t have to use print to display results, we can just type the expression directly, as we do with r here.

m = {'role': 'user', 'content': "I'm Jeremy"}
r = cli.messages.create(messages=[m], model=model, max_tokens=100)
r
Message(id='msg_01RjWdAktoZPidk5mZoXRRZL', content=[TextBlock(text="Hello Jeremy! It's nice to meet you. Is there anything I can help you with today?", type='text')], model='claude-3-5-sonnet-20240620', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=Usage(input_tokens=10, output_tokens=23))

Formatting output

That output is pretty long and hard to read, so let’s clean it up. We’ll start by pulling out the Content part of the message. To do that, we’re going to write our first function which will be included to the claudette/core.py module.

Tip

This is the first exported public function or class we’re creating (the previous export was of a variable). In the rendered version of the notebook for these you’ll see 4 things, in this order (unless the symbol starts with a single _, which indicates it’s private):

  • The signature (with the symbol name as a heading, with a horizontal rule above)
  • A table of paramater docs (if provided)
  • The doc string (in italics).
  • The source code (in a collapsible “Exported source” block)

After that, we generally provide a bit more detail on what we’ve created, and why, along with a sample usage.


source

find_block

 find_block (r:collections.abc.Mapping, blk_type:type=<class
             'anthropic.types.text_block.TextBlock'>)

Find the first block of type blk_type in r.content.

Type Default Details
r Mapping The message to look in
blk_type type TextBlock The type of block to find
Exported source
def find_block(r:abc.Mapping, # The message to look in
               blk_type:type=TextBlock  # The type of block to find
              ):
    "Find the first block of type `blk_type` in `r.content`."
    return first(o for o in r.content if isinstance(o,blk_type))

This makes it easier to grab the needed parts of Claude’s responses, which can include multiple pieces of content. By default, we look for the first text block. That will generally have the content we want to display.

find_block(r)
TextBlock(text="Hello Jeremy! It's nice to meet you. Is there anything I can help you with today?", type='text')

source

contents

 contents (r)

Helper to get the contents from Claude response r.

Exported source
def contents(r):
    "Helper to get the contents from Claude response `r`."
    blk = find_block(r)
    if not blk and r.content: blk = r.content[0]
    return blk.text.strip() if hasattr(blk,'text') else blk

For display purposes, we often just want to show the text itself.

contents(r)
"Hello Jeremy! It's nice to meet you. Is there anything I can help you with today?"
Exported source
@patch
def _repr_markdown_(self:(Message)):
    det = '\n- '.join(f'{k}: {v}' for k,v in self.model_dump().items())
    return f"""{contents(self)}

<details>

- {det}

</details>"""

Jupyter looks for a _repr_markdown_ method in displayed objects; we add this in order to display just the content text, and collapse full details into a hideable section. Note that patch is from fastcore, and is used to add (or replace) functionality in an existing class. We pass the class(es) that we want to patch as type annotations to self. In this case, _repr_markdown_ is being added to Anthropic’s Message class, so when we display the message now we just see the contents, and the details are hidden away in a collapsible details block.

r

Hello Jeremy! It’s nice to meet you. Is there anything I can help you with today?

  • id: msg_01RjWdAktoZPidk5mZoXRRZL
  • content: [{‘text’: “Hello Jeremy! It’s nice to meet you. Is there anything I can help you with today?”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 10, ‘output_tokens’: 23}

One key part of the response is the usage key, which tells us how many tokens we used by returning a Usage object.

We’ll add some helpers to make things a bit cleaner for creating and formatting these objects.

r.usage
Usage(input_tokens=10, output_tokens=23)

source

usage

 usage (inp=0, out=0)

Slightly more concise version of Usage.

Type Default Details
inp int 0 Number of input tokens
out int 0 Number of output tokens
Exported source
def usage(inp=0, # Number of input tokens
          out=0  # Number of output tokens
         ):
    "Slightly more concise version of `Usage`."
    return Usage(input_tokens=inp, output_tokens=out)

The constructor provided by Anthropic is rather verbose, so we clean it up a bit, using a lowercase version of the name.

usage(5)
Usage(input_tokens=5, output_tokens=0)

source

Usage.total

 Usage.total ()
Exported source
@patch(as_prop=True)
def total(self:Usage): return self.input_tokens+self.output_tokens

Adding a total property to Usage makes it easier to see how many tokens we’ve used up altogether.

usage(5,1).total
6

source

Usage.__repr__

 Usage.__repr__ ()

Return repr(self).

Exported source
@patch
def __repr__(self:Usage): return f'In: {self.input_tokens}; Out: {self.output_tokens}; Total: {self.total}'

In python, patching __repr__ lets us change how an object is displayed. (More generally, methods starting and ending in __ in Python are called dunder methods, and have some magic behavior – such as, in this case, changing how an object is displayed.)

r.usage
In: 10; Out: 23; Total: 33

source

Usage.__add__

 Usage.__add__ (b)

Add together each of input_tokens and output_tokens

Exported source
@patch
def __add__(self:Usage, b):
    "Add together each of `input_tokens` and `output_tokens`"
    return usage(self.input_tokens+b.input_tokens, self.output_tokens+b.output_tokens)

And, patching __add__ lets + work on a Usage object.

r.usage+r.usage
In: 20; Out: 46; Total: 66

Creating messages

Creating correctly formatted dicts from scratch every time isn’t very handy, so next up we’ll add helpers for this.

def mk_msg(content, role='user', **kw):
    return dict(role=role, content=content, **kw)

We make things a bit more convenient by writing a function to create a message for us.

Note

You may have noticed that we didn’t export the mk_msg function (i.e. there’s no “Exported source” block around it). That’s because we’ll need more functionality in our final version than this version has – so we’ll be defining a more complete version later. Rather than refactoring/editing in notebooks, often it’s helpful to simply gradually build up complexity by re-defining a symbol.

prompt = "I'm Jeremy"
m = mk_msg(prompt)
m
{'role': 'user', 'content': "I'm Jeremy"}
r = cli.messages.create(messages=[m], model=model, max_tokens=100)
r

Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to chat about or any questions you have?

  • id: msg_01TDkKPNLXoSK8DCPuyP37hX
  • content: [{‘text’: “Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to chat about or any questions you have?”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 10, ‘output_tokens’: 36}

source

mk_msgs

 mk_msgs (msgs:list, **kw)

Helper to set ‘assistant’ role on alternate messages.

Exported source
def mk_msgs(msgs:list, **kw):
    "Helper to set 'assistant' role on alternate messages."
    if isinstance(msgs,str): msgs=[msgs]
    return [mk_msg(o, ('user','assistant')[i%2], **kw) for i,o in enumerate(msgs)]

LLMs, including Claude, don’t actually have state, but instead dialogs are created by passing back all previous prompts and responses every time. With Claude, they always alternate user and assistant. Therefore we create a function to make it easier to build up these dialog lists.

But to do so, we need to update mk_msg so that we can’t only pass a str as content, but can also pass a dict or an object with a content attr, since these are both types of message that Claude can create. To do so, we check for a content key or attr, and use it if found.

def mk_msg(content, role='user', **kw):
    "Helper to create a `dict` appropriate for a Claude message. `kw` are added as key/value pairs to the message"
    if hasattr(content, 'content'): content,role = content.content,content.role
    if isinstance(content, abc.Mapping): content=content['content']
    return dict(role=role, content=content, **kw)
msgs = mk_msgs([prompt, r, 'I forgot my name. Can you remind me please?'])
msgs
[{'role': 'user', 'content': "I'm Jeremy"},
 {'role': 'assistant',
  'content': [TextBlock(text="Hello Jeremy! It's nice to meet you. How can I assist you today? Is there anything specific you'd like to chat about or any questions you have?", type='text')]},
 {'role': 'user', 'content': 'I forgot my name. Can you remind me please?'}]

Now, if we pass this list of messages to Claude, the model treats it as a conversation to respond to.

cli.messages.create(messages=msgs, model=model, max_tokens=200)

Of course! You just introduced yourself as Jeremy. That’s your name.

  • id: msg_014avFgGrLozDLLtVPy6BerE
  • content: [{‘text’: “Of course! You just introduced yourself as Jeremy. That’s your name.”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 60, ‘output_tokens’: 18}

Client


source

Client

 Client (model, cli=None)

Basic Anthropic messages client.

Exported source
class Client:
    def __init__(self, model, cli=None):
        "Basic Anthropic messages client."
        self.model,self.use = model,usage()
        self.c = (cli or Anthropic())

We’ll create a simple Client for Anthropic which tracks usage stores the model to use. We don’t add any methods right away – instead we’ll use patch for that so we can add and document them incrementally.

c = Client(model)
c.use
In: 0; Out: 0; Total: 0
Exported source
@patch
def _r(self:Client, r:Message, prefill=''):
    "Store the result of the message and accrue total usage."
    if prefill:
        blk = find_block(r)
        blk.text = prefill + (blk.text or '')
    self.result = r
    self.use += r.usage
    return r

We use a _ prefix on private methods, but we document them here in the interests of literate source code.

_r will be used each time we get a new result, to track usage and also to keep the result available for later.

c._r(r)
c.use
In: 10; Out: 36; Total: 46

Whereas OpenAI’s models use a stream parameter for streaming, Anthropic’s use a separate method. We implement Anthropic’s approach in a private method, and then use a stream parameter in __call__ for consistency:

Exported source
@patch
def _stream(self:Client, msgs:list, prefill='', **kwargs):
    with self.c.messages.stream(model=self.model, messages=mk_msgs(msgs), **kwargs) as s:
        if prefill: yield(prefill)
        yield from s.text_stream
        self._r(s.get_final_message(), prefill)

Claude supports adding an extra assistant message at the end, which contains the prefill – i.e. the text we want Claude to assume the response starts with. However Claude doesn’t actually repeat that in the response, so for convenience we add it.


source

Client.__call__

 Client.__call__ (msgs:list, sp='', temp=0, maxtok=4096, prefill='',
                  stream:bool=False, metadata:message_create_params.Metada
                  ta|NotGiven=NOT_GIVEN,
                  stop_sequences:List[str]|NotGiven=NOT_GIVEN, system:Unio
                  n[str,Iterable[TextBlockParam]]|NotGiven=NOT_GIVEN,
                  temperature:float|NotGiven=NOT_GIVEN, tool_choice:messag
                  e_create_params.ToolChoice|NotGiven=NOT_GIVEN,
                  tools:Iterable[ToolParam]|NotGiven=NOT_GIVEN,
                  top_k:int|NotGiven=NOT_GIVEN,
                  top_p:float|NotGiven=NOT_GIVEN,
                  extra_headers:Headers|None=None,
                  extra_query:Query|None=None, extra_body:Body|None=None,
                  timeout:float|httpx.Timeout|None|NotGiven=600)

Make a call to Claude.

Type Default Details
msgs list List of messages in the dialog
sp str The system prompt
temp int 0 Temperature
maxtok int 4096 Maximum tokens
prefill str Optional prefill to pass to Claude as start of its response
stream bool False Stream response?
metadata message_create_params.Metadata | NotGiven NOT_GIVEN
stop_sequences List[str] | NotGiven NOT_GIVEN
system Union[str, Iterable[TextBlockParam]] | NotGiven NOT_GIVEN
temperature float | NotGiven NOT_GIVEN
tool_choice message_create_params.ToolChoice | NotGiven NOT_GIVEN
tools Iterable[ToolParam] | NotGiven NOT_GIVEN
top_k int | NotGiven NOT_GIVEN
top_p float | NotGiven NOT_GIVEN
extra_headers Headers | None None
extra_query Query | None None
extra_body Body | None None
timeout float | httpx.Timeout | None | NotGiven 600
Exported source
@patch
@delegates(messages.Messages.create)
def __call__(self:Client,
             msgs:list, # List of messages in the dialog
             sp='', # The system prompt
             temp=0, # Temperature
             maxtok=4096, # Maximum tokens
             prefill='', # Optional prefill to pass to Claude as start of its response
             stream:bool=False, # Stream response?
             **kwargs):
    "Make a call to Claude."
    pref = [prefill.strip()] if prefill else []
    if not isinstance(msgs,list): msgs = [msgs]
    msgs = mk_msgs(msgs+pref)
    if stream: return self._stream(msgs, prefill=prefill, max_tokens=maxtok, system=sp, temperature=temp, **kwargs)
    res = self.c.messages.create(
        model=self.model, messages=msgs, max_tokens=maxtok, system=sp, temperature=temp, **kwargs)
    self._r(res, prefill)
    return self.result

Defining __call__ let’s us use an object like a function (i.e it’s callable). We use it as a small wrapper over messages.create.

c = Client(model)
c.use
In: 0; Out: 0; Total: 0
c.model = models[-1]
c('Hi')

Hello! How can I assist you today?

  • id: msg_01Ds4j94G5j51jVdY5aJKPsc
  • content: [{‘text’: ‘Hello! How can I assist you today?’, ‘type’: ‘text’}]
  • model: claude-3-haiku-20240307
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 8, ‘output_tokens’: 12}
c.use
In: 8; Out: 12; Total: 20

Let’s try out prefill:

q = "Concisely, what is the meaning of life?"
pref = 'According to Douglas Adams,'
c(q, prefill=pref)

According to Douglas Adams, “The answer to the ultimate question of life, the universe, and everything is 42.”

  • id: msg_01RQ9JXwWi8ncwhwTJBtncKB
  • content: [{‘text’: ‘According to Douglas Adams, “The answer to the ultimate question of life, the universe, and everything is 42.”’, ‘type’: ‘text’}]
  • model: claude-3-haiku-20240307
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 24, ‘output_tokens’: 23}

We can pass stream=True to stream the response back incrementally:

for o in c('Hi', stream=True): print(o, end='')
Hello! How can I assist you today?
c.use
In: 40; Out: 47; Total: 87
for o in c(q, prefill=pref, stream=True): print(o, end='')
According to Douglas Adams,  "The answer to the ultimate question of life, the universe, and everything is 42."
c.use
In: 64; Out: 70; Total: 134

Tool use

Let’s now add tool use (aka function calling).


source

mk_tool_choice

 mk_tool_choice (choose:Union[str,bool,NoneType])

Create a tool_choice dict that’s ‘auto’ if choose is None, ‘any’ if it is True, or ‘tool’ otherwise

Exported source
def mk_tool_choice(choose:Union[str,bool,None])->dict:
    "Create a `tool_choice` dict that's 'auto' if `choose` is `None`, 'any' if it is True, or 'tool' otherwise"
    return {"type": "tool", "name": choose} if isinstance(choose,str) else {'type':'any'} if choose else {'type':'auto'}
print(mk_tool_choice('sums'))
print(mk_tool_choice(True))
print(mk_tool_choice(None))
{'type': 'tool', 'name': 'sums'}
{'type': 'any'}
{'type': 'auto'}

Claude can be forced to use a particular tool, or select from a specific list of tools, or decide for itself when to use a tool. If you want to force a tool (or force choosing from a list), include a tool_choice param with a dict from mk_tool_choice.

For testing, we need a function that Claude can call; we’ll write a simple function that adds numbers together, and will tell us when it’s being called:

def sums(
    a:int,  # First thing to sum
    b:int=1 # Second thing to sum
) -> int: # The sum of the inputs
    "Adds a + b."
    print(f"Finding the sum of {a} and {b}")
    return a + b
a,b = 604542,6458932
pr = f"What is {a}+{b}?"
sp = "You are a summing expert."

Claudette can autogenerate a schema thanks to the toolslm library. We’ll force the use of the tool using the function we created earlier.

tools=[get_schema(sums)]
choice = mk_tool_choice('sums')

We’ll start a dialog with Claude now. We’ll store the messages of our dialog in msgs. The first message will be our prompt pr, and we’ll pass our tools schema.

msgs = mk_msgs(pr)
r = c(msgs, sp=sp, tools=tools, tool_choice=choice)
r

ToolUseBlock(id=‘toolu_01HeytuCw3qJvHQdoqvR7DzV’, input={‘a’: 604542, ‘b’: 6458932}, name=‘sums’, type=‘tool_use’)

  • id: msg_01XdZdVPohkqoC9VbBAdkbFS
  • content: [{‘id’: ‘toolu_01HeytuCw3qJvHQdoqvR7DzV’, ‘input’: {‘a’: 604542, ‘b’: 6458932}, ‘name’: ‘sums’, ‘type’: ‘tool_use’}]
  • model: claude-3-haiku-20240307
  • role: assistant
  • stop_reason: tool_use
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 493, ‘output_tokens’: 53}

When Claude decides that it should use a tool, it passes back a ToolUseBlock with the name of the tool to call, and the params to use.

We don’t want to allow it to call just any possible function (that would be a security disaster!) so we create a namespace – that is, a dictionary of allowable function names to call.

Exported source
def _mk_ns(*funcs:list[callable]) -> dict[str,callable]:
    "Create a `dict` of name to function in `funcs`, to use as a namespace"
    return {f.__name__:f for f in funcs}
ns = _mk_ns(sums)
ns
{'sums': <function __main__.sums(a: int, b: int = 1) -> int>}

source

call_func

 call_func (fc:anthropic.types.tool_use_block.ToolUseBlock,
            ns:Optional[collections.abc.Mapping]=None, obj:Optional=None)

Call the function in the tool response tr, using namespace ns.

Type Default Details
fc ToolUseBlock Tool use block from Claude’s message
ns Optional None Namespace to search for tools, defaults to globals()
obj Optional None Object to search for tools
Exported source
def call_func(fc:ToolUseBlock, # Tool use block from Claude's message
              ns:Optional[abc.Mapping]=None, # Namespace to search for tools, defaults to `globals()`
              obj:Optional=None # Object to search for tools
             ):
    "Call the function in the tool response `tr`, using namespace `ns`."
    if ns is None: ns=globals()
    if not isinstance(ns, abc.Mapping): ns = _mk_ns(*ns)
    func = getattr(obj, fc.name, None)
    if not func: func = ns[fc.name]
    res = func(**fc.input)
    return dict(type="tool_result", tool_use_id=fc.id, content=str(res))

We can now use the function requested by Claude. We look it up in ns, and pass in the provided parameters.

fc = find_block(r, ToolUseBlock)
res = call_func(fc, ns=ns)
res
Finding the sum of 604542 and 6458932
{'type': 'tool_result',
 'tool_use_id': 'toolu_01HeytuCw3qJvHQdoqvR7DzV',
 'content': '7063474'}

source

mk_toolres

 mk_toolres (r:collections.abc.Mapping,
             ns:Optional[collections.abc.Mapping]=None, obj:Optional=None)

Create a tool_result message from response r.

Type Default Details
r Mapping Tool use request response from Claude
ns Optional None Namespace to search for tools
obj Optional None Class to search for tools
Exported source
def mk_toolres(
    r:abc.Mapping, # Tool use request response from Claude
    ns:Optional[abc.Mapping]=None, # Namespace to search for tools
    obj:Optional=None # Class to search for tools
    ):
    "Create a `tool_result` message from response `r`."
    cts = getattr(r, 'content', [])
    res = [mk_msg(r)]
    tcs = [call_func(o, ns=ns, obj=obj) for o in cts if isinstance(o,ToolUseBlock)]
    if tcs: res.append(mk_msg(tcs))
    return res

In order to tell Claude the result of the tool call, we pass back the tool use assistant request and the tool_result response.

tr = mk_toolres(r, ns=ns)
tr
Finding the sum of 604542 and 6458932
[{'role': 'assistant',
  'content': [ToolUseBlock(id='toolu_01HeytuCw3qJvHQdoqvR7DzV', input={'a': 604542, 'b': 6458932}, name='sums', type='tool_use')]},
 {'role': 'user',
  'content': [{'type': 'tool_result',
    'tool_use_id': 'toolu_01HeytuCw3qJvHQdoqvR7DzV',
    'content': '7063474'}]}]

We add this to our dialog, and now Claude has all the information it needs to answer our question.

msgs += tr
contents(c(msgs, sp=sp, tools=tools))
'The sum of 604542 and 6458932 is 7063474.'

This works with methods as well – in this case, use the object itself for ns:

class Dummy:
    def sums(
        self,
        a:int,  # First thing to sum
        b:int=1 # Second thing to sum
    ) -> int: # The sum of the inputs
        "Adds a + b."
        print(f"Finding the sum of {a} and {b}")
        return a + b
tools = [get_schema(Dummy.sums)]
o = Dummy()

msgs = mk_msgs(pr)
r = c(msgs, sp=sp, tools=tools, tool_choice=choice)
tr = mk_toolres(r, obj=o)
msgs += tr
contents(c(msgs, sp=sp, tools=tools))
Finding the sum of 604542 and 6458932
'The sum of 604542 and 6458932 is 7063474.'

Chat

Rather than manually adding the responses to a dialog, we’ll create a simple Chat class to do that for us, each time we make a request. We’ll also store the system prompt and tools here, to avoid passing them every time.


source

Chat

 Chat (model:Optional[str]=None, cli:Optional[__main__.Client]=None,
       sp='', tools:Optional[list]=None, tool_choice:Optional[dict]=None)

Anthropic chat client.

Type Default Details
model Optional None Model to use (leave empty if passing cli)
cli Optional None Client to use (leave empty if passing model)
sp str Optional system prompt
tools Optional None List of tools to make available to Claude
tool_choice Optional None Optionally force use of some tool
Exported source
class Chat:
    def __init__(self,
                 model:Optional[str]=None, # Model to use (leave empty if passing `cli`)
                 cli:Optional[Client]=None, # Client to use (leave empty if passing `model`)
                 sp='', # Optional system prompt
                 tools:Optional[list]=None, # List of tools to make available to Claude
                 tool_choice:Optional[dict]=None): # Optionally force use of some tool
        "Anthropic chat client."
        assert model or cli
        self.c = (cli or Client(model))
        self.h,self.sp,self.tools,self.tool_choice = [],sp,tools,tool_choice

    @property
    def use(self): return self.c.use

The class stores the Client that will provide the responses in c, and a history of messages in h.

sp = "Never mention what tools you use."
chat = Chat(model, sp=sp)
chat.c.use, chat.h
(In: 0; Out: 0; Total: 0, [])

source

Chat.__call__

 Chat.__call__ (pr=None, temp=0, maxtok=4096, stream=False, prefill='',
                **kw)

Call self as a function.

Type Default Details
pr NoneType None Prompt / message
temp int 0 Temperature
maxtok int 4096 Maximum tokens
stream bool False Stream response?
prefill str Optional prefill to pass to Claude as start of its response
kw
Exported source
@patch
def _stream(self:Chat, res):
    yield from res
    self.h += mk_toolres(self.c.result, ns=self.tools, obj=self)
Exported source
@patch
def __call__(self:Chat,
             pr=None,  # Prompt / message
             temp=0, # Temperature
             maxtok=4096, # Maximum tokens
             stream=False, # Stream response?
             prefill='', # Optional prefill to pass to Claude as start of its response
             **kw):
    if pr and self.h and nested_idx(self.h, -1, 'role')=='user':
        self() # There's already a user request pending, so complete it
    if pr: self.h.append(mk_msg(pr))
    if self.tools: kw['tools'] = [get_schema(o) for o in self.tools]
    if self.tool_choice: kw['tool_choice'] = mk_tool_choice(self.tool_choice)
    res = self.c(self.h, stream=stream, prefill=prefill, sp=self.sp, temp=temp, maxtok=maxtok, **kw)
    if stream: return self._stream(res)
    self.h += mk_toolres(self.c.result, ns=self.tools, obj=self)
    return res

The __call__ method just passes the request along to the Client, but rather than just passing in this one prompt, it appends it to the history and passes it all along. As a result, we now have state!

chat("I'm Jeremy")
chat("What's my name?")

Your name is Jeremy, as you mentioned in your previous message.

  • id: msg_01HGKL13BqSvzz7q42ub79ra
  • content: [{‘text’: ‘Your name is Jeremy, as you mentioned in your previous message.’, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 64, ‘output_tokens’: 16}

Let’s try out prefill too:

q = "Concisely, what is the meaning of life?"
pref = 'According to Douglas Adams,'
chat(q, prefill=pref)

According to Douglas Adams, the meaning of life is 42. More seriously, there’s no universally agreed upon answer. Common philosophical perspectives include:

  1. Finding personal fulfillment
  2. Serving others
  3. Pursuing happiness
  4. Creating meaning through our choices
  5. Experiencing and appreciating existence

Ultimately, many believe each individual must determine their own life’s meaning.

  • id: msg_01TghsukVRvHiZdCoX1WxSHH
  • content: [{‘text’: “According to Douglas Adams, the meaning of life is 42. More seriously, there’s no universally agreed upon answer. Common philosophical perspectives include:. Finding personal fulfillment. Serving others. Pursuing happiness. Creating meaning through our choices. Experiencing and appreciating existence, many believe each individual must determine their own life’s meaning.”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 100, ‘output_tokens’: 82}
chat = Chat(model, sp=sp)
for o in chat("I'm Jeremy", stream=True): print(o, end='')
Hello Jeremy! It's nice to meet you. How are you doing today? Is there anything in particular you'd like to chat about or any questions I can help you with?
for o in chat(q, prefill=pref, stream=True): print(o, end='')
According to Douglas Adams,  the meaning of life is 42. More seriously, there's no universally agreed upon answer. Common philosophical perspectives include:

1. Finding personal fulfillment
2. Serving others
3. Pursuing happiness
4. Creating meaning through our choices
5. Experiencing and appreciating existence

Ultimately, many believe each individual must determine their own meaning.

Chat tool use

We automagically get streamlined tool use as well:

pr = f"What is {a}+{b}?"
pr
'What is 604542+6458932?'
chat = Chat(model, sp=sp, tools=[sums])
r = chat(pr)
r
Finding the sum of 604542 and 6458932

To answer this question, I can use the “sums” function to add these two numbers together. Let me do that for you.

  • id: msg_01WxUoaTJ3dAyhjEYLvc9EnY
  • content: [{‘text’: ‘To answer this question, I can use the “sums” function to add these two numbers together. Let me do that for you.’, ‘type’: ‘text’}, {‘id’: ‘toolu_01DXhNKYAdSfE6emyf3dXMWU’, ‘input’: {‘a’: 604542, ‘b’: 6458932}, ‘name’: ‘sums’, ‘type’: ‘tool_use’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: tool_use
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 428, ‘output_tokens’: 101}
chat()

The sum of 604542 and 6458932 is 7063474.

  • id: msg_011NE19yxPQcTy7zSb9814P6
  • content: [{‘text’: ‘The sum of 604542 and 6458932 is 7063474.’, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 543, ‘output_tokens’: 23}

It should be correct, because it actually used our Python function to do the addition. Let’s check:

a+b
7063474

Images

Claude can handle image data as well. As everyone knows, when testing image APIs you have to use a cute puppy.

# Image is Cute_dog.jpg from Wikimedia
fn = Path('samples/puppy.jpg')
display.Image(filename=fn, width=200)

img = fn.read_bytes()

source

img_msg

 img_msg (data:bytes)

Convert image data into an encoded dict

Exported source
def img_msg(data:bytes)->dict:
    "Convert image `data` into an encoded `dict`"
    img = base64.b64encode(data).decode("utf-8")
    mtype = mimetypes.types_map['.'+imghdr.what(None, h=data)]
    r = dict(type="base64", media_type=mtype, data=img)
    return {"type": "image", "source": r}

Anthropic have documented the particular dict structure that expect image data to be in, so we have a little function to create that for us.


source

text_msg

 text_msg (s:str)

Convert s to a text message

Exported source
def text_msg(s:str)->dict:
    "Convert `s` to a text message"
    return {"type": "text", "text": s}

A Claude message can be a list of image and text parts. So we’ve also created a helper for making the text parts.

q = "In brief, what color flowers are in this image?"
msg = mk_msg([img_msg(img), text_msg(q)])
c([msg])

The image contains purple or lavender-colored flowers, which appear to be daisies or a similar type of flower.

  • id: msg_01R34q46R6yfr1vAGuDQT1t7
  • content: [{‘text’: ‘The image contains purple or lavender-colored flowers, which appear to be daisies or a similar type of flower.’, ‘type’: ‘text’}]
  • model: claude-3-haiku-20240307
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 110, ‘output_tokens’: 28}
Exported source
def _mk_content(src):
    "Create appropriate content data structure based on type of content"
    if isinstance(src,str): return text_msg(src)
    if isinstance(src,bytes): return img_msg(src)
    return src

There’s not need to manually choose the type of message, since we figure that out from the data of the source data.

_mk_content('Hi')
{'type': 'text', 'text': 'Hi'}

source

mk_msg

 mk_msg (content, role='user', **kw)

Helper to create a dict appropriate for a Claude message. kw are added as key/value pairs to the message

Type Default Details
content A string, list, or dict containing the contents of the message
role str user Must be ‘user’ or ‘assistant’
kw
Exported source
def mk_msg(content, # A string, list, or dict containing the contents of the message
           role='user', # Must be 'user' or 'assistant'
           **kw):
    "Helper to create a `dict` appropriate for a Claude message. `kw` are added as key/value pairs to the message"
    if hasattr(content, 'content'): content,role = content.content,content.role
    if isinstance(content, abc.Mapping): content=content['content']
    if not isinstance(content, list): content=[content]
    content = [_mk_content(o) for o in content] if content else '.'
    return dict(role=role, content=content, **kw)

When we construct a message, we now use _mk_content to create the appropriate parts. Since a dialog contains multiple messages, and a message can contain multiple content parts, to pass a single message with multiple parts we have to use a list containing a single list:

c([[img, q]])

The image contains purple or lavender-colored flowers, which appear to be daisies or a similar type of flower.

  • id: msg_01DxDS3ZXKsshFFaQ7KDePXq
  • content: [{‘text’: ‘The image contains purple or lavender-colored flowers, which appear to be daisies or a similar type of flower.’, ‘type’: ‘text’}]
  • model: claude-3-haiku-20240307
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 110, ‘output_tokens’: 28}
Note

As promised (much!) earlier, we’ve now finally completed our definition of mk_msg, and this version is the one we export to the Python module.

Third party providers

Amazon Bedrock

These are Amazon’s current Claude models:

We don’t need any extra code to support Amazon Bedrock – we just have to set up the approach client:

ab = AnthropicBedrock(
    aws_access_key=os.environ['AWS_ACCESS_KEY'],
    aws_secret_key=os.environ['AWS_SECRET_KEY'],
)
client = Client(models_aws[-1], ab)
chat = Chat(cli=client)
chat("I'm Jeremy")

Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to talk about or any questions you have?

  • id: msg_bdrk_01NbZjw5bVCMxoCu8b8hVuSu
  • content: [{‘text’: “Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to talk about or any questions you have?”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 10, ‘output_tokens’: 36}

Google Vertex

from anthropic import AnthropicVertex
import google.auth
project_id = google.auth.default()[1]
region = "us-east5"
gv = AnthropicVertex(project_id=project_id, region=region)
client = Client(models_goog[-1], gv)
chat = Chat(cli=client)
chat("I'm Jeremy")

Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to talk about or any questions you have?

  • id: msg_vrtx_014RDYiPp7B2jkMJxzogWQaT
  • content: [{‘text’: “Hello Jeremy! It’s nice to meet you. How can I assist you today? Is there anything specific you’d like to talk about or any questions you have?”, ‘type’: ‘text’}]
  • model: claude-3-5-sonnet-20240620
  • role: assistant
  • stop_reason: end_turn
  • stop_sequence: None
  • type: message
  • usage: {‘input_tokens’: 10, ‘output_tokens’: 36}