Skip to content

Commit

Permalink
Merge pull request #9 from DaigakuNakayoshi/add-llm
Browse files Browse the repository at this point in the history
LangChain導入とGeminiを使った旅行プラン作成のサンプル作成
  • Loading branch information
takatea authored Jan 16, 2025
2 parents 46a11b5 + f761140 commit 2047d99
Show file tree
Hide file tree
Showing 18 changed files with 942 additions and 106 deletions.
3 changes: 2 additions & 1 deletion .env.local.sample
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"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?

# for LLM context
*repomix*
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
"dependencies": {
"@chakra-ui/react": "^3.2.0",
"@emotion/react": "^11.13.5",
"@langchain/google-genai": "^0.1.6",
"@tanstack/react-router": "^1.82.2",
"@vis.gl/react-google-maps": "^1.4.2",
"langchain": "^0.3.11",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0"
Expand Down
148 changes: 148 additions & 0 deletions src/agents/travelPlanner.ts
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 src/components/samples/agent/GoogleMapWithDirection.tsx
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>
);
}
33 changes: 33 additions & 0 deletions src/components/ui/field.tsx
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>
);
},
);
Loading

0 comments on commit 2047d99

Please sign in to comment.