-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #9 from DaigakuNakayoshi/add-llm
LangChain導入とGeminiを使った旅行プラン作成のサンプル作成
- Loading branch information
Showing
18 changed files
with
942 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
FRONTEND_GOOGLE_API_KEY="xxx" | ||
FRONTEND_GOOGLE_API_KEY="xxx" | ||
FRONTEND_GOOGLE_GEMINI_API_KEY="xxx" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,3 +20,6 @@ dist-ssr | |
*.njsproj | ||
*.sln | ||
*.sw? | ||
|
||
# for LLM context | ||
*repomix* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
import { PromptTemplate } from "@langchain/core/prompts"; | ||
import { | ||
RunnableLambda, | ||
RunnablePassthrough, | ||
RunnableSequence, | ||
} from "@langchain/core/runnables"; | ||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai"; | ||
import { StructuredOutputParser } from "langchain/output_parsers"; | ||
import { z } from "zod"; | ||
|
||
const model = new ChatGoogleGenerativeAI({ | ||
modelName: "gemini-2.0-flash-exp", | ||
apiKey: import.meta.env.FRONTEND_GOOGLE_GEMINI_API_KEY, | ||
}); | ||
|
||
const planSchema = z.object({ | ||
plan: z.object({ | ||
title: z.string().describe("title of the travel plan"), | ||
description: z.string().describe("description of the travel plan"), | ||
steps: z | ||
.array( | ||
z.object({ | ||
stepPerDay: z.number().describe("day of the travel plan"), | ||
detailSteps: z | ||
.array( | ||
z.object({ | ||
time: z.string().describe("time of the step"), | ||
description: z.string().describe("description of the step"), | ||
}), | ||
) | ||
.describe("steps of the day"), | ||
}), | ||
) | ||
.describe("steps of the travel plan, grouped by day, with time"), | ||
travel_cost: z | ||
.object({ | ||
total: z.string().describe("total estimated travel cost"), | ||
transportation: z.string().describe("estimated transportation cost"), | ||
accommodation: z.string().describe("estimated accommodation cost"), | ||
food: z.string().describe("estimated food cost"), | ||
activities: z.string().describe("estimated activities cost"), | ||
other: z.string().describe("other estimated costs"), | ||
total_per_person: z | ||
.string() | ||
.describe("total estimated travel cost per person"), | ||
}) | ||
.describe("estimated travel cost"), | ||
waypoints: z | ||
.array( | ||
z.object({ | ||
name: z.string().describe("name of the waypoint"), | ||
address: z.string().describe("address of the waypoint"), | ||
}), | ||
) | ||
.describe("waypoints of the travel plan"), | ||
origin: z | ||
.object({ | ||
name: z.string().describe("name of the starting point"), | ||
address: z.string().describe("address of the starting point"), | ||
}) | ||
.describe("starting point of the travel plan"), | ||
destination: z | ||
.object({ | ||
name: z.string().describe("name of the final destination"), | ||
address: z.string().describe("address of the final destination"), | ||
}) | ||
.describe("final destination of the travel plan"), | ||
departure_location: z | ||
.string() | ||
.describe("departure location of the travel plan"), | ||
google_maps_waypoints: z | ||
.array( | ||
z.object({ | ||
name: z.string().describe("name of the waypoint"), | ||
address: z.string().describe("address of the waypoint"), | ||
}), | ||
) | ||
.describe("waypoints for google maps directions"), | ||
google_maps_origin: z | ||
.object({ | ||
name: z.string().describe("name of the starting point for google maps"), | ||
address: z | ||
.string() | ||
.describe("address of the starting point for google maps"), | ||
}) | ||
.describe("starting point of the travel plan for google maps"), | ||
google_maps_destination: z | ||
.object({ | ||
name: z | ||
.string() | ||
.describe("name of the final destination for google maps"), | ||
address: z | ||
.string() | ||
.describe("address of the final destination for google maps"), | ||
}) | ||
.describe("final destination of the travel plan for google maps"), | ||
}), | ||
}); | ||
|
||
const outputParser = StructuredOutputParser.fromZodSchema(planSchema); | ||
|
||
const prompt = PromptTemplate.fromTemplate( | ||
`# 必要要件 | ||
- あなたは旅行プランを考えるプランナーです。 | ||
- 渡す情報は日程、出発地(都道府県)、目的地(空港、新幹線駅など)、希望する観光内容、コンセプト | ||
- 上記の情報を用いて、旅行プランを作成してください。出発地、最終目的地は、特別な理由(新幹線で到着して飛行機で帰るなど)がない限り、同じ場所か非常に近い場所になるようにしてください。経由地は、緯度経度変換させるので、住所を取得してください。もし、住所が不明な場合は、場所の正式名称を返却してください。「現在地」という文字列は使用しないでください。 | ||
- 旅行プランの最後は出発地に戻るようにしてください。 | ||
- 旅行費用は出発地から目的地までの往復費用を含めてください。現実的な価格で算出してください。 | ||
- 旅行費用は一人当たりの費用も算出してください。total_per_personフィールドに記載してください。 | ||
- 各移動手段の所要時間も考慮して、無理のない旅行プランを作成してください。 | ||
- 過度に楽観的な旅行プランは避けてください。 | ||
- Google Mapの経路は、旅行先の経由地のみを含めてください。出発地は含めないでください。 | ||
- Google Mapの出発地と目的地は、旅行先の出発地と目的地のみを含めてください。 | ||
- 検索内容は交通手段、宿泊施設、予算を含みます | ||
- ユーザーの興味関心を考慮して、旅行プランを作成してください。 | ||
# 表示する内容 | ||
- 出発地~目的地への旅行プランであること | ||
- 旅行プランの概要を100文字程度で簡潔に記載すること | ||
- 特に以下は考慮された概要にしてください | ||
- 何泊何日、何人の旅行か | ||
- 重要視したポイントはどこか | ||
- テーマがある場合は記載すること(贅沢な旅行や自然を楽しむ旅行など) | ||
- 各日程ごとに行く場所と時系列を大項目で、詳細な内容(目安の時間)などを小項目で記載すること | ||
- 日程と日程の間の移動手段は大項目で記載すること | ||
- そのほか、時間内は難しいがおすすめの観光地・食事があればその他欄として記載すること | ||
- 各項目を行う上での予算を別の見出しで記載すること | ||
- お店の紹介をする際は参考URLとして記載する | ||
{format_instructions} | ||
ユーザーのリクエスト: {input} | ||
`, | ||
); | ||
|
||
export const agent = RunnableSequence.from([ | ||
RunnablePassthrough.assign({ | ||
input: (input: { input: string }) => input.input, | ||
}), | ||
RunnableLambda.from(() => | ||
prompt.partial({ | ||
format_instructions: outputParser.getFormatInstructions(), | ||
}), | ||
), | ||
model, | ||
outputParser, | ||
]); |
151 changes: 151 additions & 0 deletions
151
src/components/samples/agent/GoogleMapWithDirection.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import { Box } from "@chakra-ui/react"; | ||
import { | ||
APIProvider, | ||
Map as GoogleMap, | ||
useMap, | ||
useMapsLibrary, | ||
} from "@vis.gl/react-google-maps"; | ||
import { useEffect, useState } from "react"; | ||
|
||
type Waypoint = { name: string; address: string }; | ||
type Location = { name: string; address: string }; | ||
|
||
interface DirectionsProps { | ||
waypoints?: Waypoint[]; | ||
origin?: Location; | ||
destination?: Location; | ||
} | ||
|
||
export function GoogleMapWithDirection({ | ||
waypoints, | ||
origin, | ||
destination, | ||
}: { | ||
waypoints: Waypoint[]; | ||
origin: Location; | ||
destination: Location; | ||
}) { | ||
const API_KEY = import.meta.env.FRONTEND_GOOGLE_API_KEY; | ||
return ( | ||
<div style={{ width: "100%", height: "500px" }}> | ||
<APIProvider apiKey={API_KEY}> | ||
<GoogleMap | ||
defaultZoom={8} | ||
gestureHandling={"greedy"} | ||
fullscreenControl={false} | ||
> | ||
<Directions | ||
waypoints={waypoints} | ||
origin={origin} | ||
destination={destination} | ||
/> | ||
</GoogleMap> | ||
</APIProvider> | ||
</div> | ||
); | ||
} | ||
|
||
// @see: https://developers.google.com/maps/documentation/javascript/directions?hl=ja | ||
function Directions({ waypoints, origin, destination }: DirectionsProps) { | ||
const map = useMap(); | ||
const routesLibrary = useMapsLibrary("routes"); | ||
const [directionsService, setDirectionsService] = | ||
useState<google.maps.DirectionsService>(); | ||
const [directionsRenderer, setDirectionsRenderer] = | ||
useState<google.maps.DirectionsRenderer>(); | ||
const [routes, setRoutes] = useState<google.maps.DirectionsRoute[]>([]); | ||
const [routeIndex] = useState(0); | ||
const selected = routes[routeIndex]; | ||
const leg = selected?.legs[0]; | ||
|
||
// Initialize directions service and renderer | ||
useEffect(() => { | ||
if (!routesLibrary || !map) return; | ||
setDirectionsService(new routesLibrary.DirectionsService()); | ||
setDirectionsRenderer(new routesLibrary.DirectionsRenderer({ map })); | ||
}, [routesLibrary, map]); | ||
|
||
// Use directions service | ||
useEffect(() => { | ||
if ( | ||
!directionsService || | ||
!directionsRenderer || | ||
!waypoints || | ||
!origin || | ||
!destination | ||
) | ||
return; | ||
|
||
const waypointsForDirections = waypoints.map((waypoint) => ({ | ||
location: waypoint.address, | ||
stopover: true, | ||
})); | ||
|
||
directionsService | ||
.route({ | ||
origin: origin.address || origin.name, | ||
waypoints: waypointsForDirections, | ||
destination: destination.address || destination.name, | ||
travelMode: google.maps.TravelMode.DRIVING, | ||
provideRouteAlternatives: true, | ||
avoidHighways: true, | ||
}) | ||
.then((response) => { | ||
directionsRenderer.setDirections(response); | ||
setRoutes(response.routes); | ||
}); | ||
|
||
return () => directionsRenderer.setMap(null); | ||
}, [directionsService, directionsRenderer, waypoints, origin, destination]); | ||
|
||
// Update direction route | ||
useEffect(() => { | ||
if (!directionsRenderer) return; | ||
|
||
directionsRenderer.setRouteIndex(routeIndex); | ||
}, [routeIndex, directionsRenderer]); | ||
|
||
if (!leg) return null; | ||
|
||
return ( | ||
<Box | ||
bg="white" | ||
p={4} | ||
borderRadius="md" | ||
shadow="lg" | ||
maxW="sm" | ||
maxH="80vh" | ||
overflowY="auto" | ||
fontSize={10} | ||
> | ||
<div className="directions"> | ||
<h2>{selected.summary || "ルート"}</h2> | ||
|
||
{selected.legs.map((leg, index) => ( | ||
<div key={`${leg.start_address}-${leg.end_address}`}> | ||
<p> | ||
{index === 0 ? origin?.name : waypoints?.[index]?.name} ( | ||
{routes.length > 0 && index === 0 ? "Start" : "Waypoint"} | ||
) | ||
<br /> | ||
{leg.start_address.split(",")[0]} | ||
<br />→{" "} | ||
{index === selected.legs.length - 1 | ||
? destination?.name | ||
: waypoints?.[index + 1]?.name}{" "} | ||
( | ||
{routes.length > 0 && index === selected.legs.length - 1 | ||
? "End" | ||
: "Waypoint"} | ||
) | ||
<br /> | ||
{leg.end_address.split(",")[0]} | ||
</p> | ||
<p>距離: {leg.distance?.text}</p> | ||
<p>所要時間: {leg.duration?.text}</p> | ||
</div> | ||
))} | ||
</div> | ||
</Box> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Field as ChakraField } from "@chakra-ui/react"; | ||
import * as React from "react"; | ||
|
||
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> { | ||
label?: React.ReactNode; | ||
helperText?: React.ReactNode; | ||
errorText?: React.ReactNode; | ||
optionalText?: React.ReactNode; | ||
} | ||
|
||
export const Field = React.forwardRef<HTMLDivElement, FieldProps>( | ||
function Field(props, ref) { | ||
const { label, children, helperText, errorText, optionalText, ...rest } = | ||
props; | ||
return ( | ||
<ChakraField.Root ref={ref} {...rest}> | ||
{label && ( | ||
<ChakraField.Label> | ||
{label} | ||
<ChakraField.RequiredIndicator fallback={optionalText} /> | ||
</ChakraField.Label> | ||
)} | ||
{children} | ||
{helperText && ( | ||
<ChakraField.HelperText>{helperText}</ChakraField.HelperText> | ||
)} | ||
{errorText && ( | ||
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText> | ||
)} | ||
</ChakraField.Root> | ||
); | ||
}, | ||
); |
Oops, something went wrong.