@yottagraph-app/aether-instructions 1.1.33 → 1.1.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/rules/agents-data.mdc
CHANGED
|
@@ -80,10 +80,69 @@ etc.) handle entity resolution, NEID formatting, and schema lookups
|
|
|
80
80
|
automatically. Use the `data-model` skill docs for entity types,
|
|
81
81
|
properties, and relationship schemas.
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
### Wiring McpToolset (transport class)
|
|
84
|
+
|
|
85
|
+
The Elemental MCP server uses **Streamable HTTP** transport. Use
|
|
86
|
+
`StreamableHTTPConnectionParams` — **NOT** `SseConnectionParams`. The
|
|
87
|
+
`SseConnectionParams` class is for older SSE-based MCP servers and will
|
|
88
|
+
silently fail against the Elemental server (the agent starts with zero
|
|
89
|
+
tools and the LLM hallucinates code).
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from google.adk.tools.mcp_tool import McpToolset
|
|
93
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
|
94
|
+
|
|
95
|
+
McpToolset(
|
|
96
|
+
connection_params=StreamableHTTPConnectionParams(url=mcp_url)
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Resolve the MCP URL from the environment or `broadchurch.yaml`:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
import os
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
import yaml
|
|
106
|
+
|
|
107
|
+
def _get_mcp_url(server_name: str = "elemental") -> str:
|
|
108
|
+
"""Resolve MCP server URL from env or broadchurch.yaml."""
|
|
109
|
+
env_url = os.environ.get("ELEMENTAL_MCP_URL")
|
|
110
|
+
if env_url:
|
|
111
|
+
return env_url
|
|
112
|
+
for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
|
|
113
|
+
if candidate.exists():
|
|
114
|
+
config = yaml.safe_load(candidate.read_text()) or {}
|
|
115
|
+
gw = config.get("gateway", {})
|
|
116
|
+
org_id = config.get("tenant", {}).get("org_id", "")
|
|
117
|
+
if gw.get("url") and org_id:
|
|
118
|
+
return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
|
|
119
|
+
return config.get("mcp", {}).get(server_name, "")
|
|
120
|
+
return ""
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For **local dev**, set the env var:
|
|
124
|
+
```bash
|
|
125
|
+
export ELEMENTAL_MCP_URL="https://mcp.news.prod.g.lovelace.ai/elemental/mcp"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
In **production**, the agent reads the gateway URL and org_id from
|
|
129
|
+
`broadchurch.yaml` and routes through the Portal MCP proxy, which handles
|
|
130
|
+
authentication automatically.
|
|
131
|
+
|
|
132
|
+
### Silent failure warning
|
|
133
|
+
|
|
134
|
+
If `McpToolset` cannot connect (wrong transport class, bad URL, or server
|
|
135
|
+
down), **the agent starts with zero MCP tools and no error is raised**.
|
|
136
|
+
The LLM will then hallucinate tool calls instead of executing real ones.
|
|
137
|
+
Always validate the MCP URL at agent startup:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
mcp_url = _get_mcp_url()
|
|
141
|
+
if not mcp_url:
|
|
142
|
+
raise RuntimeError("No MCP URL — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### MCP response patterns
|
|
87
146
|
|
|
88
147
|
**Read the `elemental-mcp-patterns` skill** (`skills/elemental-mcp-patterns/`)
|
|
89
148
|
before writing tool code. It covers MCP response shapes, property type
|
package/rules/agents.mdc
CHANGED
|
@@ -48,7 +48,11 @@ Key rules:
|
|
|
48
48
|
- Pin dependency versions in `requirements.txt` for reproducible deployments
|
|
49
49
|
|
|
50
50
|
For agents that call the **Elemental API** (`broadchurch_auth`, endpoints,
|
|
51
|
-
local `ELEMENTAL_*` env vars), see the `agents-data` rule.
|
|
51
|
+
local `ELEMENTAL_*` env vars), see the `agents-data` rule. For agents
|
|
52
|
+
using **Elemental MCP tools**, the `agents-data` rule also covers
|
|
53
|
+
`McpToolset` wiring — use `StreamableHTTPConnectionParams` (not
|
|
54
|
+
`SseConnectionParams`) and read the `elemental-mcp-patterns` skill for
|
|
55
|
+
response handling patterns.
|
|
52
56
|
|
|
53
57
|
## Local Testing
|
|
54
58
|
|
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: elemental-mcp-patterns
|
|
3
|
+
description: How to correctly call Elemental MCP tools and process their responses when writing Python ADK agent tool functions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Elemental MCP Patterns
|
|
7
|
+
|
|
8
|
+
This skill is for **build agents writing Python tool code** that calls
|
|
9
|
+
Elemental MCP tools. It covers MCP wiring, transport configuration,
|
|
10
|
+
response shapes, property type handling, and copy-paste patterns for
|
|
11
|
+
common operations.
|
|
12
|
+
|
|
13
|
+
For domain knowledge (what entity types and properties exist), see the
|
|
14
|
+
**data-model** skill.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## MCP Wiring: Connecting ADK Agents to Elemental MCP
|
|
19
|
+
|
|
20
|
+
### Transport class
|
|
21
|
+
|
|
22
|
+
The Elemental MCP server uses **Streamable HTTP** transport. You **must**
|
|
23
|
+
use `StreamableHTTPConnectionParams`:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from google.adk.tools.mcp_tool import McpToolset
|
|
27
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> **Do NOT use `SseConnectionParams`.** `SseConnectionParams` is for
|
|
31
|
+
> legacy SSE-based MCP servers. Using it against the Elemental MCP server
|
|
32
|
+
> will silently fail — the agent starts with zero tools and no error is
|
|
33
|
+
> raised. The LLM then hallucinates tool calls.
|
|
34
|
+
|
|
35
|
+
### Resolving the MCP URL
|
|
36
|
+
|
|
37
|
+
The MCP URL comes from the environment (`ELEMENTAL_MCP_URL`) for local
|
|
38
|
+
dev, or from `broadchurch.yaml` for deployed agents:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import os
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
import yaml
|
|
44
|
+
|
|
45
|
+
def _get_mcp_url(server_name: str = "elemental") -> str:
|
|
46
|
+
"""Resolve MCP server URL from env or broadchurch.yaml."""
|
|
47
|
+
env_url = os.environ.get("ELEMENTAL_MCP_URL")
|
|
48
|
+
if env_url:
|
|
49
|
+
return env_url
|
|
50
|
+
for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
|
|
51
|
+
if candidate.exists():
|
|
52
|
+
config = yaml.safe_load(candidate.read_text()) or {}
|
|
53
|
+
gw = config.get("gateway", {})
|
|
54
|
+
org_id = config.get("tenant", {}).get("org_id", "")
|
|
55
|
+
if gw.get("url") and org_id:
|
|
56
|
+
return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
|
|
57
|
+
return config.get("mcp", {}).get(server_name, "")
|
|
58
|
+
return ""
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Complete wiring example
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from google.adk.agents import Agent
|
|
65
|
+
from google.adk.tools.mcp_tool import McpToolset
|
|
66
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
|
67
|
+
|
|
68
|
+
mcp_url = _get_mcp_url() # function from above
|
|
69
|
+
if not mcp_url:
|
|
70
|
+
raise RuntimeError("No MCP URL — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
|
|
71
|
+
|
|
72
|
+
root_agent = Agent(
|
|
73
|
+
model="gemini-2.0-flash",
|
|
74
|
+
name="my_mcp_agent",
|
|
75
|
+
instruction="You are a research assistant with access to the Lovelace knowledge graph.",
|
|
76
|
+
tools=[
|
|
77
|
+
my_custom_tool, # your Python tool functions
|
|
78
|
+
McpToolset(connection_params=StreamableHTTPConnectionParams(url=mcp_url)),
|
|
79
|
+
],
|
|
80
|
+
)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The `McpToolset` automatically discovers all MCP tools at startup and
|
|
84
|
+
exposes them to the agent. To restrict which tools are exposed, use
|
|
85
|
+
`tool_filter`:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
McpToolset(
|
|
89
|
+
connection_params=StreamableHTTPConnectionParams(url=mcp_url),
|
|
90
|
+
tool_filter=["elemental_get_entity", "elemental_get_related", "elemental_get_events"],
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Silent failure mode
|
|
95
|
+
|
|
96
|
+
If `McpToolset` cannot connect, **no error is raised at agent startup**.
|
|
97
|
+
The agent simply has zero MCP tools. Symptoms:
|
|
98
|
+
- The agent never calls any `elemental_*` tools
|
|
99
|
+
- The LLM fabricates code or data instead of using tools
|
|
100
|
+
- No connection error in logs
|
|
101
|
+
|
|
102
|
+
**Always validate** the MCP URL and check for tool availability during
|
|
103
|
+
development. If MCP tools aren't working, verify:
|
|
104
|
+
1. The URL is correct (check `broadchurch.yaml` `mcp.elemental` or env var)
|
|
105
|
+
2. You're using `StreamableHTTPConnectionParams` (not `SseConnectionParams`)
|
|
106
|
+
3. The MCP server is reachable (try `curl -X POST <url> -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'`)
|
|
107
|
+
|
|
108
|
+
### Local dev setup
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
export ELEMENTAL_MCP_URL="https://mcp.news.prod.g.lovelace.ai/elemental/mcp"
|
|
112
|
+
export GOOGLE_CLOUD_PROJECT=broadchurch
|
|
113
|
+
export GOOGLE_CLOUD_LOCATION=us-central1
|
|
114
|
+
export GOOGLE_GENAI_USE_VERTEXAI=1
|
|
115
|
+
cd agents/
|
|
116
|
+
adk web
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Tool Quick Reference
|
|
122
|
+
|
|
123
|
+
| Tool | Use when you need to... |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `elemental_get_entity` | Resolve an entity by name or ID, fetch its properties (supports `history` for time-series) |
|
|
126
|
+
| `elemental_get_related` | Find related entities (requires `related_flavor`); use `direction` and `relationship_types` to filter |
|
|
127
|
+
| `elemental_get_events` | Get typed events with categories, dates, participants |
|
|
128
|
+
| `elemental_get_citations` | Look up provenance for `ref` hashes returned by other tools |
|
|
129
|
+
| `elemental_get_schema` | Discover flavors (entity types), property names, and property types |
|
|
130
|
+
| `elemental_get_relationships` | Get relationship types and counts between two entities |
|
|
131
|
+
| `elemental_graph_neighborhood` | Get the most influential neighbors of an entity |
|
|
132
|
+
| `elemental_graph_sentiment` | Sentiment time series, trend analysis, and statistics from news articles |
|
|
133
|
+
| `elemental_introspect` | Discover what data **actually exists**: entity counts, populated properties with fill rates, sample values. Use before building features to verify data availability. |
|
|
134
|
+
| `elemental_traverse` | Stateful graph navigation — build a working set of entities across multiple calls (start → expand → filter → inspect) |
|
|
135
|
+
| `elemental_health` | Health check — verify MCP server connectivity |
|
|
136
|
+
|
|
137
|
+
### MCP Prompts
|
|
138
|
+
|
|
139
|
+
The server also exposes built-in prompts that provide pre-composed
|
|
140
|
+
workflows:
|
|
141
|
+
|
|
142
|
+
| Prompt | Purpose |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `company-deep-dive` | Comprehensive company research workflow |
|
|
145
|
+
| `blast-radius` | Analyze impact/connections radiating from an entity |
|
|
146
|
+
| `event-monitor` | Track events and developments for entities |
|
|
147
|
+
|
|
148
|
+
### MCP Resources
|
|
149
|
+
|
|
150
|
+
Documentation resources are available directly from the server:
|
|
151
|
+
|
|
152
|
+
| Resource | Description |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `elemental_data_model` | Entity types, properties, and relationships |
|
|
155
|
+
| `elemental_guide` | Usage guide for the MCP tools |
|
|
156
|
+
| `elemental_schema` | Live schema data |
|
|
157
|
+
| `elemental_workflows` | Common workflow patterns and examples |
|
|
158
|
+
| `elemental_mcp_server_info` | Server capabilities and configuration |
|
|
159
|
+
|
|
160
|
+
These prompts and resources can be useful shortcuts — check if your MCP
|
|
161
|
+
client supports them before building equivalent logic from scratch.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Response Shapes
|
|
166
|
+
|
|
167
|
+
Every MCP tool call returns a JSON dict. Below are annotated examples of
|
|
168
|
+
the three tools you'll use most.
|
|
169
|
+
|
|
170
|
+
### elemental_get_entity
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
result = await mcp_call("elemental_get_entity", {
|
|
174
|
+
"entity": "Intel",
|
|
175
|
+
"properties": ["country", "total_revenue", "ticker_symbol", "net_income"]
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
# Response shape:
|
|
179
|
+
{
|
|
180
|
+
"entity": {
|
|
181
|
+
"neid": "04926132345040704022",
|
|
182
|
+
"name": "Intel Corporation",
|
|
183
|
+
"flavor": "organization",
|
|
184
|
+
"properties": {
|
|
185
|
+
"country": {
|
|
186
|
+
"value": "5816460566439750832", # <-- THIS IS A NEID, NOT A NAME
|
|
187
|
+
"ref": "ref_a3f2b1c8"
|
|
188
|
+
},
|
|
189
|
+
"total_revenue": {
|
|
190
|
+
"value": 52900000000,
|
|
191
|
+
"ref": "ref_d4e5f678",
|
|
192
|
+
"attributes": {"filing_period": "FY"}
|
|
193
|
+
},
|
|
194
|
+
"ticker_symbol": {
|
|
195
|
+
"value": "INTC"
|
|
196
|
+
},
|
|
197
|
+
"net_income": {
|
|
198
|
+
"value": -267000000,
|
|
199
|
+
"ref": "ref_b2c3d4e5"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Key points:
|
|
207
|
+
- `entity` is `null` if resolution failed
|
|
208
|
+
- Each property value is `{"value": ..., "ref"?: "...", "attributes"?: {...}}`
|
|
209
|
+
- **`value` can be a NEID** for reference-typed properties — see
|
|
210
|
+
"The Property Type Problem" below
|
|
211
|
+
- `ref` is a citation hash — pass it through to the LLM exactly as-is
|
|
212
|
+
- `low_confidence` means the entity match is fuzzy — confirm with the user
|
|
213
|
+
|
|
214
|
+
### elemental_get_related
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
result = await mcp_call("elemental_get_related", {
|
|
218
|
+
"entity": "Intel",
|
|
219
|
+
"related_flavor": "person", # REQUIRED
|
|
220
|
+
"relationship_types": ["board_member_of"], # optional filter
|
|
221
|
+
"related_properties": ["nationality", "title"], # properties on each person
|
|
222
|
+
"limit": 20
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
# Response shape:
|
|
226
|
+
{
|
|
227
|
+
"resolved": {
|
|
228
|
+
"neid": "04926132345040704022",
|
|
229
|
+
"name": "Intel Corporation",
|
|
230
|
+
"flavor": "organization"
|
|
231
|
+
},
|
|
232
|
+
"total": 12,
|
|
233
|
+
"relationships": [
|
|
234
|
+
{
|
|
235
|
+
"neid": "08371625409283746152",
|
|
236
|
+
"name": "Patrick Gelsinger",
|
|
237
|
+
"flavor": "person",
|
|
238
|
+
"relationship_types": ["board_member_of"],
|
|
239
|
+
"properties": {
|
|
240
|
+
"nationality": {"value": "United States"},
|
|
241
|
+
"title": {"value": "CEO"}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
# ... more related entities
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Key points:
|
|
250
|
+
- `related_flavor` is **required** — you must specify what type of entity
|
|
251
|
+
to look for
|
|
252
|
+
- `resolved` is the center entity (can be `null` if resolution failed)
|
|
253
|
+
- Each item in `relationships` has the same property value shape as
|
|
254
|
+
`elemental_get_entity`
|
|
255
|
+
- Use `direction` (`"outgoing"`, `"incoming"`, `"both"`) to control
|
|
256
|
+
traversal direction
|
|
257
|
+
|
|
258
|
+
### elemental_get_events
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
result = await mcp_call("elemental_get_events", {
|
|
262
|
+
"entity": "Intel",
|
|
263
|
+
"categories": ["Bankruptcy", "IPO", "Regulatory Action"], # optional
|
|
264
|
+
"time_range": {"after": "2025-01-01"}, # optional
|
|
265
|
+
"include_participants": True, # optional
|
|
266
|
+
"limit": 20
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
# Response shape:
|
|
270
|
+
{
|
|
271
|
+
"resolved": {
|
|
272
|
+
"neid": "04926132345040704022",
|
|
273
|
+
"name": "Intel Corporation"
|
|
274
|
+
},
|
|
275
|
+
"total": 3,
|
|
276
|
+
"events": [
|
|
277
|
+
{
|
|
278
|
+
"neid": "09283746152837461528",
|
|
279
|
+
"name": "Intel CHIPS Act Award",
|
|
280
|
+
"flavor": "event",
|
|
281
|
+
"properties": {
|
|
282
|
+
"category": {"value": "Regulatory Action"},
|
|
283
|
+
"date": {"value": "2025-03-15"},
|
|
284
|
+
"description": {"value": "Intel awarded $8.5B in CHIPS Act funding"},
|
|
285
|
+
"likelihood": {"value": 0.95}
|
|
286
|
+
},
|
|
287
|
+
"participants": [
|
|
288
|
+
{
|
|
289
|
+
"neid": "04926132345040704022",
|
|
290
|
+
"name": "Intel Corporation",
|
|
291
|
+
"flavor": "organization",
|
|
292
|
+
"relationship_types": ["participant"]
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Key points:
|
|
301
|
+
- Events have typed fields: `category`, `date`, `description`, `likelihood`
|
|
302
|
+
- Use `categories` to filter — do NOT try to find events by scanning
|
|
303
|
+
property names for keywords
|
|
304
|
+
- `participants` only included when `include_participants` is `True`
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## The Property Type Problem
|
|
309
|
+
|
|
310
|
+
This is the single biggest source of bugs in MCP-based agents.
|
|
311
|
+
|
|
312
|
+
**The issue:** property values can be entity references (NEIDs), not
|
|
313
|
+
display text. If you render them raw, the user sees `"5816460566439750832"`
|
|
314
|
+
instead of `"United States"`.
|
|
315
|
+
|
|
316
|
+
### Step 1: Get the schema to learn property types
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
schema = await mcp_call("elemental_get_schema", {"flavor": "organization"})
|
|
320
|
+
|
|
321
|
+
# Response includes a properties array:
|
|
322
|
+
# [
|
|
323
|
+
# {"name": "country", "type": "nindex", ...}, <-- entity reference!
|
|
324
|
+
# {"name": "total_revenue", "type": "float", ...},
|
|
325
|
+
# {"name": "ticker_symbol", "type": "string", ...},
|
|
326
|
+
# {"name": "industry", "type": "nindex", ...}, <-- entity reference!
|
|
327
|
+
# ]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Step 2: Build a type map (once per session)
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
def build_property_type_map(schema_result: dict) -> dict[str, str]:
|
|
334
|
+
"""Map property names to their types from schema."""
|
|
335
|
+
type_map = {}
|
|
336
|
+
for prop in schema_result.get("properties", []):
|
|
337
|
+
type_map[prop["name"]] = prop["type"]
|
|
338
|
+
return type_map
|
|
339
|
+
|
|
340
|
+
# Cache this — don't re-fetch schema for every tool call
|
|
341
|
+
property_types = build_property_type_map(schema)
|
|
342
|
+
# {"country": "nindex", "total_revenue": "float", "ticker_symbol": "string", ...}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Step 3: Resolve reference values before display
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
async def format_properties(
|
|
349
|
+
properties: dict,
|
|
350
|
+
property_types: dict[str, str],
|
|
351
|
+
mcp_call,
|
|
352
|
+
) -> dict[str, str]:
|
|
353
|
+
"""Format property values for user-facing display."""
|
|
354
|
+
formatted = {}
|
|
355
|
+
for name, prop in properties.items():
|
|
356
|
+
value = prop["value"]
|
|
357
|
+
prop_type = property_types.get(name, "string")
|
|
358
|
+
|
|
359
|
+
if prop_type == "nindex" and isinstance(value, (str, int)):
|
|
360
|
+
# This is an entity reference — resolve to display name
|
|
361
|
+
resolved = await mcp_call("elemental_get_entity", {
|
|
362
|
+
"entity_id": {"id": str(value), "id_type": "neid"}
|
|
363
|
+
})
|
|
364
|
+
entity = resolved.get("entity")
|
|
365
|
+
formatted[name] = entity["name"] if entity else str(value)
|
|
366
|
+
elif isinstance(value, (int, float)) and abs(value) >= 1_000_000:
|
|
367
|
+
formatted[name] = _format_large_number(value)
|
|
368
|
+
else:
|
|
369
|
+
formatted[name] = str(value)
|
|
370
|
+
return formatted
|
|
371
|
+
|
|
372
|
+
def _format_large_number(n: float) -> str:
|
|
373
|
+
"""Format large numbers: 52900000000 -> '$52.9B'."""
|
|
374
|
+
abs_n = abs(n)
|
|
375
|
+
if abs_n >= 1e12:
|
|
376
|
+
return f"${n/1e12:.1f}T"
|
|
377
|
+
if abs_n >= 1e9:
|
|
378
|
+
return f"${n/1e9:.1f}B"
|
|
379
|
+
if abs_n >= 1e6:
|
|
380
|
+
return f"${n/1e6:.1f}M"
|
|
381
|
+
return f"${n:,.0f}"
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Property type values you'll encounter
|
|
385
|
+
|
|
386
|
+
| Schema type | Value is | How to display |
|
|
387
|
+
|---|---|---|
|
|
388
|
+
| `string` | Plain text | Display directly |
|
|
389
|
+
| `integer`, `float` | Number | Format with units (check `unit` in schema) |
|
|
390
|
+
| `nindex` | Entity NEID | **Must resolve** via `elemental_get_entity` |
|
|
391
|
+
| `boolean` | `true`/`false` | Display as Yes/No |
|
|
392
|
+
| `datetime` | ISO 8601 string | Format as human-readable date |
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Common Patterns
|
|
397
|
+
|
|
398
|
+
### Resolve and cache an entity
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
async def resolve_entity(
|
|
402
|
+
name: str,
|
|
403
|
+
session_state: dict,
|
|
404
|
+
mcp_call,
|
|
405
|
+
neid: str | None = None,
|
|
406
|
+
) -> dict | None:
|
|
407
|
+
"""Resolve entity, using cache if available."""
|
|
408
|
+
cache = session_state.setdefault("entities", {})
|
|
409
|
+
|
|
410
|
+
# Check cache by NEID or name
|
|
411
|
+
cache_key = neid or name.lower()
|
|
412
|
+
if cache_key in cache:
|
|
413
|
+
return cache[cache_key]
|
|
414
|
+
|
|
415
|
+
params = {}
|
|
416
|
+
if neid:
|
|
417
|
+
params["entity_id"] = {"id": neid, "id_type": "neid"}
|
|
418
|
+
else:
|
|
419
|
+
params["entity"] = name
|
|
420
|
+
|
|
421
|
+
result = await mcp_call("elemental_get_entity", params)
|
|
422
|
+
entity = result.get("entity")
|
|
423
|
+
if not entity:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
# Cache by both NEID and lowercase name
|
|
427
|
+
cache[entity["neid"]] = entity
|
|
428
|
+
cache[entity["name"].lower()] = entity
|
|
429
|
+
return entity
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Build a rich entity briefing
|
|
433
|
+
|
|
434
|
+
Don't return a one-paragraph summary. Chain multiple calls to build a
|
|
435
|
+
comprehensive report:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
async def entity_briefing(name: str, mcp_call, session_state: dict) -> str:
|
|
439
|
+
"""Build a comprehensive entity briefing."""
|
|
440
|
+
# 1. Resolve + properties
|
|
441
|
+
entity = await mcp_call("elemental_get_entity", {
|
|
442
|
+
"entity": name,
|
|
443
|
+
"properties": [
|
|
444
|
+
"country", "ticker_symbol", "total_revenue", "net_income",
|
|
445
|
+
"total_assets", "industry", "lei", "company_cik"
|
|
446
|
+
]
|
|
447
|
+
})
|
|
448
|
+
if not entity.get("entity"):
|
|
449
|
+
return f"Could not resolve entity: {name}"
|
|
450
|
+
|
|
451
|
+
e = entity["entity"]
|
|
452
|
+
neid = e["neid"]
|
|
453
|
+
report_parts = [f"# {e['name']}", f"Type: {e.get('flavor', 'unknown')}"]
|
|
454
|
+
|
|
455
|
+
# 2. Format properties (handling nindex resolution)
|
|
456
|
+
if e.get("properties"):
|
|
457
|
+
schema = await mcp_call("elemental_get_schema", {"flavor": e.get("flavor", "")})
|
|
458
|
+
ptypes = build_property_type_map(schema)
|
|
459
|
+
props = await format_properties(e["properties"], ptypes, mcp_call)
|
|
460
|
+
for k, v in props.items():
|
|
461
|
+
report_parts.append(f"- {k}: {v}")
|
|
462
|
+
|
|
463
|
+
# 3. Key relationships
|
|
464
|
+
for related_flavor, label in [("person", "Key People"), ("organization", "Related Orgs")]:
|
|
465
|
+
related = await mcp_call("elemental_get_related", {
|
|
466
|
+
"entity_id": {"id": neid, "id_type": "neid"},
|
|
467
|
+
"related_flavor": related_flavor,
|
|
468
|
+
"limit": 10
|
|
469
|
+
})
|
|
470
|
+
if related.get("relationships"):
|
|
471
|
+
report_parts.append(f"\n## {label}")
|
|
472
|
+
for r in related["relationships"]:
|
|
473
|
+
types = ", ".join(r.get("relationship_types", []))
|
|
474
|
+
report_parts.append(f"- {r['name']} ({types})")
|
|
475
|
+
|
|
476
|
+
# 4. Recent events
|
|
477
|
+
events = await mcp_call("elemental_get_events", {
|
|
478
|
+
"entity_id": {"id": neid, "id_type": "neid"},
|
|
479
|
+
"limit": 10
|
|
480
|
+
})
|
|
481
|
+
if events.get("events"):
|
|
482
|
+
report_parts.append("\n## Recent Events")
|
|
483
|
+
for ev in events["events"]:
|
|
484
|
+
props = ev.get("properties", {})
|
|
485
|
+
date = props.get("date", {}).get("value", "")
|
|
486
|
+
cat = props.get("category", {}).get("value", "")
|
|
487
|
+
desc = props.get("description", {}).get("value", ev["name"])
|
|
488
|
+
report_parts.append(f"- [{date}] {cat}: {desc}")
|
|
489
|
+
|
|
490
|
+
return "\n".join(report_parts)
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Fetch events correctly
|
|
494
|
+
|
|
495
|
+
Always use `elemental_get_events`. Never try to find events by scanning
|
|
496
|
+
property names or PID names for keywords like "event" or "filing".
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
# CORRECT — use the dedicated events tool
|
|
500
|
+
events = await mcp_call("elemental_get_events", {
|
|
501
|
+
"entity": "Intel",
|
|
502
|
+
"categories": ["Regulatory Action", "Acquisition"],
|
|
503
|
+
"time_range": {"after": "2025-01-01"},
|
|
504
|
+
"include_participants": True
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
# WRONG — do NOT scan properties/PIDs for event-like names
|
|
508
|
+
# This matches "filed" (a document relationship), not actual events
|
|
509
|
+
schema = await mcp_call("elemental_get_schema", {"flavor": "organization"})
|
|
510
|
+
event_pids = [p for p in schema["properties"] if "event" in p["name"]] # BAD
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Fetch related entities with properties
|
|
514
|
+
|
|
515
|
+
```python
|
|
516
|
+
# Get board members with their titles and nationalities
|
|
517
|
+
board = await mcp_call("elemental_get_related", {
|
|
518
|
+
"entity": "JPMorgan Chase",
|
|
519
|
+
"related_flavor": "person",
|
|
520
|
+
"relationship_types": ["board_member_of", "is_officer"],
|
|
521
|
+
"related_properties": ["title", "nationality"],
|
|
522
|
+
"limit": 30
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
for person in board.get("relationships", []):
|
|
526
|
+
name = person["name"]
|
|
527
|
+
title = person.get("properties", {}).get("title", {}).get("value", "")
|
|
528
|
+
print(f"{name} — {title}")
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
## Common Properties & Relationships Quick Reference
|
|
534
|
+
|
|
535
|
+
Use `elemental_introspect` to discover what's actually populated for a
|
|
536
|
+
given flavor. Use `elemental_get_schema` for the full property list. The
|
|
537
|
+
tables below are a starting-point cheat sheet — not exhaustive.
|
|
538
|
+
|
|
539
|
+
### Properties by flavor
|
|
540
|
+
|
|
541
|
+
| Flavor | Common properties |
|
|
542
|
+
|---|---|
|
|
543
|
+
| `organization` | `country` (nindex), `ticker_symbol`, `total_revenue`, `net_income`, `total_assets`, `industry` (nindex), `lei`, `company_cik`, `ein`, `website` |
|
|
544
|
+
| `person` | `nationality` (nindex), `title`, `birth_date`, `gender` |
|
|
545
|
+
| `government_body` | `country` (nindex), `jurisdiction` |
|
|
546
|
+
| `article` | `headline`, `published_date`, `source`, `url` |
|
|
547
|
+
| `event` | `category`, `date`, `description`, `likelihood` |
|
|
548
|
+
| `financial_instrument` | `ticker_symbol`, `exchange`, `currency` |
|
|
549
|
+
|
|
550
|
+
Properties marked `(nindex)` are entity references — their raw value is
|
|
551
|
+
a NEID that must be resolved to a display name. See "The Property Type
|
|
552
|
+
Problem" above.
|
|
553
|
+
|
|
554
|
+
### Relationship types and direction
|
|
555
|
+
|
|
556
|
+
The `direction` parameter on `elemental_get_related` controls traversal.
|
|
557
|
+
Getting it wrong returns zero results with no error.
|
|
558
|
+
|
|
559
|
+
| Relationship | Meaning | Direction from center |
|
|
560
|
+
|---|---|---|
|
|
561
|
+
| `board_member_of` | Person sits on org's board | `"incoming"` when center is org |
|
|
562
|
+
| `is_officer` | Person is an officer of org | `"incoming"` when center is org |
|
|
563
|
+
| `subsidiary_of` | Org is a subsidiary of parent | `"outgoing"` from subsidiary |
|
|
564
|
+
| `owns` | Entity owns another entity | `"outgoing"` from owner |
|
|
565
|
+
| `appears_in` | Entity mentioned in article | `"both"` is usually safest |
|
|
566
|
+
| `participant` | Entity participates in event | `"both"` is usually safest |
|
|
567
|
+
| `filed` | Org filed a document | `"outgoing"` from org |
|
|
568
|
+
| `works_at` | Person works at org | `"outgoing"` from person |
|
|
569
|
+
|
|
570
|
+
> **Tip:** If you're unsure about direction, use `"both"` (the default)
|
|
571
|
+
> first and check the results. Then narrow to `"incoming"` or
|
|
572
|
+
> `"outgoing"` once you know which way the edges point. You can also use
|
|
573
|
+
> `elemental_get_relationships` to see all relationship types and counts
|
|
574
|
+
> between two specific entities.
|
|
575
|
+
|
|
576
|
+
> **Tip:** Use `elemental_introspect(flavor="organization")` to see which
|
|
577
|
+
> properties and relationships are actually populated with data and their
|
|
578
|
+
> fill rates. This prevents building features against empty data.
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## Anti-Patterns
|
|
583
|
+
|
|
584
|
+
These are mistakes previous agents have made. Do not repeat them.
|
|
585
|
+
|
|
586
|
+
1. **Do not substring-match PID names to find events or filings.**
|
|
587
|
+
PIDs like `"filed"` are document relationship IDs, not event timestamps.
|
|
588
|
+
Use `elemental_get_events` for events.
|
|
589
|
+
|
|
590
|
+
2. **Do not render raw NEID values in user-facing text.** If a property
|
|
591
|
+
value looks like a large number (`5816460566439750832`), it's probably
|
|
592
|
+
a `nindex` reference. Check the schema.
|
|
593
|
+
|
|
594
|
+
3. **Do not skip schema lookup.** Property types are not guessable from
|
|
595
|
+
names alone. `"country"` looks like it should be a string, but it's
|
|
596
|
+
an `nindex` (entity reference). Always call `elemental_get_schema`
|
|
597
|
+
at least once per flavor.
|
|
598
|
+
|
|
599
|
+
4. **Do not return thin briefings.** When a user asks "tell me about
|
|
600
|
+
Intel," they expect a comprehensive research report — not a single
|
|
601
|
+
paragraph. Chain entity + related + events into a thorough response.
|
|
602
|
+
|
|
603
|
+
5. **Do not fabricate citation refs.** Only include `[ref_...]` markers
|
|
604
|
+
when the `ref` field is actually present in the tool response data.
|
|
605
|
+
The client renders these as numbered citations with source links.
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
## Citation Handling
|
|
610
|
+
|
|
611
|
+
Property values may include a `ref` field (e.g. `"ref_a3f2b1c8"`). When
|
|
612
|
+
building user-facing text, include the ref in brackets after the fact:
|
|
613
|
+
|
|
614
|
+
```
|
|
615
|
+
Revenue was $52.9B [ref_a3f2b1c8]
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The chat UI translates these into numbered source citations. Rules:
|
|
619
|
+
- Only include refs that are present in the actual response data
|
|
620
|
+
- Copy the ref string exactly — never construct or modify refs
|
|
621
|
+
- Omit the bracket when no ref is present for a value
|
|
@@ -6,7 +6,13 @@ globs: agents/**
|
|
|
6
6
|
|
|
7
7
|
# Agents: Elemental MCP (MCP-only)
|
|
8
8
|
|
|
9
|
-
In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the knowledge graph. **`broadchurch_auth` HTTP** to the Query Server is the **api-mcp** path; here you wire **MCP tools** into ADK
|
|
9
|
+
In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the knowledge graph. **`broadchurch_auth` HTTP** to the Query Server is the **api-mcp** path; here you wire **MCP tools** into ADK via **McpToolset** + **`StreamableHTTPConnectionParams`** to your Elemental MCP URL — from env or `broadchurch.yaml` / gateway.
|
|
10
|
+
|
|
11
|
+
> **Transport class:** The Elemental MCP server uses **Streamable HTTP**
|
|
12
|
+
> transport. Use `StreamableHTTPConnectionParams` — **NOT**
|
|
13
|
+
> `SseConnectionParams`. Using `SseConnectionParams` will silently fail:
|
|
14
|
+
> the agent starts with zero tools and the LLM hallucinates code. See the
|
|
15
|
+
> "Wiring McpToolset" section below for a working snippet.
|
|
10
16
|
|
|
11
17
|
## Primary agent patterns (all first-class)
|
|
12
18
|
|
|
@@ -18,10 +24,52 @@ In **mcp-only** mode, **Elemental MCP** is the primary way agents reach the know
|
|
|
18
24
|
|
|
19
25
|
You can deploy **multiple agents** with different mixes of A–C.
|
|
20
26
|
|
|
27
|
+
## Wiring McpToolset
|
|
28
|
+
|
|
29
|
+
Use `StreamableHTTPConnectionParams` to connect to the Elemental MCP
|
|
30
|
+
server. Resolve the URL from `broadchurch.yaml` (gateway-proxied in
|
|
31
|
+
production) or fall back to `ELEMENTAL_MCP_URL` for local dev.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
import yaml
|
|
37
|
+
from google.adk.tools.mcp_tool import McpToolset
|
|
38
|
+
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
|
|
39
|
+
|
|
40
|
+
def _get_mcp_url(server_name: str = "elemental") -> str:
|
|
41
|
+
"""Resolve MCP server URL from env or broadchurch.yaml."""
|
|
42
|
+
env_url = os.environ.get("ELEMENTAL_MCP_URL")
|
|
43
|
+
if env_url:
|
|
44
|
+
return env_url
|
|
45
|
+
for candidate in [Path("broadchurch.yaml"), Path(__file__).parent / "broadchurch.yaml"]:
|
|
46
|
+
if candidate.exists():
|
|
47
|
+
config = yaml.safe_load(candidate.read_text()) or {}
|
|
48
|
+
gw = config.get("gateway", {})
|
|
49
|
+
org_id = config.get("tenant", {}).get("org_id", "")
|
|
50
|
+
if gw.get("url") and org_id:
|
|
51
|
+
return f"{gw['url'].rstrip('/')}/api/mcp/{org_id}/{server_name}/mcp"
|
|
52
|
+
return config.get("mcp", {}).get(server_name, "")
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
mcp_url = _get_mcp_url()
|
|
56
|
+
mcp_tools = [McpToolset(connection_params=StreamableHTTPConnectionParams(url=mcp_url))] if mcp_url else []
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Silent failure warning:** If `McpToolset` cannot connect (wrong
|
|
60
|
+
> transport, bad URL, server down), the agent starts with **zero MCP
|
|
61
|
+
> tools** and the LLM will hallucinate code instead of calling tools.
|
|
62
|
+
> Always verify tool count at startup:
|
|
63
|
+
>
|
|
64
|
+
> ```python
|
|
65
|
+
> if not mcp_url:
|
|
66
|
+
> raise RuntimeError("No MCP URL configured — check broadchurch.yaml or ELEMENTAL_MCP_URL env var")
|
|
67
|
+
> ```
|
|
68
|
+
|
|
21
69
|
## Python stack (typical)
|
|
22
70
|
|
|
23
71
|
- `google-adk` — agent framework
|
|
24
|
-
-
|
|
72
|
+
- ADK **McpToolset** + **`StreamableHTTPConnectionParams`** — `ELEMENTAL_MCP_URL` or gateway-proxied MCP URL
|
|
25
73
|
- **`psycopg2-binary`** — only when an agent **persists** to `DATABASE_URL`
|
|
26
74
|
|
|
27
75
|
```bash
|