DEV Community

Süleyman İbiş
Süleyman İbiş

Posted on

I built an open-source Python Mini SDK for Google Gemini from scratch. Here's everything I learned

I built an open-source Python Mini SDK for Google Gemini from scratch 🧛

I'm a computer engineering student from Turkey, and over the past 5 days
I built Dracula. It's an open-source Python Mini SDK for Google Gemini AI.

I started this project because I wanted to learn how real Python libraries
are built, published, and maintained. What started as a simple wrapper
quickly grew into a full Mini SDK with features I'm really proud of.

In this article, I'll share everything I learned along the way.


🚀 What is Dracula?

Dracula is a Python Mini SDK for Google Gemini that makes it easy to
add AI to any Python project. It has a clean API, full async support,
function calling, and much more.

Install it with:

pip install dracula-ai
Enter fullscreen mode Exit fullscreen mode

Basic usage:

from dracula import Dracula

ai = Dracula(api_key="your-api-key")
response = ai.chat("Hello, who are you?")
print(response)
Enter fullscreen mode Exit fullscreen mode

🛠️ The Coolest Feature: Function Calling

The most powerful feature of Dracula is the @tool decorator system.
You can give Gemini access to any Python function, and it will
automatically decide when and how to call it based on the user's message.

from dracula import Dracula, tool

@tool(description="Get the current weather for a city")
def get_weather(city: str) -> str:
    # In real life this would call a weather API
    return f"It's 25°C and sunny in {city}"

@tool(description="Search the web for information")
def search_web(query: str) -> str:
    return f"Search results for: {query}"

ai = Dracula(
    api_key="your-api-key",
    tools=[get_weather, search_web]
)

# Gemini automatically calls get_weather("Istanbul")! 🤯
response = ai.chat("What's the weather in Istanbul?")
print(response)
# "The weather in Istanbul is currently 25°C and sunny!"
Enter fullscreen mode Exit fullscreen mode

The @tool decorator automatically extracts the function's name,
parameters, and type hints and converts them into a schema that
Gemini can understand. No manual schema writing needed!

You can also handle tool calls manually if you want full control:

result = ai.chat("What's the weather in Ankara?", auto_call=False)
if result.requires_tool_call:
    print(f"Tool: {result.tool_name}")   # get_weather
    print(f"Args: {result.tool_args}")   # {"city": "Ankara"}
Enter fullscreen mode Exit fullscreen mode

The Hardest Bug I Fixed — Thought Signatures

When I first implemented function calling, I kept getting this error:

400 INVALID_ARGUMENT: Function call is missing a thought_signature
Enter fullscreen mode Exit fullscreen mode

After reading the Google documentation, I discovered that Gemini 3
models require thought signatures — encrypted tokens that preserve
the model's reasoning between tool calls. When I manually built the
conversation history, the thought signatures were lost.

The fix was to use Gemini's built-in persistent chat session which
handles thought signatures automatically:

# Wrong  — loses thought signatures
response = self.client.models.generate_content(
    model=self.model_name,
    contents=history,
    config=config
)

# Correct  — handles thought signatures automatically
self._chat_session = self.client.chats.create(
    model=self.model_name,
    config=config
)
response = self._chat_session.send_message(message)
Enter fullscreen mode Exit fullscreen mode

This was the most satisfying bug to fix in the entire project!


Async Support with AsyncDracula

For async applications like Discord bots and FastAPI, Dracula has
a separate AsyncDracula class with full async support:

import asyncio
from dracula import AsyncDracula, tool

@tool(description="Get the weather for a city")
async def get_weather(city: str) -> str:
    return f"25°C and sunny in {city}"

async def main():
    async with AsyncDracula(
        api_key="your-api-key",
        tools=[get_weather]
    ) as ai:
        response = await ai.chat("What's the weather in Istanbul?")
        print(response)

        async for chunk in ai.stream("Tell me a story."):
            print(chunk, end="", flush=True)

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

This design was inspired by httpx which has httpx.Client for sync
and httpx.AsyncClient for async — keeping the two completely separate
makes the API much cleaner.


Conversation Memory

Dracula automatically remembers the conversation history:

ai = Dracula(api_key="your-api-key")
ai.chat("My name is Ahmet.")
response = ai.chat("What is my name?")
print(response)  # It remembers! ✅

# Save and load history
ai.save_history("conversation.json")
ai.load_history("conversation.json")
Enter fullscreen mode Exit fullscreen mode

Professional Logging System

One of the things I'm most proud of is the logging system.
By default it's completely silent, but developers can turn it on:

# Simple logging
ai = Dracula(api_key="your-api-key", logging=True)

# With file rotation — creates new log file when size limit is reached
ai = Dracula(
    api_key="your-api-key",
    logging=True,
    log_file="dracula.log",
    log_max_bytes=1 * 1024 * 1024,  # 1MB per file
    log_backup_count=3               # keep 3 backups
)
Enter fullscreen mode Exit fullscreen mode

I used Python's built-in RotatingFileHandler for this — when the
log file reaches the size limit, it automatically creates a new one
and keeps the specified number of backups.


Role Playing Mode

Dracula comes with 6 built-in personas:

ai.set_persona("pirate")
print(ai.chat("Hello, who are you?"))
# Arrr, I be a fearsome pirate! 🏴‍☠️

print(ai.list_personas())
# ['assistant', 'pirate', 'chef', 'shakespeare', 'scientist', 'comedian']
Enter fullscreen mode Exit fullscreen mode

GeminiModel Enum

Instead of typing model names as strings which can cause typos,
Dracula provides a GeminiModel enum:

from dracula import Dracula, GeminiModel

ai = Dracula(api_key="your-api-key", model=GeminiModel.FLASH)
ai = Dracula(api_key="your-api-key", model=GeminiModel.PRO)

# Discover all available models in real time
print(ai.list_available_models())
Enter fullscreen mode Exit fullscreen mode

CLI Tool

Dracula comes with a built-in CLI tool:

dracula chat "Hello!"
dracula chat "Tell me a joke" --persona comedian
dracula chat "Merhaba" --language Turkish --stream
dracula list-personas
dracula stats
Enter fullscreen mode Exit fullscreen mode

71 Passing Tests

One of the most important things I learned was how to write proper
unit tests. Every feature has tests that check both the happy path
and error cases:

 Final Results: 71 passed, 0 failed
Enter fullscreen mode Exit fullscreen mode

Writing tests forced me to think about edge cases I never would
have considered otherwise. For example, I discovered that
validate_language wasn't being called in __init__
the tests caught it immediately!


What I Learned

Building Dracula taught me more than any tutorial ever could:

  • How to structure and publish a real Python library to PyPI
  • How async/await works in practice
  • How decorators work internally
  • How to handle Google's thought signatures in function calling
  • How RotatingFileHandler works for log rotation
  • How pathlib makes file handling cross-platform
  • How context managers work (__enter__ and __exit__)
  • How chainable methods work (return self)
  • How semantic versioning works in practice
  • How to write 71 unit tests and fix every failure
  • How to stay motivated when you want to give up

Try It Out!

pip install dracula-ai
Enter fullscreen mode Exit fullscreen mode

If you find it useful, a ⭐ on GitHub would mean the world to me
as a CS student!

I'd love to hear your feedback, suggestions, or criticism in the
comments. What features would you like to see next? 🧛

Top comments (0)