DEV Community

Isaac
Isaac

Posted on

How I Built an AI Coworker for Telex.im

So I wanted to build an AI agent that could help me generate content, and I ended up creating something pretty cool - a conversational AI coworker that lives in Telex.im and generates blog posts.

But here's the thing - it started as a newsletter generator. Yep, completely different use case. Let me tell you how it evolved.

The Newsletter Pivot 🔄

Originally, I was building this to generate newsletters. The idea was simple: give the AI a title and content, and it spits out a nicely formatted newsletter. I even had it generating infographics and returning them as artifacts with this structure:

{
  "kind": "data",
  "data": {
    "title": "My Newsletter",
    "content": "..."
  }
}
Enter fullscreen mode Exit fullscreen mode

But when I tried integrating it with Telex.im, the artifacts weren't rendering correctly. The platform just didn't know what to do with that data format, or I was probably doing it wrong (that's more likely the case). So I thought, "Why not just make it simpler?"

I pivoted to blog posts and changed the artifact format to plain text:

{
  "kind": "text",
  "text": "Here's your blog post content..."
}
Enter fullscreen mode Exit fullscreen mode

Way cleaner, and Telex.im loved it. Sometimes the simplest solution is the best one!

What I Built

An AI agent that:

  • Has natural conversations (not just one-shot queries)
  • Asks for blog title and topic step-by-step
  • Generates professional blog posts using Groq's fast LLM API
  • Remembers the entire conversation
  • Works seamlessly in Telex.im workflows

The Stack

Go: Because I wanted something fast and concurrent

LangChain Go: For managing conversations and LLM interactions

Groq: Lightning-fast inference with Llama 3.1 (seriously, 750+ tokens/second!)

JSON-RPC 2.0: Standard protocol for communication

Telex.im: Where the magic happens

How the Conversation Flows

The agent doesn't just take a prompt and generate. It actually guides you through:

Agent: "Want to create a blog post?"
You: "yes"
Agent: "What's the title?"
You: "Building AI Agents"
Agent: "What's the topic?"
You: "Go and LangChain"
Agent: "Should I proceed?"
You: "yes"
Agent: ✅ [generates full blog post]
Enter fullscreen mode Exit fullscreen mode

This conversational approach makes it feel less like using a tool and more like working with a teammate.

The Code Structure

I kept it clean with separation of concerns:

blog-generator/
├── main.go                 # Entry point
├── config/                 # API keys & settings
├── requests/                 # Request/Response types
├── services/              
│   ├── agent_service.go    # Conversation logic
│   └── blog_service.go     # Content generation
└── controllers/            # HTTP handlers
Enter fullscreen mode Exit fullscreen mode

Key Components

1. Configuration

First, load your Groq API key (grab one free at console.groq.com):

package config

import "os"

type Config struct {
    GroqAPIKey string
    Port       string
}

func Load() *Config {
    groqAPIKey := os.Getenv("GROQ_API_KEY")
    if groqAPIKey == "" {
        panic("GROQ_API_KEY not set")
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    return &Config{
        GroqAPIKey: groqAPIKey,
        Port:       port,
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Blog Service - The Content Generator

type BlogPostService struct {
    llm llms.Model
}

func NewBlogPostService(groqAPIKey string) *BlogPostService {
    llm, err := openai.New(
        openai.WithToken(groqAPIKey),
        openai.WithBaseURL("https://api.groq.com/openai/v1"),
        openai.WithModel("groq/compound"),
    )
    if err != nil {
        panic(fmt.Sprintf("failed to create blogpost service: %v", err))
    }

    return &BlogPostService{llm: llm}
}

func (s *BlogPostService) getSystemPrompt() string {
    return `GENERATE_NEWSLETTER|Title: `
}

func (s *BlogPostService) Generate(ctx context.Context, session *requests.SessionData, title string) (string, error) {
    messages := []llms.MessageContent{
        llms.TextParts(llms.ChatMessageTypeSystem, s.getSystemPrompt()),
        llms.TextParts(llms.ChatMessageTypeSystem, "You are a blog writer. Create engaging, well-structured blog posts."),
        llms.TextParts(llms.ChatMessageTypeHuman, fmt.Sprintf(`Create a cool blog post based on the following:

Title and Content: %s

Generate a well-structured blogpost with:
- A catchy headline
- An engaging introduction
-if you notice any code snippets, include it in the blog explanation
- 3-4 key points or sections with clear headings (use format "## Heading")
- A conclusion or call-to-action

Keep it concise and professional.`, title)),
    }

    memoryVars, err := session.Memory.LoadMemoryVariables(ctx, map[string]any{})
    if err == nil {
        if history, ok := memoryVars["history"].(string); ok && history != "" {
            messages = append(messages, llms.TextParts(llms.ChatMessageTypeHuman, history))
        }
    }

    messages = append(messages, llms.TextParts(llms.ChatMessageTypeHuman, title))

    response, err := s.llm.GenerateContent(ctx, messages, llms.WithTemperature(0.7))
    if err != nil {
        return "", err
    }

    return response.Choices[0].Content, nil
}

Enter fullscreen mode Exit fullscreen mode

3. Agent Service - The Brain

This is where the conversation magic happens:

type AgentService struct {
    llm      llms.Model
    blogSvc  *BlogService
    sessions map[string]*models.SessionData
    mu       sync.RWMutex  // Thread safety!
}
Enter fullscreen mode Exit fullscreen mode

The sync.RWMutex is crucial. Each HTTP request runs in its own goroutine, so we need to protect the sessions map from concurrent access issues.

Session management:

func (s *AgentService) getOrCreateSession(taskID string) *models.SessionData {
    s.mu.Lock()
    defer s.mu.Unlock()

    if session, exists := s.sessions[taskID]; exists {
        return session
    }

    // Create new session with LangChain memory
    session := &models.SessionData{
        ContextID: uuid.New().String(),
        History:   []models.HistoryMessage{},
        Memory:    memory.NewConversationBuffer(),
    }
    s.sessions[MessageID] = session
    return session
}
Enter fullscreen mode Exit fullscreen mode

The ConversationBuffer from LangChain automatically tracks the full conversation history, so the AI remembers everything that was said.

func (s *AgentService) processAIResponse(ctx context.Context, session *models.SessionData, taskID, aiResponse string) *models.TaskResult {
    // Check for generation command
    if strings.Contains(aiResponse, "GENERATE_BLOG|") {
        return s.handleBlogGeneration(ctx, session, aiResponse)
    }

    // Otherwise just return the AI's message
    return s.buildResponse(taskID, session, aiResponse)
}

func (s *AgentService) handleBlogPostGeneration(ctx context.Context, session *requests.SessionData, userText string) (string, []requests.Artifact) {


    title := userText

    session.Title = title

    blogpost, err := s.blogpostSvc.Generate(ctx, session, title)
    if err != nil {
        return fmt.Sprintf("I apologize, but I encountered an error generating the blogpost: %v\n\nWould you like to try again?", err), nil
    }

    session.BlogPost = blogpost

    blog := fmt.Sprintf("\nTitle: %s \n\n Content: %s", title, blogpost)

    artifact := requests.Artifact{
        ArtifactID: uuid.New().String(),
        Name:       "blogpost",
        Parts: []requests.ResponsePart{
            {
                Kind: "text",
                Text: blog,
            },
        },
    }

    response := "BlogPost generated successfully!"

    return response, []requests.Artifact{artifact}
}
Enter fullscreen mode Exit fullscreen mode

This separates the AI's decision-making from the implementation. Clean and modular!

Integrating with Telex.im 🚀

This is where things got really fun. I wanted to turn my agent into a proper AI coworker in Telex.im.

I followed the docs at docs.telex.im and checked out Pheonix's blog for some integration patterns. Both were super helpful!

My Telex.im Workflow

Here's the workflow config I'm using:

{
  "active": true,
  "category": "utilities",
  "description": "A workflow that creates blogposts",
  "id": "iceu-blogpost-001",
  "name": "blogpost_agent",
  "long_description": "You are a helpful assistant that provides sample BlogPosts.\n\nYour primary function is to help users generate BlogPosts from title and description they send. When responding:\n- Always ask for the blog title if none is provided\n- Ask for the content/topic description\n- Confirm the details and ask if they want to proceed\n- Keep responses concise but informative\n- Use the blogpostgeneration tool with title and content",
  "short_description": "creates blogpost from title and content given",
  "nodes": [
    {
      "id": "blogpost_agent",
      "name": "blogpost agent",
      "parameters": {},
      "position": [816, -112],
      "type": "a2a/mastra-a2a-node",
      "typeVersion": 1,
      "url": "https://newsletter-ai-coworker-i-ceu4383-amxl8p89.leapcell.dev/agent"
    }
  ],
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }  
}
Enter fullscreen mode Exit fullscreen mode

The workflow is pretty straightforward - it's a single node that points to my agent endpoint. The long_description gives Telex.im context about what my agent does and how to interact with it.

How It Works in Telex

  1. User messages the workflow in Telex.im
  2. Telex sends a JSON-RPC request to my endpoint
  3. My agent processes it and responds conversationally
  4. Telex displays the response to the user
  5. The conversation continues naturally

The cool part is that Telex maintains the taskId across messages, so my agent can track the full conversation and remember context.

Running It

# Set your API key
export GROQ_API_KEY="gsk_your_key_here"

# Run
go run main.go
Enter fullscreen mode Exit fullscreen mode

Deploy it anywhere (I used Leapcell), and you've got yourself an AI coworker!

What I Learned

1. Start simple - I spent time building infographic generation, but for my use case, plain text was all I needed.

2. Architecture matters - Separating the blog service from the agent service made it super easy to test and iterate.

3. Conversation > Commands - Making it conversational instead of a one-shot API feels way more natural.

4. LangChain is awesome - The built-in memory management saved me from writing a bunch of boilerplate.

5. Groq is fast - Seriously, the speed difference compared to other providers is wild.

The Result

Now I have an AI coworker living in Telex.im that I can message anytime like:

Me: "hey, need a blog post"

Agent: "Sure! What's the title?"

Me: "Building with Go"

Agent: "Great! What should it be about?"

Me: "Microservices patterns"

Agent: "Got it! Should I proceed?"

Me: "yes"

Agent: ✅ [Full blog post appears]

It's like having a content writer on standby. Pretty neat for when I need to quickly draft technical content!
I'll hopefully work on fully integrating it into platforms like here or hashnode so it creates the blogs automatically.
Might be a fun next step.

Resources

Want to build your own?


Have you built any AI agents? What are you using them for? Drop a comment below - I'd love to hear about your projects! 🚀

Tags: #go #ai #langchain #groq #telex

Top comments (0)