Skip to content

Commit f94b9c1

Browse files
committed
fix(serper,linkedin): map the /places shape, restore real search extras
The places mapper had been written against the /maps response, not /places. serper.dev's tab-switcher has ten panels and BOTH Maps and Places return a top-level 'places' array with different item shapes; Maps is the one carrying a top-level 'll'. An earlier extraction collapsed the two. Consequences: category read item.type and snippet read item.description, both of which exist only on /maps, so both were permanently undefined. The test fixture was built from the same Maps example, so it confirmed the wrong endpoint. Rebuilt from the real /places example, plus two guard tests that a Maps-only key must not produce output. /places genuinely has no link and no snippet analogue, so those stay unmapped; latitude and longitude are now surfaced, which the find-local-businesses skill needs to rank by proximity and previously could not. Also restored knowledgeGraph, peopleAlsoAsk and relatedSearches, which /search really returns and which a previous pass deleted as phantoms. They were never populated -- but deleting the declarations meant discarding real, already-billed data. Gated on the search vertical so other verticals cannot emit them. answerBox and topStories stay deleted; they appear in none of the ten panels. images and shopping mappers also read a snippet neither endpoint returns. linkedin: /v2/ugcPosts returns a ugcPost URN, not the legacy urn:li:share: family the descriptions and every test fixture claimed.
1 parent b6c790d commit f94b9c1

7 files changed

Lines changed: 390 additions & 79 deletions

File tree

apps/sim/blocks/blocks/serper.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ export const SerperBlock: BlockConfig<SearchResponse> = {
107107
},
108108
outputs: {
109109
searchResults: { type: 'json', description: 'Search results data' },
110+
knowledgeGraph: {
111+
type: 'json',
112+
description: 'Google knowledge panel. Web search vertical only, when Google renders one',
113+
},
114+
peopleAlsoAsk: {
115+
type: 'json',
116+
description: 'Google "People also ask" entries. Web search vertical only',
117+
},
118+
relatedSearches: {
119+
type: 'json',
120+
description: 'Google "Related searches" queries. Web search vertical only',
121+
},
110122
},
111123
}
112124

@@ -208,7 +220,7 @@ export const SerperBlockMeta = {
208220
description:
209221
'Use the Serper places operation to find local businesses for a query and area, and rank them.',
210222
content:
211-
'# Find Local Businesses\n\nPull Google Maps style local results for a query in a target area.\n\n## Steps\n1. Run the places operation with a query that includes the business type and location (for example coffee shops in Seattle), setting the country and language to scope results.\n2. Read each place result: name, address, rating, review count, category, and phone or website where present.\n3. Rank or filter the results by rating, review volume, or proximity to the target area.\n\n## Output\nReturn a ranked list of local businesses with name, address, rating, and review count, noting the top candidates for the query.',
223+
'# Find Local Businesses\n\nPull Google Maps style local results for a query in a target area.\n\n## Steps\n1. Run the places operation with a query that includes the business type and location (for example coffee shops in Seattle), setting the country and language to scope results.\n2. Read each place result: title (the business name), address, rating, ratingCount (the review count), category, phoneNumber, website, and latitude/longitude where present. Places results carry no link and no snippet.\n3. Rank or filter the results by rating, review volume, or by latitude/longitude distance from the target area.\n\n## Output\nReturn a ranked list of local businesses with name, address, rating, and review count, noting the top candidates for the query.',
212224
},
213225
],
214226
} as const satisfies BlockMeta

apps/sim/tools/linkedin/share_post.test.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,31 +37,33 @@ describe('linkedInSharePostTool.postProcess', () => {
3737
mockFetch.mockResolvedValue(
3838
new Response(null, {
3939
status: 201,
40-
headers: { 'x-restli-id': 'urn:li:share:123' },
40+
headers: { 'x-restli-id': 'urn:li:ugcPost:7264398217238917120' },
4141
})
4242
)
4343

4444
const result = await linkedInSharePostTool.postProcess!(profileResult, params, executeTool)
4545

46-
expect(result.output.postId).toBe('urn:li:share:123')
46+
expect(result.output.postId).toBe('urn:li:ugcPost:7264398217238917120')
4747
expect(result.success).toBe(true)
4848
expect(result.error).toBeUndefined()
4949
})
5050

51-
it('builds the member-viewable post URL from the returned share URN', async () => {
51+
it('builds the member-viewable post URL from the returned ugcPost URN', async () => {
5252
mockFetch.mockResolvedValue(
5353
new Response(null, {
5454
status: 201,
55-
headers: { 'x-restli-id': 'urn:li:share:123' },
55+
headers: { 'x-restli-id': 'urn:li:ugcPost:7264398217238917120' },
5656
})
5757
)
5858

5959
const result = await linkedInSharePostTool.postProcess!(profileResult, params, executeTool)
6060

61-
expect(result.output.postUrl).toBe('https://www.linkedin.com/feed/update/urn:li:share:123/')
61+
expect(result.output.postUrl).toBe(
62+
'https://www.linkedin.com/feed/update/urn:li:ugcPost:7264398217238917120/'
63+
)
6264
})
6365

64-
it('leaves the post URL undefined when there is no share URN to build it from', async () => {
66+
it('leaves the post URL undefined when there is no ugcPost URN to build it from', async () => {
6567
mockFetch.mockResolvedValue(new Response(null, { status: 201 }))
6668

6769
const result = await linkedInSharePostTool.postProcess!(profileResult, params, executeTool)
@@ -96,7 +98,7 @@ describe('linkedInSharePostTool.postProcess', () => {
9698
mockFetch.mockResolvedValue(
9799
new Response(null, {
98100
status: 201,
99-
headers: { 'x-restli-id': 'urn:li:share:123' },
101+
headers: { 'x-restli-id': 'urn:li:ugcPost:7264398217238917120' },
100102
})
101103
)
102104

@@ -124,4 +126,16 @@ describe('linkedInSharePostTool.outputs', () => {
124126
it('declares exactly the keys postProcess returns', () => {
125127
expect(Object.keys(linkedInSharePostTool.outputs ?? {}).sort()).toEqual(['postId', 'postUrl'])
126128
})
129+
130+
it('names the ugcPost URN family that /v2/ugcPosts actually returns', () => {
131+
const descriptions = [
132+
linkedInSharePostTool.outputs?.postId?.description ?? '',
133+
linkedInSharePostTool.outputs?.postUrl?.description ?? '',
134+
]
135+
136+
for (const description of descriptions) {
137+
expect(description).toContain('ugcPost')
138+
expect(description).not.toMatch(/\bshare\b/i)
139+
}
140+
})
127141
})

apps/sim/tools/linkedin/share_post.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import type { ToolConfig } from '@/tools/types'
1111
const logger = createLogger('LinkedInSharePost')
1212

1313
/**
14-
* LinkedIn's documented permalink prefix for a share URN. LinkedIn describes the resulting URL as
15-
* viewable by an authorized member, not as a guaranteed public permalink.
14+
* LinkedIn's documented permalink prefix for a `urn:li:ugcPost:` URN — the family `/v2/ugcPosts`
15+
* returns. LinkedIn describes the resulting URL as viewable by an authorized member, not as a
16+
* guaranteed public permalink.
1617
*/
1718
const LINKEDIN_FEED_UPDATE_BASE = 'https://www.linkedin.com/feed/update/'
1819

@@ -158,13 +159,13 @@ export const linkedInSharePostTool: ToolConfig<SharePostParams, SharePostRespons
158159
postId: {
159160
type: 'string',
160161
description:
161-
'URN of the created share, read from the `x-restli-id` response header. Absent when LinkedIn omits that header.',
162+
'The `urn:li:ugcPost:` URN of the created post, read from the `x-restli-id` response header. Absent when LinkedIn omits that header.',
162163
optional: true,
163164
},
164165
postUrl: {
165166
type: 'string',
166167
description:
167-
'LinkedIn URL of the created post. Viewable by an authorized LinkedIn member — not a guaranteed public permalink. Absent when no share URN was returned.',
168+
'LinkedIn URL of the created post. Viewable by an authorized LinkedIn member — not a guaranteed public permalink. Absent when no ugcPost URN was returned.',
168169
optional: true,
169170
},
170171
},

apps/sim/tools/linkedin/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export interface SharePostParams {
3535

3636
export interface SharePostResponse extends ToolResponse {
3737
output: {
38-
/** Share URN from the `x-restli-id` response header; absent if LinkedIn omits the header. */
38+
/**
39+
* The `urn:li:ugcPost:` URN of the created post, from the `x-restli-id` response header;
40+
* absent if LinkedIn omits the header. `/v2/ugcPosts` returns a ugcPost URN, never a
41+
* `urn:li:share:` URN from the legacy Shares API.
42+
*/
3943
postId?: string
4044
/**
4145
* LinkedIn's `feed/update/<urn>` permalink for the created post. LinkedIn documents this URL

0 commit comments

Comments
 (0)