Spaces:
Configuration error
Configuration error
feat: implement memory_antigravity_sync natively in Python
Browse files- src/app.py +211 -0
src/app.py
CHANGED
|
@@ -482,6 +482,21 @@ def api_session_start():
|
|
| 482 |
except Exception as e:
|
| 483 |
return jsonify({"error": str(e)}), 400
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
@app.route("/agentmemory/session/end", methods=["POST"])
|
| 486 |
def api_session_end():
|
| 487 |
auth_err = check_auth()
|
|
@@ -1551,6 +1566,182 @@ def enrich_search_results(kv_inst, results):
|
|
| 1551 |
enriched.append(item)
|
| 1552 |
return enriched
|
| 1553 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1554 |
@app.route("/agentmemory/mcp/tools", methods=["GET"])
|
| 1555 |
def mcp_tools_list():
|
| 1556 |
auth_err = check_auth()
|
|
@@ -1794,6 +1985,19 @@ def mcp_tools_list():
|
|
| 1794 |
},
|
| 1795 |
"required": ["content", "project"]
|
| 1796 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1797 |
}
|
| 1798 |
]
|
| 1799 |
return jsonify({"tools": tools}), 200
|
|
@@ -1973,6 +2177,13 @@ def mcp_tools_call():
|
|
| 1973 |
res = functions.remember(kv, payload)
|
| 1974 |
text_out = json.dumps(res)
|
| 1975 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1976 |
else:
|
| 1977 |
return jsonify({"error": f"unknown tool: {name}"}), 400
|
| 1978 |
|
|
|
|
| 482 |
except Exception as e:
|
| 483 |
return jsonify({"error": str(e)}), 400
|
| 484 |
|
| 485 |
+
@app.route("/agentmemory/antigravity/sync", methods=["POST"])
|
| 486 |
+
def api_antigravity_sync():
|
| 487 |
+
auth_err = check_auth()
|
| 488 |
+
if auth_err:
|
| 489 |
+
return auth_err
|
| 490 |
+
try:
|
| 491 |
+
body = request.get_json(force=True) or {}
|
| 492 |
+
mode = body.get("mode") or "current_session"
|
| 493 |
+
current_convo = body.get("currentConversationId")
|
| 494 |
+
current_folder = body.get("currentFolder")
|
| 495 |
+
res = perform_antigravity_sync(mode, current_convo, current_folder)
|
| 496 |
+
return jsonify(res), 200
|
| 497 |
+
except Exception as e:
|
| 498 |
+
return jsonify({"error": str(e)}), 500
|
| 499 |
+
|
| 500 |
@app.route("/agentmemory/session/end", methods=["POST"])
|
| 501 |
def api_session_end():
|
| 502 |
auth_err = check_auth()
|
|
|
|
| 1566 |
enriched.append(item)
|
| 1567 |
return enriched
|
| 1568 |
|
| 1569 |
+
def perform_antigravity_sync(mode="current_session", current_conversation_id=None, current_folder=None):
|
| 1570 |
+
import os
|
| 1571 |
+
import json
|
| 1572 |
+
import glob
|
| 1573 |
+
import re
|
| 1574 |
+
|
| 1575 |
+
brain_dir = os.path.join(os.path.expanduser("~"), ".gemini", "antigravity", "brain")
|
| 1576 |
+
if not os.path.exists(brain_dir):
|
| 1577 |
+
return {"success": False, "syncedSessions": [], "observationsAdded": 0, "error": f"Brain directory not found at {brain_dir}"}
|
| 1578 |
+
|
| 1579 |
+
pattern = os.path.join(brain_dir, "*", ".system_generated", "logs", "transcript.jsonl")
|
| 1580 |
+
files = glob.glob(pattern)
|
| 1581 |
+
if not files:
|
| 1582 |
+
return {"success": True, "syncedSessions": [], "observationsAdded": 0}
|
| 1583 |
+
|
| 1584 |
+
conversations = []
|
| 1585 |
+
for fpath in files:
|
| 1586 |
+
try:
|
| 1587 |
+
mtime = os.path.getmtime(fpath)
|
| 1588 |
+
convo_id = os.path.basename(os.path.dirname(os.path.dirname(os.path.dirname(fpath))))
|
| 1589 |
+
conversations.append({
|
| 1590 |
+
"id": convo_id,
|
| 1591 |
+
"transcriptPath": fpath,
|
| 1592 |
+
"mtime": mtime
|
| 1593 |
+
})
|
| 1594 |
+
except Exception:
|
| 1595 |
+
pass
|
| 1596 |
+
|
| 1597 |
+
if not conversations:
|
| 1598 |
+
return {"success": True, "syncedSessions": [], "observationsAdded": 0}
|
| 1599 |
+
|
| 1600 |
+
conversations.sort(key=lambda x: x["mtime"], reverse=True)
|
| 1601 |
+
|
| 1602 |
+
targets = []
|
| 1603 |
+
if mode == "current_session":
|
| 1604 |
+
if current_conversation_id:
|
| 1605 |
+
match = next((c for c in conversations if c["id"] == current_conversation_id), None)
|
| 1606 |
+
if match:
|
| 1607 |
+
targets = [match]
|
| 1608 |
+
else:
|
| 1609 |
+
targets = [conversations[0]]
|
| 1610 |
+
elif mode == "current_folder":
|
| 1611 |
+
target_folder = current_folder if current_folder else ""
|
| 1612 |
+
if not target_folder:
|
| 1613 |
+
target_folder = os.getcwd()
|
| 1614 |
+
|
| 1615 |
+
target_folder_norm = target_folder.replace("\\", "/").lower().strip()
|
| 1616 |
+
|
| 1617 |
+
for convo in conversations:
|
| 1618 |
+
try:
|
| 1619 |
+
with open(convo["transcriptPath"], "r", encoding="utf-8") as tf:
|
| 1620 |
+
text = tf.read().lower()
|
| 1621 |
+
text_norm = text.replace("\\/", "/").replace("\\\\", "/")
|
| 1622 |
+
if target_folder_norm in text_norm:
|
| 1623 |
+
targets.append(convo)
|
| 1624 |
+
except Exception:
|
| 1625 |
+
pass
|
| 1626 |
+
elif mode == "all":
|
| 1627 |
+
targets = conversations
|
| 1628 |
+
else:
|
| 1629 |
+
return {"success": False, "syncedSessions": [], "observationsAdded": 0, "error": f"Invalid mode: {mode}"}
|
| 1630 |
+
|
| 1631 |
+
if not targets:
|
| 1632 |
+
return {"success": True, "syncedSessions": [], "observationsAdded": 0}
|
| 1633 |
+
|
| 1634 |
+
synced_sessions = []
|
| 1635 |
+
observations_added = 0
|
| 1636 |
+
|
| 1637 |
+
for convo in targets:
|
| 1638 |
+
convo_id = convo["id"]
|
| 1639 |
+
tpath = convo["transcriptPath"]
|
| 1640 |
+
session_id = f"antigravity_{convo_id[:18].replace('-', '_')}"
|
| 1641 |
+
|
| 1642 |
+
project_path = None
|
| 1643 |
+
try:
|
| 1644 |
+
with open(tpath, "r", encoding="utf-8") as tf:
|
| 1645 |
+
first_line = tf.readline()
|
| 1646 |
+
if first_line:
|
| 1647 |
+
step = json.loads(first_line)
|
| 1648 |
+
match = re.search(r"\[([^\]]+)\]\s*->\s*\[([^\]]+)\]", step.get("content", ""))
|
| 1649 |
+
if match:
|
| 1650 |
+
project_path = match.group(2)
|
| 1651 |
+
except Exception:
|
| 1652 |
+
pass
|
| 1653 |
+
|
| 1654 |
+
if not project_path:
|
| 1655 |
+
project_path = os.getcwd()
|
| 1656 |
+
|
| 1657 |
+
turns = []
|
| 1658 |
+
current_prompt = None
|
| 1659 |
+
current_timestamp = None
|
| 1660 |
+
|
| 1661 |
+
try:
|
| 1662 |
+
with open(tpath, "r", encoding="utf-8") as tf:
|
| 1663 |
+
for line in tf:
|
| 1664 |
+
if not line.strip():
|
| 1665 |
+
continue
|
| 1666 |
+
try:
|
| 1667 |
+
step = json.loads(line)
|
| 1668 |
+
step_type = step.get("type")
|
| 1669 |
+
if step_type == "USER_INPUT":
|
| 1670 |
+
p_text = step.get("content", "")
|
| 1671 |
+
if "<USER_REQUEST>" in p_text:
|
| 1672 |
+
parts = p_text.split("<USER_REQUEST>")
|
| 1673 |
+
if len(parts) > 1:
|
| 1674 |
+
p_text = parts[1]
|
| 1675 |
+
if "</USER_REQUEST>" in p_text:
|
| 1676 |
+
p_text = p_text.split("</USER_REQUEST>")[0]
|
| 1677 |
+
current_prompt = p_text.strip()
|
| 1678 |
+
current_timestamp = step.get("created_at")
|
| 1679 |
+
elif step_type == "PLANNER_RESPONSE" and current_prompt:
|
| 1680 |
+
turns.append({
|
| 1681 |
+
"prompt": current_prompt,
|
| 1682 |
+
"response": step.get("content", ""),
|
| 1683 |
+
"timestamp": current_timestamp or step.get("created_at") or datetime_now_iso()
|
| 1684 |
+
})
|
| 1685 |
+
current_prompt = None
|
| 1686 |
+
current_timestamp = None
|
| 1687 |
+
except Exception:
|
| 1688 |
+
pass
|
| 1689 |
+
except Exception:
|
| 1690 |
+
continue
|
| 1691 |
+
|
| 1692 |
+
if not turns:
|
| 1693 |
+
continue
|
| 1694 |
+
|
| 1695 |
+
existing_inputs = set()
|
| 1696 |
+
obs_list = kv.list(KV.observations(session_id))
|
| 1697 |
+
for obs in obs_list:
|
| 1698 |
+
tool_input = obs.get("toolInput") or (obs.get("raw") or {}).get("tool_input")
|
| 1699 |
+
if tool_input:
|
| 1700 |
+
existing_inputs.add(tool_input.strip())
|
| 1701 |
+
|
| 1702 |
+
session_exists = kv.get(KV.sessions, session_id) is not None
|
| 1703 |
+
if not session_exists:
|
| 1704 |
+
session = {
|
| 1705 |
+
"id": session_id,
|
| 1706 |
+
"project": project_path,
|
| 1707 |
+
"cwd": project_path,
|
| 1708 |
+
"startedAt": datetime_now_iso(),
|
| 1709 |
+
"status": "active",
|
| 1710 |
+
"observationCount": 0,
|
| 1711 |
+
"summary": f"Antigravity Pair Programming ({convo_id[:8]})",
|
| 1712 |
+
"firstPrompt": f"Antigravity Pair Programming ({convo_id[:8]})",
|
| 1713 |
+
"agentId": "antigravity"
|
| 1714 |
+
}
|
| 1715 |
+
functions.create_session(kv, session)
|
| 1716 |
+
|
| 1717 |
+
convo_synced = False
|
| 1718 |
+
for turn in turns:
|
| 1719 |
+
prompt = turn["prompt"]
|
| 1720 |
+
if prompt.strip() in existing_inputs:
|
| 1721 |
+
continue
|
| 1722 |
+
|
| 1723 |
+
payload = {
|
| 1724 |
+
"sessionId": session_id,
|
| 1725 |
+
"project": project_path,
|
| 1726 |
+
"cwd": project_path,
|
| 1727 |
+
"hookType": "post_tool_use",
|
| 1728 |
+
"timestamp": turn["timestamp"],
|
| 1729 |
+
"agentId": "antigravity",
|
| 1730 |
+
"data": {
|
| 1731 |
+
"tool_name": "conversation",
|
| 1732 |
+
"tool_input": prompt,
|
| 1733 |
+
"tool_output": turn["response"],
|
| 1734 |
+
}
|
| 1735 |
+
}
|
| 1736 |
+
functions.observe(kv, payload)
|
| 1737 |
+
observations_added += 1
|
| 1738 |
+
convo_synced = True
|
| 1739 |
+
|
| 1740 |
+
if convo_synced:
|
| 1741 |
+
synced_sessions.append(convo_id)
|
| 1742 |
+
|
| 1743 |
+
return {"success": True, "syncedSessions": synced_sessions, "observationsAdded": observations_added}
|
| 1744 |
+
|
| 1745 |
@app.route("/agentmemory/mcp/tools", methods=["GET"])
|
| 1746 |
def mcp_tools_list():
|
| 1747 |
auth_err = check_auth()
|
|
|
|
| 1985 |
},
|
| 1986 |
"required": ["content", "project"]
|
| 1987 |
}
|
| 1988 |
+
},
|
| 1989 |
+
{
|
| 1990 |
+
"name": "memory_antigravity_sync",
|
| 1991 |
+
"description": "Sync Antigravity chat transcripts to agentmemory. Supports syncing the current session, all sessions, or sessions associated with the current folder.",
|
| 1992 |
+
"inputSchema": {
|
| 1993 |
+
"type": "object",
|
| 1994 |
+
"properties": {
|
| 1995 |
+
"mode": {"type": "string", "description": "Sync mode: current_session (default), current_folder, or all"},
|
| 1996 |
+
"currentConversationId": {"type": "string", "description": "Optional conversation ID of the current active session"},
|
| 1997 |
+
"currentFolder": {"type": "string", "description": "Optional current folder path to filter by"}
|
| 1998 |
+
},
|
| 1999 |
+
"required": ["mode"]
|
| 2000 |
+
}
|
| 2001 |
}
|
| 2002 |
]
|
| 2003 |
return jsonify({"tools": tools}), 200
|
|
|
|
| 2177 |
res = functions.remember(kv, payload)
|
| 2178 |
text_out = json.dumps(res)
|
| 2179 |
|
| 2180 |
+
elif name == "memory_antigravity_sync":
|
| 2181 |
+
mode = args.get("mode") or "current_session"
|
| 2182 |
+
current_convo = args.get("currentConversationId")
|
| 2183 |
+
current_folder = args.get("currentFolder")
|
| 2184 |
+
res = perform_antigravity_sync(mode, current_convo, current_folder)
|
| 2185 |
+
text_out = json.dumps(res)
|
| 2186 |
+
|
| 2187 |
else:
|
| 2188 |
return jsonify({"error": f"unknown tool: {name}"}), 400
|
| 2189 |
|