Skip to content

feat(EVO-2170): add edit_calendar_event tool (edit / reschedule) - #43

Merged
gomessguii merged 4 commits into
developfrom
fix/EVO-2170-edit-event-tool
Jul 20, 2026
Merged

feat(EVO-2170): add edit_calendar_event tool (edit / reschedule)#43
gomessguii merged 4 commits into
developfrom
fix/EVO-2170-edit-event-tool

Conversation

@pastoriniMatheus

@pastoriniMatheus pastoriniMatheus commented Jul 18, 2026

Copy link
Copy Markdown

EVO-2170 — nova tool edit_calendar_event (editar/remarcar reunião)

PR independente, base develop (completa o toolset junto da #42 cancel).

O que faz

  • Localiza o evento por event_id (events().get) ou pela janela start_date/end_date (via client.check_availability) + filtro title.
  • Altera só o pedido: new_start_date/new_end_date (remarcar), new_title, new_descriptionpatch só dos campos informados. Exige ≥1 alteração.
  • Preserva a duração quando só new_start_date é dado.
  • service.events().patch(sendUpdates="all") (notifica). Desambiguação em >1; mensagem informativa em 0 (não alucina). Recarrega credenciais do banco quando sanitizadas.

Testes (tests/unit/test_google_calendar_edit.py, 8) — 8 passed

remarca preservando duração · só título · start+end explícitos · sem alteração→erro · >1→desambiguação · 0→informa · event_id via get · credenciais sanitizadas recarregam.

Validado em produção

14:00–15:00 → remarcado para 16:30 → Google confirmou 16:30–17:30 (1h preservada) + título trocado.

Relação com a #42 (cancel)

As duas são independentes (cada uma adiciona seu próprio arquivo de tool). A única sobreposição é a linha de log que lista as tools registradas em tool_builder.py — quando a segunda mergear, é um conflito trivial de 1 linha (juntar os nomes). Nenhuma depende da outra.

Follow-up

create/check/cancel/edit usam calendário primary, não o selectedCalendarId da UI — bug separado.

Summary by Sourcery

Add a Google Calendar tool for editing or rescheduling existing events and wire it into the agent toolset.

New Features:

  • Introduce an edit_calendar_event Google Calendar tool that updates existing meetings’ time, title, and description while handling ambiguous or missing matches and credential reloading.

Tests:

  • Add unit tests covering rescheduling behavior, title-only updates, explicit time changes, error paths, multi-match disambiguation, event_id lookup, and reloading sanitized credentials.

@sourcery-ai

sourcery-ai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new Google Calendar edit/reschedule tool (edit_calendar_event) to the ADK toolset, wires it into the tool builder, and covers it with unit tests including credential reloading and event-disambiguation behavior.

Sequence diagram for the new edit_calendar_event tool behavior

sequenceDiagram
    actor Agent
    participant edit_calendar_event
    participant GoogleCalendarClient
    participant Postgres
    participant GoogleCalendarAPI

    Agent->>edit_calendar_event: call with start_date end_date optional_changes

    edit_calendar_event->>edit_calendar_event: _credentials_have_secrets
    alt credentials sanitized
        edit_calendar_event->>Postgres: _load_full_credentials_from_db
        Postgres-->>edit_calendar_event: full_credentials
        edit_calendar_event->>edit_calendar_event: _credentials_have_secrets
        alt still incomplete
            edit_calendar_event-->>Agent: status error (incomplete credentials)
        end
    end

    edit_calendar_event->>GoogleCalendarClient: get_calendar_service
    GoogleCalendarClient-->>edit_calendar_event: service

    alt event_id provided
        edit_calendar_event->>GoogleCalendarAPI: events().get
        GoogleCalendarAPI-->>edit_calendar_event: target_event
    else search by time range
        edit_calendar_event->>GoogleCalendarClient: check_availability
        GoogleCalendarClient-->>edit_calendar_event: events
        alt zero events
            edit_calendar_event-->>Agent: status error (no meeting found)
        else multiple events
            edit_calendar_event-->>Agent: status needs_clarification with _event_label list
        else single event
            edit_calendar_event->>edit_calendar_event: build target_event
        end
    end

    edit_calendar_event->>edit_calendar_event: build patch body
    edit_calendar_event->>GoogleCalendarAPI: events().patch sendUpdates=all
    GoogleCalendarAPI-->>edit_calendar_event: updated_event

    edit_calendar_event-->>Agent: status success with _event_label and link
Loading

File-Level Changes

Change Details Files
Introduce edit_calendar_event Google Calendar tool that edits/reschedules existing events with precise field patching and robust event lookup.
  • Create create_edit_event_tool factory that builds an async edit_calendar_event FunctionTool using GoogleCalendarClient
  • Implement event lookup by event_id via events().get or by time window via client.check_availability with optional title filtering and disambiguation handling
  • Build patch body to update only provided fields (start/end, title, description), preserving original duration when only new_start_date is supplied
  • Handle missing/invalid inputs (no changes, invalid dates, no matches, Google API errors) and return structured status/messages for the agent
src/services/adk/tools/google_calendar/edit_event.py
Ensure credentials used by the edit tool contain OAuth secrets by optionally reloading full credentials from the database when sanitized configs are supplied.
  • Add helper to detect whether credentials contain required OAuth secrets
  • Add helper to load full Google Calendar credentials directly from Postgres using agent_id and provider google_calendar_credentials
  • Fallback to DB-loaded credentials when the integration config credentials are sanitized, otherwise return a clear error about incomplete credentials
src/services/adk/tools/google_calendar/edit_event.py
Register the new edit_event tool in the Google Calendar toolset so it is available to agents.
  • Export create_edit_event_tool from google_calendar package init
  • Append the edit_event tool in build_tools using agent_id, calendar_config, credentials_config, and db
  • Update logging to mention the new edit_event tool alongside check_availability and create_event
src/services/adk/tool_builder.py
src/services/adk/tools/google_calendar/__init__.py
Add unit tests validating the edit_calendar_event behavior, including rescheduling logic, partial updates, event lookup, and credential reloading.
  • Test rescheduling with only new_start_date preserves original duration and leaves title untouched
  • Test title-only updates avoid modifying start/end times
  • Test explicit new_start_date and new_end_date are used as-is
  • Test validation error when no change fields are provided and patch is not called
  • Test multiple and zero matches behaviors (needs_clarification vs informative error)
  • Test event_id path uses events().get and skips availability search
  • Test sanitized credentials trigger DB reload and then use full credentials
tests/unit/test_google_calendar_edit.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The _load_full_credentials_from_db helper bypasses the injected db dependency and opens a new psycopg2 connection using an env DSN, which makes the tool harder to test and configure; consider reusing the existing DB abstraction / db handle instead of creating a raw connection here.
  • The async edit_calendar_event function performs blocking operations (psycopg2 connect/query and Google API calls) directly, which may block the event loop under load; if this pattern isn't already established, consider offloading these to a sync context or threadpool helper for better async behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_load_full_credentials_from_db` helper bypasses the injected `db` dependency and opens a new psycopg2 connection using an env DSN, which makes the tool harder to test and configure; consider reusing the existing DB abstraction / `db` handle instead of creating a raw connection here.
- The async `edit_calendar_event` function performs blocking operations (psycopg2 connect/query and Google API calls) directly, which may block the event loop under load; if this pattern isn't already established, consider offloading these to a sync context or threadpool helper for better async behavior.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…ting)

Completes the calendar toolset: the agent could create and cancel, but not edit /
reschedule an existing meeting.

- edit_event.py: new edit_calendar_event tool. Locates the event by event_id
  (events().get) or by the search window (client.check_availability) + optional
  title. Applies only the provided fields (new_start_date/new_end_date to
  reschedule, new_title, new_description) via events().patch(sendUpdates="all").
  When only new_start_date is given, the original duration is preserved. Requires
  at least one change; disambiguates on multiple matches; informative message on
  none. Same sanitized-credentials DB reload and default calendar as the others.
- __init__.py / tool_builder.py: register the tool (log lists edit_event).
- tests: reschedule preserves duration; title-only; explicit start+end; no-change
  error; multiple -> disambiguation; none -> informative; event_id via get;
  sanitized creds reloaded.

Validated in the client's production (14:00-15:00 -> rescheduled to 16:30 -> Google
confirmed 16:30-17:30, 1h preserved, title changed).

Independent of EVO-2169 (#42, cancel): both add their own tool file. When both
merge, the only overlap is the tools-registered log line — a trivial resolution.
@pastoriniMatheus
pastoriniMatheus force-pushed the fix/EVO-2170-edit-event-tool branch from eb376cf to 92f7ad0 Compare July 18, 2026 02:16
@pastoriniMatheus
pastoriniMatheus changed the base branch from fix/EVO-2169-cancel-event-tool to develop July 18, 2026 02:16

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The _load_full_credentials_from_db helper bypasses the existing db dependency and opens a new psycopg2 connection from an env var each time; consider wiring this through the existing DB abstraction (or the injected db parameter) so connection management, configuration, and pooling remain consistent with the rest of the service.
  • The ToolContext parameter of edit_calendar_event is currently unused; either remove it from the signature or leverage it (e.g., for logging/trace information) to avoid confusion about its purpose.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_load_full_credentials_from_db` helper bypasses the existing `db` dependency and opens a new psycopg2 connection from an env var each time; consider wiring this through the existing DB abstraction (or the injected `db` parameter) so connection management, configuration, and pooling remain consistent with the rest of the service.
- The `ToolContext` parameter of `edit_calendar_event` is currently unused; either remove it from the signature or leverage it (e.g., for logging/trace information) to avoid confusion about its purpose.

## Individual Comments

### Comment 1
<location path="src/services/adk/tools/google_calendar/edit_event.py" line_range="140-144" />
<code_context>
+                    "message": "Informe ao menos uma alteração (novo horário, novo título ou nova descrição).",
+                }
+
+            # Load full credentials from DB when the ones passed are sanitized.
+            effective_credentials = credentials_config
+            if not _credentials_have_secrets(effective_credentials):
+                full_creds = _load_full_credentials_from_db(effective_agent_id)
+                if _credentials_have_secrets(full_creds):
+                    effective_credentials = full_creds
+                    logger.info("Loaded full Google Calendar credentials from database")
</code_context>
<issue_to_address>
**issue (performance):** Blocking DB calls inside async tool can stall the event loop.

This async tool calls `_load_full_credentials_from_db` (psycopg2) directly, which will block the event loop under load. Please either load credentials before entering this async context and pass them in, run the DB call in a thread executor, or make this tool synchronous if the framework supports it.
</issue_to_address>

### Comment 2
<location path="src/services/adk/tools/google_calendar/edit_event.py" line_range="38-47" />
<code_context>
+                    body["end"] = {"dateTime": _parse(new_end_date).isoformat(), "timeZone": timezone}
+                else:
+                    # keep original duration
+                    try:
+                        o_start = _parse(target["start"].get("dateTime"))
+                        o_end = _parse(target["end"].get("dateTime"))
+                        duration = o_end - o_start
+                    except Exception:
+                        duration = timedelta(hours=1)
</code_context>
<issue_to_address>
**suggestion:** Handling all-day events when preserving original duration could be more precise.

When `dateTime` is absent for all-day events (only `start['date']`/`end['date']` present), this `try` will always fail and we fall back to a 1‑hour duration. Consider parsing the date-only fields to compute the real duration so all‑day events aren’t reduced to 1 hour when rescheduling with only `new_start_date`.
</issue_to_address>

### Comment 3
<location path="tests/unit/test_google_calendar_edit.py" line_range="140-149" />
<code_context>
+    svc.events.return_value.patch.assert_called_once()
+
+
+def test_sanitized_credentials_are_reloaded_from_db():
+    svc = _mock_service()
+    gcs = MagicMock(return_value=svc)
+    with patch(
+        "src.services.adk.tools.google_calendar.edit_event._load_full_credentials_from_db",
+        return_value=FULL_CREDS,
+    ) as load_mock, patch.object(
+        GoogleCalendarClient, "get_calendar_service", new=gcs
+    ), patch.object(
+        GoogleCalendarClient,
+        "check_availability",
+        new=AsyncMock(return_value={"status": "success", "available": False, "events": [TARGET]}),
+    ):
+        res = asyncio.run(
+            _tool(SANITIZED_CREDS)(
+                start_date="2026-07-20T00:00:00",
+                end_date="2026-07-20T23:59:59",
+                new_title="X",
+            )
+        )
+    load_mock.assert_called_once_with(AGENT_ID)
+    assert res["status"] == "success"
+    gcs.assert_called_once_with(FULL_CREDS)
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test for the case where credentials are incomplete and the DB also fails to provide full secrets

The implementation also handles the case where `_credentials_have_secrets` is still `False` after `_load_full_credentials_from_db`. Please add a test where `credentials_config` is sanitized and `_load_full_credentials_from_db` returns `None` (or creds without secrets), and assert that the tool returns `status == 'error'` with a message about incomplete/missing OAuth secrets, and that `GoogleCalendarClient.get_calendar_service` is not called. This locks in the failure behaviour and prevents regressions that might start using unusable credentials.
</issue_to_address>

### Comment 4
<location path="tests/unit/test_google_calendar_edit.py" line_range="61-68" />
<code_context>
+        )
+
+
+def test_reschedule_new_start_only_preserves_duration():
+    svc = _mock_service()
+    res = _run_search(_tool(), [TARGET], svc, new_start_date="2026-07-20T16:30:00-03:00")
+    assert res["status"] == "success"
+    body = svc.events.return_value.patch.call_args.kwargs["body"]
+    assert "16:30:00" in body["start"]["dateTime"]
+    assert "17:30:00" in body["end"]["dateTime"]  # original 1h duration preserved
+    assert "summary" not in body  # title untouched
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test that exercises the fallback duration logic for events without `dateTime` (all-day events)

Since the duration logic falls back to a hard-coded `timedelta(hours=1)` when parsing `start`/`end` fails (e.g., all-day events with only `date`), please add a test where the target event uses only `start['date']`/`end['date']` (or another unparsable structure), calls the tool with `new_start_date`, and asserts that `end` is exactly 1 hour after `start` to cover this fallback path.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +140 to +144
# Load full credentials from DB when the ones passed are sanitized.
effective_credentials = credentials_config
if not _credentials_have_secrets(effective_credentials):
full_creds = _load_full_credentials_from_db(effective_agent_id)
if _credentials_have_secrets(full_creds):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (performance): Blocking DB calls inside async tool can stall the event loop.

This async tool calls _load_full_credentials_from_db (psycopg2) directly, which will block the event loop under load. Please either load credentials before entering this async context and pass them in, run the DB call in a thread executor, or make this tool synchronous if the framework supports it.

Comment on lines +38 to +47
try:
import psycopg2

conn = psycopg2.connect(dsn)
try:
cur = conn.cursor()
cur.execute(
"SELECT config FROM evo_core_agent_integrations "
"WHERE agent_id = %s AND provider = %s LIMIT 1",
(str(agent_id), "google_calendar_credentials"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Handling all-day events when preserving original duration could be more precise.

When dateTime is absent for all-day events (only start['date']/end['date'] present), this try will always fail and we fall back to a 1‑hour duration. Consider parsing the date-only fields to compute the real duration so all‑day events aren’t reduced to 1 hour when rescheduling with only new_start_date.

Comment on lines +140 to +149
def test_sanitized_credentials_are_reloaded_from_db():
svc = _mock_service()
gcs = MagicMock(return_value=svc)
with patch(
"src.services.adk.tools.google_calendar.edit_event._load_full_credentials_from_db",
return_value=FULL_CREDS,
) as load_mock, patch.object(
GoogleCalendarClient, "get_calendar_service", new=gcs
), patch.object(
GoogleCalendarClient,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test for the case where credentials are incomplete and the DB also fails to provide full secrets

The implementation also handles the case where _credentials_have_secrets is still False after _load_full_credentials_from_db. Please add a test where credentials_config is sanitized and _load_full_credentials_from_db returns None (or creds without secrets), and assert that the tool returns status == 'error' with a message about incomplete/missing OAuth secrets, and that GoogleCalendarClient.get_calendar_service is not called. This locks in the failure behaviour and prevents regressions that might start using unusable credentials.

Comment on lines +61 to +68
def test_reschedule_new_start_only_preserves_duration():
svc = _mock_service()
res = _run_search(_tool(), [TARGET], svc, new_start_date="2026-07-20T16:30:00-03:00")
assert res["status"] == "success"
body = svc.events.return_value.patch.call_args.kwargs["body"]
assert "16:30:00" in body["start"]["dateTime"]
assert "17:30:00" in body["end"]["dateTime"] # original 1h duration preserved
assert "summary" not in body # title untouched

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a test that exercises the fallback duration logic for events without dateTime (all-day events)

Since the duration logic falls back to a hard-coded timedelta(hours=1) when parsing start/end fails (e.g., all-day events with only date), please add a test where the target event uses only start['date']/end['date'] (or another unparsable structure), calls the tool with new_start_date, and asserts that end is exactly 1 hour after start to cover this fallback path.

pastoriniMatheus pushed a commit that referenced this pull request Jul 18, 2026
…ty + create_event

The tools defaulted calendar_id to 'primary' and never read the calendar the user
picks in the UI (settings.selectedCalendarId), so the agent operated on the OAuth
account's primary calendar instead of the configured one. Resolve the calendar from
config (config selection is authoritative; fall back to the tool arg / primary) and
use it in every API call. Tests assert the selected calendar is used and that an
empty/whitespace selection falls back to primary.

Part of EVO-2171 (also applied to cancel_event #42 and edit_event #43).
Matheus Pastorini and others added 2 commits July 18, 2026 01:15
Resolve the calendar from config (selectedCalendarId, else the tool arg / primary)
so edit locates (events().get / search) and patches the event on the calendar the
event lives on, not always primary. Test asserts search + patch target the selected
calendar.
…event-tool

# Conflicts:
#	src/services/adk/tool_builder.py
#	src/services/adk/tools/google_calendar/__init__.py
@gomessguii
gomessguii merged commit 2b5a22b into develop Jul 20, 2026
4 checks passed
@gomessguii
gomessguii deleted the fix/EVO-2170-edit-event-tool branch July 20, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants