markhneedham/blog huggingface dataset:
Python: TypeError: Instance and class checks can only be used with @runtime_checkable protocols
Date: 2023-08-21 00:44:37
In this post, we'll learn how to work around an exception when trying to find Python classes that extend a sub class.

:icons: font

I've been playing around with https://www.trychroma.com/[ChromaDB^] and I wanted to programatically get a list of the embedding functions, which was a little trickier thna I expected.
In this blog post, we'll explore how I failed and then succeeded at this task.

But first, let's install ChromaDB:

[source, bash]
----
pip install chromadb
----

The embedding functions live in the `chromadb.utils.embedding_functions` module. 
So my first thought was that I could list all the things defined in that module and then check which ones were a sub class of `EmbeddingFunction`:

[source, python]
----
import chromadb.utils.embedding_functions as ef

[
    cls_name in dir(ef) 
    for cls_name in dir(ef) 
    if issubclass(getattr(ef, cls_name), ef.EmbeddingFunction)
]
----

If we run this, we'll see the following error:

[source, text]
----
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in <listcomp>
  File "/Users/markhneedham/Library/Caches/pypoetry/virtualenvs/chroma-example-SSMwZxEc-py3.11/lib/python3.11/site-packages/typing_extensions.py", line 662, in __subclasscheck__
    raise TypeError(
TypeError: Instance and class checks can only be used with @runtime_checkable protocols
----

From my understanding, we'd need `EmbeddingFunction` to have a `@runtime_checkable` annotation for this technique to work. 
ChatGPT guided me to the following alternative, where we check if `EmbeddingFunction` exists in in the base classes of a given class via the  `__bases__` attribute:

[source, python]
----
import inspect

[
    cls_name
    for cls_name in dir(ef) 
    if inspect.isclass(getattr(ef,cls_name)) and ef.EmbeddingFunction in getattr(ef,cls_name).__bases__
]
----

If we run that piece of code, we'll see the following output:

[source, text]
----
['CohereEmbeddingFunction',
 'GooglePalmEmbeddingFunction',
 'GoogleVertexEmbeddingFunction',
 'HuggingFaceEmbeddingFunction',
 'InstructorEmbeddingFunction',
 'ONNXMiniLM_L6_V2',
 'OpenAIEmbeddingFunction',
 'SentenceTransformerEmbeddingFunction',
 'Text2VecEmbeddingFunction']
----
pyarrow: pyarrow.lib.ArrowNotImplementedError: Filter argument must be boolean type
Date: 2023-08-23 00:44:37
In this post, we'll learn how to filter columns using Apache Arrow in Python.

:icons: font

I wanted to filter a table in pyarrow table recently and ran into troubles when trying to use the filter syntax that I'm used to from DuckDB.
In this blog post I'll explain my mistake and how to fix it.

First, let's install `pyarrow`:

[source, bash]
----
pip install pyarrow
----

And now we're going to create a table that has a few countries and their corresponding continents:

[source, python]
----
import pyarrow as pa

countries = pa.Table.from_arrays(
    [
      pa.array(['India', 'Pakistan', 'Belgium', 'Finland'], pa.string()),
      pa.array(['Asia', 'Asia', 'Europe', 'Europe'], pa.string())
    ],
    names=['Country', 'Continent']
)
----

Let's say we want to find just the rows where the continent is Europe. 
I initially tried to do that using the following syntax:

[source, python]
----
countries.filter("Continent = 'Europe'")
----

.Output
[source, text]
----
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "pyarrow/table.pxi", line 3154, in pyarrow.lib.Table.filter
  File "/Users/markhneedham/Library/Caches/pypoetry/virtualenvs/ch07-YVG4Qrie-py3.11/lib/python3.11/site-packages/pyarrow/compute.py", line 259, in wrapper
    return func.call(args, options, memory_pool)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyarrow/_compute.pyx", line 367, in pyarrow._compute.Function.call
  File "pyarrow/error.pxi", line 144, in pyarrow.lib.pyarrow_internal_check_status
  File "pyarrow/error.pxi", line 121, in pyarrow.lib.check_status
pyarrow.lib.ArrowNotImplementedError: Filter argument must be boolean type
----

Hmmm, that didn't work so well.
Instead we need to construct a filter predicate using some functions from the `pyarrow.compute` module, so let's import that:

[source, python]
----
import pyarrow.compute as pc
----

And now we have (at least) two ways to write the filtering statement. 
We could use `pc.equal` like this:

[source, python]
----
countries.filter(pc.equal(countries["Continent"], "Europe"))
----

Or `pc.field` like this:

[source, python]
----
countries.filter(pc.field("Continent") == "Europe")
----

Either way we get the same result:

.Output
[source, text]
-----
pyarrow.Table
Country: string
Continent: string
----
Country: [["Belgium","Finland"]]
Continent: [["Europe","Europe"]]
-----
Python: Padding a string
Date: 2023-06-05 02:44:37
In this post we'll learn how to pad the left and right of a string in Python.

I've been writing some scripts to parse data from Apache Pinot's HTTP API and I wanted to format the values stored in a map to make them more readable.
In this blog post, we'll look at some ways that I did that.

I started with a map that looked a bit like this:

[source, python]
----
segments = {
    "events3__4__1__20230605T1335Z": "CONSUMED",
    "events3__4__20__20230605T1335Z": "CONSUMING"
}
----

And then I iterated over and printed each item like this:

[source, python]
----
for segment_id, status in segments.items():
  print(segment_id, status)
----

.Output
[source, text]
----
events3__4__1__20230605T1335Z CONSUMED
events3__4__20__20230605T1335Z CONSUMING
----

I wanted to have the `segment_id` be a fixed width so that the statuses would be aligned.
One way that we can do this is with an f-string.
If we want to add padding to the right with a fixed width of 35 columns, it'd be like this:

[source, python]
----
for segment_id, status in segments.items():
  print(f"{segment_id: <35}", status)
----

.Output
[source, text]
----
events3__4__1__20230605T1335Z       CONSUMED
events3__4__20__20230605T1335Z      CONSUMING
----

And if we want to add padding to the left:

[source, python]
----
for segment_id, status in segments.items():
  print(f"{segment_id: >35}", status)
----

.Output
[source, text]
----
      events3__4__1__20230605T1335Z CONSUMED
     events3__4__20__20230605T1335Z CONSUMING
----

That works well if we want to fill with spaces, but what if we want to specify a filler character? 
In that case, we can use `ljust` and `rjust`:

[source, python]
----
for segment_id, status in segments.items():
  print(segment_id.ljust(35, "."), status)
----

.Output
[source, text]
----
events3__4__1__20230605T1335Z...... CONSUMED
events3__4__20__20230605T1335Z..... CONSUMING
----

And padding from the left:

[source, python]
----
for segment_id, status in segments.items():
  print(segment_id.rjust(35, "-"), status)
----

.Output
[source, text]
----
------events3__4__1__20230605T1335Z CONSUMED
-----events3__4__20__20230605T1335Z CONSUMING
----
DuckDB/SQL: Convert string in YYYYmmdd format to Date
Date: 2023-06-20 02:44:37
In this post we'll learn how to convert a date string to a Date type in DuckDB.

I've been working with a data set that represents dates as strings in the format 'YYYYmmdd' and I wanted to convert those values to Dates in DuckDB.
In this blog post, we'll learn how to do that.

Let's create a small table with a single column that represents date of births:

[source, sql]
----
create table players (dob VARCHAR);
insert into players values('20080203'), ('20230708');
----

We can write the following query to return the rows in the table:

[source, sql]
----
select * from players;
----

.Output
[%header,format=csv]
|===
dob
20080203
20230708
|===

At the moment the date of birth is a string, but we can use the `strptime` function to convert it to a timestamp:

[source, sql]
----
SELECT strptime(dob, '%Y%m%d') AS dob
FROM players;
----

.Output
[%header,format=csv]
|===
dob
"2008-02-03 00:00:00"
"2023-07-08 00:00:00"
|===

I don't really care about the time component though, so let's get rid of that by casting it to a `DATE`:

[source, sql]
----
SELECT cast(strptime(dob, '%Y%m%d') AS DATE) AS dob
FROM players;
----

.Output
[%header,format=csv]
|===
dob
2008-02-03
2023-07-08
|===

Job done!
Python: Get the first item from a collection, ignore the rest
Date: 2023-06-27 04:44:37
In this post, we're going to explore some ways that we can get the first item from a collection while throwing away the other values.

:icons: font

When writing Python scripts, I often find myself wanting to take the first item from a collection and ignore the rest of the values.
I usually use something like `values[0]` to take the first value from the list, but I was curious whether I could do better by using https://blog.teclado.com/destructuring-in-python/[destructuring^].
That's what we're going to explore in this blog post.

We'll start with a list that contains some names:

[source, python]
----
values = ["Michael", "Ryan", "Karin", "Mark", "Jennifer", "Will"]
----

We can destructure this list to get the first item and assign the rest to another variable like this:

[source, python]
----
first, *rest = values
print(first)
print(rest)
----

.Output
[source, text]
----
Michael
['Ryan', 'Karin', 'Mark', 'Jennifer', 'Will']
----

If we don't care about the other values, we could use `_` instead of `rest`:

[source, python]
----
first, *_ = values
----

The other values will still be assigned to a variable named `_`, but this seems to be a convention for throwing away data in Python.

What about if we try to get the first item when the list is empty?

[source, python]
----
values = []
first, *_ = values
----

.Output
[source, text]
----
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected at least 1, got 0)
----

One way around this would be to check that the collection contains some values first:

[source, python]
----
values = []
if len(values) > 0:
    first, *_ = values
else:
    print("values is empty")
----

.Output
[source, text]
----
values is empty
----

Hmmm, a bit verbose.
We could convert that into a one-liner if we aren't worried about printing the message when it's empty:

[source, python]
----
values = []
first, *_ = values if len(values) > 0 else [None]
print(first)
print(rest)
----

.Output
[source, text]
----
None
[]
----

Not bad, but probably not better than my usual technique, which would read like this:

[source, python]
----
values = []
first = values[0] if len(values) > 0 else None
print(first)
----
Python: All about the next function
Date: 2023-06-28 04:44:37
In this post, we're going to learn about the Python next function, which can be used to extract values from an iterator.

Yesterday I wrote a blog post about some different ways to https://www.markhneedham.com/blog/2023/06/27/python-get-first-item-collection-ignore-rest/[take the first element from a Python list^]. 
Afterward I was chatting to my new rubber duck, ChatGPT, which suggested the `next` function on an iterator as an alternative approach.
And so that's what we're going to explore in this blog post.

The `next` function gets the first value from an iterator and optionally returns a provided default value if the iterator is empty.
Let's try it out on our example:

[source, python]
----
values = ["Michael", "Ryan", "Karin", "Mark", "Jennifer", "Will"]
print(next(iter(values)))
----

.Output
[source, text]
----
Michael
----

That works well.
We could extract the other values as well if we wanted:

[source, python]
----
values = iter(["Michael", "Ryan", "Karin", "Mark", "Jennifer", "Will"])
print(next(values))
print(next(values))
print(next(values))
----

.Output
[source, text]
----
Michael
Ryan
Karin
----

And the empty case?

[source, python]
----
values = []
print(next(iter(values)))
----

.Output
[source, text]
----
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
----

The `StopIteration` exception gets thrown if you try to read from an iterator that's been exhausted.
We created an empty iterator, so that was to be expected.

Let's instead pass in a default value:

[source, python]
----
values = []
print(next(iter(values), None))
----

.Output
[source, text]
----
None
----

This time it returns the default value instead of throwing the exception and I think this is probably a nicer solution than the one we ended up with in the last blog post, which was as follows:

[source, python]
----
values = []
first, *_ = values if len(values) > 0 else [None]
----
Hugging Face: Using `max_length`'s default (20) to control the generation length. This behaviour is deprecated
Date: 2023-06-19 02:44:37
In this post, we'll learn how to set the maximum token length when using the google/flan-t5-large model in Hugging Face.

I've been trying out some of the https://huggingface.co/[Hugging Face^] tutorials and came across an interesting warning message while playing around with the https://huggingface.co/google/flan-t5-large#usage[google/flan-t5-large model^].
In this blog post, we'll learn how to get rid of that warning.

I was running a variation of the getting started example:

[source, python]
----

from transformers import T5Tokenizer, T5ForConditionalGeneration

tokenizer = T5Tokenizer.from_pretrained("google/flan-t5-large")
model = T5ForConditionalGeneration.from_pretrained("google/flan-t5-large")

input_text = "Who is the UK Prime Minister? Explain step by step"
input_ids = tokenizer(input_text, return_tensors="pt").input_ids

outputs = model.generate(input_ids)
print(tokenizer.decode(outputs[0]))
----

The output from running this fragment of code is shown below:

[source, text]
----
<pad>The Prime Minister of the United Kingdom is Theresa May. Theresa May is the
----

To be fair, we have had a lot of Prime Ministers over the last few years, but it hasn't been Theresa May for several years!
In any case, I also got the following error message when it executed the `outputs =` line:

[source, text]
----
/Users/markhneedham/projects/docs-bot/env/lib/python3.11/site-packages/transformers/generation/utils.py:1353: UserWarning: Using `max_length`'s default (20) to control the generation length. This behaviour is deprecated and will be removed from the config in v5 of Transformers -- we recommend using `max_new_tokens` to control the maximum length of the generation.
  warnings.warn(
----

I'm not actually setting a maximum length, but we seem to end up down this code path if no maximum is set.
You can also see that the output is chopped off. 
We can fix that by setting the parameter `max_new_tokens`:

[source, python]
----
outputs = model.generate(input_ids, max_new_tokens=1000)
print(tokenizer.decode(outputs[0]))
----

The new output is shown below:

[source, text]
----
<pad> The Prime Minister of the United Kingdom is Theresa May. Theresa May is the wife of David Cameron. David Cameron is the Prime Minister of the United Kingdom. So, the final answer is Theresa May.

----

The accuracy is pretty bad, but at least it's not chopping off the output anymore.
Creating LinkedIn Carousel/Slides
Date: 2023-06-08 02:44:37
In this post we'll learn how to create a LinkedIn carousel using Google Slides and canva.com

If you've been using LinkedIn recently, you've likely seen those posts where people post slides in a kind of carousel that you can horizontally scroll.
I wanted to create one to explain Apache Pinot's Upserts feature, but I wasn't sure how to create one.

Since it's 2023, I started by asking ChatGPT:

.ChatGPT doesn't know about LinkedIn Carousel
image::{{<siteurl>}}/uploads/2023/06/chatgpt-carousel.png[]

Unfortunately ChatGPT doesn't know how to do it, but https://www.linkedin.com/in/harrison-avisto-42996077/[Harrison Avisto^] does and was happy to teach me.

The basic idea is that you need to upload a PDF when you're creating your post, as showing in the animation below:

.Uploading a PDF
image::{{<siteurl>}}/uploads/2023/06/linkedin-post.gif[]

So the next question is, how do you go about creating those PDFs?
I'm sure there are lots of ways to do this, but I've come across two that I think are pretty accessible.

The first is to create a 'LinkedIn Carousel' using https://www.canva.com/[canva.com] and you can do this on a free account.

.A LinkedIn Carousel on canva.com
image::{{<siteurl>}}/uploads/2023/06/carousel-canvas.png[]

You can then create a bunch of images and then download as PDF, as shown below:

.Download as PDF on canva.com
image::{{<siteurl>}}/uploads/2023/06/canva-pdf.png[]

If you don't want to use canva, you can do a similar thing with Google Slides.
Create a new slides document as normal and then go to `File > Page Setup` and change the dimensions of the slide/canvas:

.Google Slides Canvas size
image::{{<siteurl>}}/uploads/2023/06/google-slides.png[]

Again, create slides like normal and export as PDF when you're done.

And here's the one I made:

++++
<iframe src="https://www.linkedin.com/embed/feed/update/urn:li:ugcPost:7072557847632306176" height="640" width="504" frameborder="0" allowfullscreen="" title="Embedded post"></iframe>
++++
Kafka/Kubernetes: Failed to resolve: nodename nor servname provided, or not known
Date: 2023-06-06 02:44:37
In this post we'll learn how to work around a problem when trying to write from the host OS to Kafka running inside Kubernetes.

I've been trying out the https://docs.pinot.apache.org/basics/getting-started/kubernetes-quickstart[Running Pinot in Kubernetes^] tutorial and ran into a problem trying to write data to Kafka.
In this blog we'll explore how I got around that problem.

I'm using Helm with Kubernetes and started a Kafka service by running the following:

[source, bash]
----
helm repo add kafka https://charts.bitnami.com/bitnami
helm install -n pinot-quickstart kafka kafka/kafka --set replicas=1,zookeeper.image.tag=latest
----

I waited until the service had started and then ran the following command to port forward the Kafka service's port 9092 to port 9092 on my host OS:

[source, bash]
----
kubectl port-forward service/kafka-headless 9092:9092 -n pinot-quickstart
----

I then ran a script that attempted to write some data into Kafka:

[source, bash]
----
python datagen.py --sleep 0.0001 2>/dev/null |
jq -cr --arg sep ø '[.uuid, tostring] | join($sep)' |
kcat -P -b localhost:9092 -t events -Kø
----

This hung for a few seconds before throwing the following error:

.Output
[source, text]
----
%3|1685969550.862|FAIL|rdkafka#producer-1| [thrd:kafka-0.kafka-headless.pinot-quickstart.svc.cluster.local:9092/]: kafka-0.kafka-headless.pinot-quickstart.svc.cluster.local:9092/0: Failed to resolve 'kafka-0.kafka-headless.pinot-quickstart.svc.cluster.local:9092': nodename nor servname provided, or not known (after 5002ms in state CONNECT)
----

I'd assumed that all I needed to do was map the port, but it seems like it tries to resolve the hostname, which isn't known by the host OS.
A workaround for this issue is to https://github.com/bitnami/charts/issues/1021[update your `/etc/hosts`^] file:

```
127.0.0.1 kafka-0.kafka-headless.pinot-quickstart.svc.cluster.local
```

If your Kafka has a different host name, be sure to change the second argument appropriately.
LangChain: 1 validation error for LLMChain - value is not a valid dict (type=type_error.dict)
Date: 2023-06-23 02:44:37
In this post we'll learn about an error message when passing the wrong GPT4All model to LangChain.

I surely can't be the first to make the mistake that I'm about to describe and I expect I won't be the last!
I'm still swimming in the LLM waters and I was trying to get GPT4All to play nicely with https://python.langchain.com/docs/get_started/introduction.html[LangChain^].

I wrote the following code to create an LLM chain in LangChain so that every question would use the same prompt template:

[source, python]
----
from langchain import PromptTemplate, LLMChain
from gpt4all import GPT4All

llm = GPT4All(
    model_name="ggml-gpt4all-j-v1.3-groovy",
    model_path="/Users/markhneedham/Library/Application Support/nomic.ai/GPT4All/"
)

template = """
You are a friendly chatbot assistant that responds in a conversational
manner to users questions. Keep the answers short, unless specifically
asked by the user to elaborate on something.

Question: {question}

Answer:"""
prompt = PromptTemplate(template=template, input_variables=["question"])

llm_chain = LLMChain(prompt=prompt, llm=llm)
----

When I ran this code, I got the following error:

.Output
[source, text]
----
Traceback (most recent call last):
  File "/Users/markhneedham/projects/docs-bot/gpt4all_chain.py", line 26, in <module>
    llm_chain = LLMChain(prompt=prompt, llm=llm)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/markhneedham/projects/docs-bot/env/lib/python3.11/site-packages/langchain/load/serializable.py", line 65, in __init__
    super().__init__(**kwargs)
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for LLMChain
llm
  value is not a valid dict (type=type_error.dict)
----

It took me a long time to work out that the issue is that the value we've passed in as `llm` is invalid as it doesn't extend LangChain's `BaseLanguageModel` class.
And the reason for that is that I accidentally passed in the `GPT4All` class, rather than the one with the same name from the LangChain library.
Let's fix that:

[source, python]
----
from langchain.llms import GPT4All

llm = GPT4All(
    model="/Users/markhneedham/Library/Application Support/nomic.ai/GPT4All/ggml-gpt4all-j-v1.3-groovy",
)
----

And now we can run that code without any errors!
Chroma/LangChain: 'NoneType' object has no attribute 'info'
Date: 2023-06-22 02:44:37
In this post we'll learn about an error message when using the Chroma database.

Following on from https://www.markhneedham.com/blog/2023/06/21/chroma-index-not-found-create-instance-querying/[a blog post that I wrote yesterday^] about doing similarity search with ChromaDB, I noticed an odd error message being printed as the script was exiting.
In this blog post, we'll explore what was going on.

To recap, I have the following code to find chunks of YouTube transcripts that are most similar to an input query:

.test_chroma.py
[source, python]
----
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Chroma

hf_embeddings = HuggingFaceEmbeddings(model_name='sentence-transformers/all-MiniLM-L6-v2')
store = Chroma(collection_name="transcript", persist_directory="db", embedding_function=hf_embeddings)

result = store.similarity_search("Who is Tim Berglund?", top_n=2)
for row in result:
  print(row)
----

When I run this script by typing `python test_chroma.py`, after the similar documents are printed, I get the following exception:

.Output
[source, text]
----
Exception ignored in: <function PersistentDuckDB.__del__ at 0x2abfe39c0>
Traceback (most recent call last):
  File "/Users/markhneedham/projects/docs-bot/env/lib/python3.11/site-packages/chromadb/db/duckdb.py", line 445, in __del__
AttributeError: 'NoneType' object has no attribute 'info'
----

I came across the following https://github.com/chroma-core/chroma/issues/364[GitHub thread^] that was discussing the issue and one suggested fix was to set `store = None` at the end of the script:

[source, python]
----
store = None
----

This did indeed work, but further down the thread it was suggested that the bug is fixed in the latest version of Chroma.
I checked my version:

[source, bash]
----
pip freeze | grep chroma
----

.Output
[source, text]
----
chromadb==0.3.21
----

A https://pypi.org/project/chromadb/[quick check on PyPi^] revealed that the latest version is actually 0.3.26, so let's update our version:


[source, bash]
----
pip install --upgrade chromadb
----

Let's check our version:

[source, bash]
----
pip freeze | grep chroma
----

.Output
[source, text]
----
chromadb==0.3.26
----

If I delete the `store = None` line and re-run the script again, the issue has been fixed and I don't get an error message.