Skip to content

Commit

Permalink
Added nearby location lookup and reverse geocode lookup for images. (m…
Browse files Browse the repository at this point in the history
…icrosoft#436)

Now when adding an image to the system we do a reverse geocode lookup we
also perform a nearby POI search.
  • Loading branch information
robgruen authored Nov 27, 2024
1 parent 6c29043 commit d10d68a
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 7 deletions.
1 change: 1 addition & 0 deletions ts/packages/aiclient/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AuthTokenProvider {

export enum AzureTokenScopes {
CogServices = "https://cognitiveservices.azure.com/.default",
AzureMaps = "https://atlas.microsoft.com/.default",
}

export function createAzureTokenProvider(
Expand Down
3 changes: 3 additions & 0 deletions ts/packages/aiclient/src/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export enum EnvVars {
AZURE_OPENAI_ENDPOINT_DALLE = "AZURE_OPENAI_ENDPOINT_DALLE",

OLLAMA_ENDPOINT = "OLLAMA_ENDPOINT",

AZURE_MAPS_ENDPOINT = "AZURE_MAPS_ENDPOINT",
AZURE_MAPS_CLIENTID = "AZURE_MAPS_CLIENTID",
}

export const MAX_PROMPT_LENGTH_DEFAULT = 1000 * 60;
Expand Down
2 changes: 2 additions & 0 deletions ts/packages/commonUtils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
"tsc": "tsc -b"
},
"dependencies": {
"@azure-rest/maps-search": "^2.0.0-beta.2",
"@azure/maps-search": "^1.0.0-beta.2",
"@typeagent/agent-sdk": "workspace:*",
"aiclient": "workspace:*",
"chalk": "^5.3.0",
Expand Down
4 changes: 3 additions & 1 deletion ts/packages/commonUtils/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function extractRelevantExifTags(exifTags: ExifReader.Tags) {
${exifTags.DateTime ? "Date Taken: " + exifTags.DateTime.value : ""}
${exifTags.OffsetTime ? "Offset Time: " + exifTags.OffsetTime.value : ""}
${exifTags.GPSLatitude ? "GPS Latitude: " + exifTags.GPSLatitude.description : ""}
${exifTags.GPSLongitude ? "GPS Longitude: " + exifTags.GPSLongitude.description : ""}
${exifTags.GPSLatitudeRef ? "GPS Latitude Reference: " + exifTags.GPSLatitudeRef.value : ""}
${exifTags.GPSLongitude ? "GPS Longitude Reference: " + exifTags.GPSLongitude.description : ""}
${exifTags.GPSLongitudeRef ? "GPS Longitude Reference: " + exifTags.GPSLongitudeRef?.value : ""}
${exifTags.GPSAltitudeRef ? "GPS Altitude Reference: " + exifTags.GPSAltitudeRef.value : ""}
${exifTags.GPSAltitude ? "GPS Altitude: " + exifTags.GPSAltitude.description : ""}
`;
Expand Down
2 changes: 2 additions & 0 deletions ts/packages/commonUtils/src/indexNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ export {
} from "./mimeTypes.js";

export { getObjectProperty, setObjectProperty } from "./objectProperty.js";

export * from "./location.js";
38 changes: 36 additions & 2 deletions ts/packages/commonUtils/src/jsonTranslator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import {
IncrementalJsonValueCallBack,
} from "./incrementalJsonParser.js";
import { CachedImageWithDetails, extractRelevantExifTags } from "./image.js";
import { apiSettingsFromEnv } from "../../aiclient/dist/openai.js";
import {
exifGPSTagToLatLong,
findNearbyPointsOfInterest,
reverseGeocode,
} from "./location.js";

export type InlineTranslatorSchemaDef = {
kind: "inline";
Expand Down Expand Up @@ -152,7 +158,7 @@ export function enableJsonTranslatorStreaming<T extends object>(
cb?: IncrementalJsonValueCallBack,
attachments?: CachedImageWithDetails[],
) => {
attachAttachments(attachments, promptPreamble);
await attachAttachments(attachments, promptPreamble);
return originalTranslate(
request,
initializeStreamingParser(promptPreamble, cb),
Expand All @@ -162,7 +168,7 @@ export function enableJsonTranslatorStreaming<T extends object>(
return translatorWithStreaming;
}

function attachAttachments(
async function attachAttachments(
attachments: CachedImageWithDetails[] | undefined,
promptPreamble?: string | PromptSection[],
) {
Expand All @@ -188,6 +194,34 @@ function attachAttachments(
type: "text",
text: `Image EXIF tags: \n${extractRelevantExifTags(attachments![i].exifTags)}`,
},
{
type: "text",
text: `Nearby Points of Interest: \n${JSON.stringify(
await findNearbyPointsOfInterest(
exifGPSTagToLatLong(
attachments![i].exifTags.GPSLatitude,
attachments![i].exifTags.GPSLatitudeRef,
attachments![i].exifTags.GPSLongitude,
attachments![i].exifTags.GPSLongitudeRef,
),
apiSettingsFromEnv(),
),
)}`,
},
{
type: "text",
text: `Reverse Geocode Results: \n${JSON.stringify(
await reverseGeocode(
exifGPSTagToLatLong(
attachments![i].exifTags.GPSLatitude,
attachments![i].exifTags.GPSLatitudeRef,
attachments![i].exifTags.GPSLongitude,
attachments![i].exifTags.GPSLongitudeRef,
),
apiSettingsFromEnv(),
),
)}`,
},
],
});
}
Expand Down
209 changes: 209 additions & 0 deletions ts/packages/commonUtils/src/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import {
AuthTokenProvider,
AzureTokenScopes,
createAzureTokenProvider,
getEnvSetting,
} from "aiclient";
import { StringArrayTag, TypedTag, XmpTag } from "exifreader";
import { ApiSettings, EnvVars } from "../../aiclient/dist/openai.js";
import { env } from "process";
import {
GeoJsonFeature,
GeoJsonFeatureCollection,
SearchAddressResult,
SearchAddressResultItem,
} from "@azure/maps-search";
import { AddressOutput } from "@azure-rest/maps-search";

/**
* Point of interest
*/
export type PointOfInterest = {
name?: String | undefined;
categories?: String[] | undefined;
freeFormAddress?: String | undefined;
position?: LatLong | undefined;
};

/**
* Reverse geocode lookup
*/
export type ReverseGeocodeAddressLookup = {
address?: AddressOutput | undefined;
confidence?: "High" | "Medium" | "Low" | undefined;
type: any;
};

/**
* Latitude, longitude coordinates
*/
export type LatLong = {
latitude: Number | String | undefined;
longitude: Number | String | undefined;
};

/**
* Helper method to convert EXIF tags to Latlong type
* @param exifLat - The exif latitude.
* @param exifLatRef - The exif latitude reference.
* @param exifLong - The exif longitude
* @param exifLongRef - The exif longitude reference.
* @returns The LatLong represented by the EXIF tag or undefined when data is incomplete
*/
export function exifGPSTagToLatLong(
exifLat:
| XmpTag
| TypedTag<[[number, number], [number, number], [number, number]]>
| undefined,
exifLatRef: XmpTag | StringArrayTag | undefined,
exifLong:
| XmpTag
| TypedTag<[[number, number], [number, number], [number, number]]>
| undefined,
exifLongRef: XmpTag | StringArrayTag | undefined,
): LatLong | undefined {
if (
exifLat !== undefined &&
exifLong !== undefined &&
exifLatRef !== undefined &&
exifLongRef !== undefined
) {
return {
latitude:
exifLatRef.value == "S"
? parseFloat("-" + exifLat.description)
: parseFloat(exifLat.description),
longitude:
exifLongRef.value == "W"
? parseFloat("-" + exifLong.description)
: parseFloat(exifLong.description),
};
}

return undefined;
}

/**
* Gets the nearby POIs for the supplied coordinate and search radius
* @param position - the position at which to do a nearby search
* @param settings - the API settings containing the endpoint to call
* @param radius - the search radius
* @returns A list of summarized nearby POIs
*/
export async function findNearbyPointsOfInterest(
position: LatLong | undefined,
settings: ApiSettings,
radius: Number = 10,
): Promise<PointOfInterest[]> {
if (position === undefined) {
return [];
}

const tokenProvider: AuthTokenProvider = createAzureTokenProvider(
AzureTokenScopes.AzureMaps,
);
const tokenResult = await tokenProvider.getAccessToken();
if (!tokenResult.success) {
return [];
}

try {
//let fuzzySearch = `${getEnvSetting(env, EnvVars.AZURE_MAPS_ENDPOINT)}search/fuzzy/json?api-version=1.0&query={lat,long}`
//let poi = `${getEnvSetting(env, EnvVars.AZURE_MAPS_ENDPOINT)}search/poi/{format}?api-version=1.0&lat={LAT}&lon={LON}`
const nearby = `${getEnvSetting(env, EnvVars.AZURE_MAPS_ENDPOINT)}search/nearby/json?api-version=1.0&lat=${position.latitude}&lon=${position.longitude}&radius=${radius}`;
const options: RequestInit = {
method: "GET",
headers: new Headers({
Authorization: `Bearer ${tokenResult.data}`,
"x-ms-client-id": `${getEnvSetting(env, EnvVars.AZURE_MAPS_CLIENTID)}`,
}),
};

// get the result
const response = await fetch(nearby, options);
let responseBody = await response.json();

// summarize results
const results: SearchAddressResult =
responseBody as SearchAddressResult;
const retVal: PointOfInterest[] = [];
results.results.map((result: SearchAddressResultItem) => {
if (result.type == "POI") {
retVal.push({
name: result.pointOfInterest?.name,
categories: result.pointOfInterest?.categories,
freeFormAddress: result.address.freeformAddress,
position: {
latitude: result.position[0],
longitude: result.position[1],
},
});
} else {
// TODO: handle more result types
throw new Error("Unknown nearby search result!");
}
});

// TODO: if there are no POI, can we just send back the address?
// Do we increase POI search radius until we find something in some predefined maximum area?
return retVal;
} catch (e) {
console.warn(`Error performing nearby POI lookup: ${e}`);
return [];
}
}

export async function reverseGeocode(
position: LatLong | undefined,
settings: ApiSettings,
): Promise<ReverseGeocodeAddressLookup[]> {
if (position === undefined) {
return [];
}

const tokenProvider: AuthTokenProvider = createAzureTokenProvider(
AzureTokenScopes.AzureMaps,
);
const tokenResult = await tokenProvider.getAccessToken();
if (!tokenResult.success) {
return [];
}

try {
let reverseGeocode = `${getEnvSetting(env, EnvVars.AZURE_MAPS_ENDPOINT)}reverseGeocode?api-version=2023-06-01&coordinates=${position.longitude},${position.latitude}`;

const options: RequestInit = {
method: "GET",
headers: new Headers({
Authorization: `Bearer ${tokenResult.data}`,
"x-ms-client-id": `${getEnvSetting(env, EnvVars.AZURE_MAPS_CLIENTID)}`,
}),
};

// get the result
const response = await fetch(reverseGeocode, options);
let responseBody = await response.json();

// summarize results
const results: GeoJsonFeatureCollection =
responseBody as GeoJsonFeatureCollection;
const retVal: ReverseGeocodeAddressLookup[] = [];
results.features?.map((result: GeoJsonFeature) => {
if (result.properties !== undefined) {
retVal.push({
address: result.properties.address,
confidence: result.properties.confidence,
type: result.properties.type,
});
}
});

return retVal;
} catch (e) {
console.warn(`Unable to perform reverse geocode lookup: ${e}`);
return [];
}
}
42 changes: 42 additions & 0 deletions ts/packages/commonUtils/test/location.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { XmpTag } from "exifreader";
import { exifGPSTagToLatLong, LatLong } from "../src/location.js";

describe("Location Tests", () => {
it("EXIF LatLong to LatLong", () => {
const lat: XmpTag = {
value: "47.6204",
description: "47.6204",
attributes: {},
};
const long: XmpTag = {
value: "122.3491",
description: "122.3491",
attributes: {},
};
const latRef: XmpTag = {
value: "N",
description: "North Latitude",
attributes: {},
};
const longRef: XmpTag = {
value: "W",
description: "West Longitude",
attributes: {},
};

const ll: LatLong = exifGPSTagToLatLong(lat, latRef, long, longRef)!;
expect(ll.latitude == "47.6204");
expect(ll.longitude == "-122.3491");

const llu: LatLong | undefined = exifGPSTagToLatLong(
lat,
latRef,
undefined,
longRef,
);
expect(llu === undefined);
});
});
2 changes: 1 addition & 1 deletion ts/packages/shell/src/main/shellSettingsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const defaultSettings: ShellSettingsType = {
notifyFilter: "error;warning;",
tts: false,
ttsSettings: {},
agentGreeting: false,
agentGreeting: true,
multiModalContent: true,
devUI: false,
partialCompletion: true,
Expand Down
6 changes: 4 additions & 2 deletions ts/packages/shell/src/renderer/assets/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,10 @@ body {
padding: 5px;
background-color: @chat-input-bg;
align-self: flex-end;
margin: 10px 10px 10px 20px;
width: calc(100% - 40px);
margin: 10px 10px 10px 10px;
width: calc(
100% - 20px
); // Note: subtracted value should match horizontal margin total from line above
border: solid 1px rgb(224, 224, 224);
border-radius: 6px;
position: relative;
Expand Down
Loading

0 comments on commit d10d68a

Please sign in to comment.