Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

📅 Calendar Scheduling Agent

An AI agent that schedules calendar events using natural language. Built with Node.js + React. Works with Ollama (local) or Anthropic Claude (production).


Quick Start

# 1. Pull model (Ollama)
ollama pull qwen2.5-coder:7b
ollama serve

# 2. Server
cd server && npm install
cp .env.example .env
npm run dev       # → http://localhost:3001

# 3. Client
cd client && npm install
npm run dev       # → http://localhost:5173

How It Works — Full Detailed Flow

What You Type

"Schedule a team standup tomorrow at 10am to 10:30am"

Step 1 — System Prompt (sent every turn, never changes)

You are a calendar scheduling assistant.
Today's date is 2026-07-25.

When a user wants to schedule a meeting, you MUST:
1. Extract the EXACT date, start_time, end_time and title
2. Call check_availability with REAL extracted values
3. If available, call schedule_event with the same REAL values
4. Confirm success with a friendly message

IMPORTANT:
- Always use real dates in YYYY-MM-DD format (e.g. 2026-07-26)
- Always use real times in HH:MM format (e.g. 10:00)
- If user says "tomorrow", calculate from today: 2026-07-25
- Never pass placeholder values like "YYYY-MM-DD" or "00:00"

Step 2 — Tool Definitions (sent every turn, never changes)

[
  {
    "name": "check_availability",
    "description": "Check if a time slot is available on the calendar",
    "input_schema": {
      "type": "object",
      "properties": {
        "date":       { "type": "string", "description": "Date in YYYY-MM-DD" },
        "start_time": { "type": "string", "description": "Start time HH:MM" },
        "end_time":   { "type": "string", "description": "End time HH:MM" }
      },
      "required": ["date", "start_time", "end_time"]
    }
  },
  {
    "name": "schedule_event",
    "description": "Schedule a calendar event after availability confirmed",
    "input_schema": {
      "type": "object",
      "properties": {
        "title":      { "type": "string" },
        "date":       { "type": "string" },
        "start_time": { "type": "string" },
        "end_time":   { "type": "string" },
        "attendees":  { "type": "array", "items": { "type": "string" } }
      },
      "required": ["title", "date", "start_time", "end_time"]
    }
  }
]

Step 3 — LLM Request (Turn 1)

What we send to the LLM:

{
  "model": "claude-sonnet-4-6",
  "max_tokens": 1024,
  "system": "...system prompt above...",
  "tools": [...tool definitions above...],
  "messages": [
    {
      "role": "user",
      "content": "Schedule a team standup tomorrow at 10am to 10:30am"
    }
  ]
}

Step 4 — LLM Response (Turn 1)

LLM decides to check availability first:

{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "text",
      "text": "I'll check if that time slot is available for you."
    },
    {
      "type": "tool_use",
      "id": "tool_abc123",
      "name": "check_availability",
      "input": {
        "date":       "2026-07-26",
        "start_time": "10:00",
        "end_time":   "10:30"
      }
    }
  ]
}

stop_reason: "tool_use" → loop continues


Step 5 — Tool Call: check_availability

Your code executes the tool (not the LLM):

Input:
  date       : "2026-07-26"
  start_time : "10:00"
  end_time   : "10:30"

Result:
  available  : true
  slot       : "10:00–10:30"
  date       : "2026-07-26"
  message    : "Time slot is available"

Step 6 — LLM Request (Turn 2)

We send the full conversation history back to LLM:

{
  "model": "claude-sonnet-4-6",
  "system": "...same system prompt...",
  "tools": [...same tools...],
  "messages": [
    {
      "role": "user",
      "content": "Schedule a team standup tomorrow at 10am to 10:30am"
    },
    {
      "role": "assistant",
      "content": [
        { "type": "text", "text": "I'll check if that time slot is available." },
        { "type": "tool_use", "id": "tool_abc123",
          "name": "check_availability",
          "input": { "date": "2026-07-26", "start_time": "10:00", "end_time": "10:30" }
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "tool_abc123",
          "content": "{\"available\":true,\"slot\":\"10:00–10:30\",\"date\":\"2026-07-26\"}"
        }
      ]
    }
  ]
}

Step 7 — LLM Response (Turn 2)

LLM sees slot is available, decides to book it:

{
  "stop_reason": "tool_use",
  "content": [
    {
      "type": "tool_use",
      "id": "tool_def456",
      "name": "schedule_event",
      "input": {
        "title":      "Team Standup",
        "date":       "2026-07-26",
        "start_time": "10:00",
        "end_time":   "10:30",
        "attendees":  []
      }
    }
  ]
}

stop_reason: "tool_use" → loop continues


Step 8 — Tool Call: schedule_event

Your code executes the tool:

Input:
  title      : "Team Standup"
  date       : "2026-07-26"
  start_time : "10:00"
  end_time   : "10:30"
  attendees  : []

Result:
  success        : true
  event_id       : "EVT-1753401600000"
  title          : "Team Standup"
  date           : "2026-07-26"
  time           : "10:00–10:30"
  calendar_link  : "https://calendar.example.com/event/EVT-1753401600000"

Step 9 — LLM Request (Turn 3)

Full conversation history sent again — now with both tool results:

{
  "model": "claude-sonnet-4-6",
  "system": "...same system prompt...",
  "tools": [...same tools...],
  "messages": [
    { "role": "user",      "content": "Schedule a team standup tomorrow at 10am to 10:30am" },
    { "role": "assistant", "content": [ ...check_availability tool_use... ] },
    { "role": "user",      "content": [ ...check_availability tool_result... ] },
    { "role": "assistant", "content": [ ...schedule_event tool_use... ] },
    {
      "role": "user",
      "content": [
        {
          "type": "tool_result",
          "tool_use_id": "tool_def456",
          "content": "{\"success\":true,\"event_id\":\"EVT-1753401600000\",\"title\":\"Team Standup\"}"
        }
      ]
    }
  ]
}

Step 10 — LLM Response (Turn 3) — Final

LLM sees both tools succeeded, generates friendly confirmation:

{
  "stop_reason": "end_turn",
  "content": [
    {
      "type": "text",
      "text": "Your Team Standup has been scheduled! ✅\n\n📅 Date : Sunday, July 26, 2026\n🕙 Time : 10:00 AM – 10:30 AM\n🔗 Link : https://calendar.example.com/event/EVT-1753401600000\n\nLet me know if you need any changes!"
    }
  ]
}

stop_reason: "end_turn" → loop exits → response shown to user


The Agentic Loop — Summary

┌─────────────────────────────────────────────────────────┐
│                     USER INPUT                          │
│        "Schedule standup tomorrow at 10am"              │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              SYSTEM PROMPT + TOOLS + MESSAGE            │
│                   → sent to LLM                         │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                LLM TURN 1 RESPONSE                      │
│           stop_reason: "tool_use"                       │
│           → tool: check_availability                    │
│           → input: { date, start_time, end_time }       │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              YOUR CODE EXECUTES TOOL                    │
│              check_availability()                       │
│              → result: { available: true }              │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│        FULL HISTORY + TOOL RESULT → sent to LLM         │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                LLM TURN 2 RESPONSE                      │
│           stop_reason: "tool_use"                       │
│           → tool: schedule_event                        │
│           → input: { title, date, start_time, end_time }│
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│              YOUR CODE EXECUTES TOOL                    │
│              schedule_event()                           │
│              → result: { success: true, event_id }      │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│        FULL HISTORY + TOOL RESULT → sent to LLM         │
└─────────────────────────┬───────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                LLM TURN 3 RESPONSE                      │
│           stop_reason: "end_turn" ← LOOP EXITS          │
│           → "Your standup is booked! ✅"                │
└─────────────────────────────────────────────────────────┘

Key Concepts

Concept Explanation
stop_reason: "tool_use" LLM wants to call a tool — loop continues
stop_reason: "end_turn" LLM is done — loop exits, show final response
messages[] Grows every turn — this IS the agent's memory
System prompt Sent every turn unchanged — defines agent behavior
Tools Sent every turn unchanged — LLM picks when to call
executeTool() YOUR code calls real APIs — LLM never calls directly

Project Structure

calendar-agent/
  ├── server/
  │   ├── index.js        # Express server (port 3001)
  │   ├── agent.js        # Agentic loop + provider adapter
  │   ├── logger.js       # Session logger → logs/*.txt
  │   └── .env.example    # Copy to .env
  └── client/
      └── src/
          └── App.jsx     # React UI — shows each agent step

Switch Provider

# server/.env
LLM_PROVIDER=ollama       # local dev (default)
LLM_PROVIDER=anthropic    # production

ANTHROPIC_API_KEY=sk-ant-xxxx
OLLAMA_MODEL=qwen2.5-coder:7b

No code changes needed — one env variable switches everything.


Logs

Every session creates a log file in server/logs/:

logs/session-2026-07-25T10-30-00.txt

Contains: system prompt, user input, every LLM request, every LLM decision, every tool call, every tool result, and the final response — in order.

About

AI agent that schedules calendar events using natural language — agentic loop, tool use, Ollama + Anthropic Claude

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages