Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ firebase emulators:start --only functions

```bash
firebase deploy --only functions
```
```

## Reference
- https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/develop/langchain#vertex-ai-extension
44 changes: 44 additions & 0 deletions functions/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from vertexai import agent_engines, init
from langchain_google_vertexai import HarmBlockThreshold, HarmCategory

init(project="ai-agent-hackathon-3-471422", location="us-central1")
Comment on lines +3 to +4
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded project ID should be moved to environment variables or configuration file to avoid exposing sensitive project information in source code.

Suggested change
init(project="ai-agent-hackathon-3-471422", location="us-central1")
import os
init(
project=os.environ.get("VERTEXAI_PROJECT_ID"),
location="us-central1"
)

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +4

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 [Security Risk]: The project ID is hardcoded in the code. This is a sensitive configuration that should be managed through environment variables or a secure configuration system1.

Suggested change
init(project="ai-agent-hackathon-3-471422", location="us-central1")
from os import getenv
from vertexai import agent_engines, init
init(project=getenv("GOOGLE_CLOUD_PROJECT"), location=getenv("GOOGLE_CLOUD_LOCATION", "us-central1"))

Footnotes

  1. CWE-798: Use of Hard-coded Credentials - https://cwe.mitre.org/data/definitions/798.html

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Hardcoding the project ID and location can lead to issues when deploying to different environments (e.g., staging, production) and is a security risk if the repository is public. It's better to load these values from environment variables. You will need to add import os at the top of the file.

Suggested change
init(project="ai-agent-hackathon-3-471422", location="us-central1")
init(project=os.environ.get("GCP_PROJECT"), location=os.environ.get("GCP_LOCATION", "us-central1"))


safety_settings = {
HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_ONLY_HIGH,
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE,
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider setting SEXUALLY_EXPLICIT content blocking to at least BLOCK_MEDIUM_AND_ABOVE for better content safety. The current BLOCK_NONE setting might allow inappropriate content through.

}

model_kwargs = {
# temperature (float): The sampling temperature controls the degree of
# randomness in token selection.
"temperature": 0.28,
# max_output_tokens (int): The token limit determines the maximum amount of
# text output from one prompt.
"max_output_tokens": 1000,
# top_p (float): Tokens are selected from most probable to least until
# the sum of their probabilities equals the top-p value.
"top_p": 0.95,
# top_k (int): The next token is selected from among the top-k most
# probable tokens. This is not supported by all model versions. See
# https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/image-understanding#valid_parameter_values
# for details.
"top_k": None,
# safety_settings (Dict[HarmCategory, HarmBlockThreshold]): The safety
# settings to use for generating content.
# (you must create your safety settings using the previous step first).
"safety_settings": safety_settings,
}

model = "gemini-2.0-flash"

agent = agent_engines.LangchainAgent(
model=model, # Required.
model_kwargs=model_kwargs, # Optional.
)

if __name__ == "__main__":
response = agent.query(input="What is the exchange rate from US dollars to SEK today?")
print(response)
54 changes: 54 additions & 0 deletions functions/gee_ndvi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import ee
import requests

def initialize_gee():
"""認証と初期化。ローカル認証が必要な場合はee.Authenticate()を使う。"""
try:
ee.Initialize()
except Exception:
Comment on lines +6 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 [Error Handling]: The exception handling is too broad. Catching all exceptions can mask serious issues. Consider catching specific exceptions and adding proper error logging.

Suggested change
try:
ee.Initialize()
except Exception:
def initialize_gee():
"""認証と初期化。ローカル認証が必要な場合はee.Authenticate()を使う。"""
try:
ee.Initialize()
except ee.EEException as e:
logging.info("Initial initialization failed, attempting authentication")
try:
ee.Authenticate()
ee.Initialize()
except ee.EEException as e:
logging.error(f"Failed to initialize Google Earth Engine: {e}")
raise

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catching a generic Exception is too broad and can hide other unexpected issues. It's better to catch the specific exception that ee.Initialize() might raise, which is ee.ee_exception.EEException. This makes the error handling more robust and explicit.

Suggested change
except Exception:
except ee.ee_exception.EEException:

ee.Authenticate()
ee.Initialize()

def get_ndvi_image(lat, lon, start_date, end_date, out_path):
"""
指定座標・期間でNDVI画像を生成し、ファイル保存する。
Args:
lat (float): 緯度
lon (float): 経度
start_date (str): 取得開始日(YYYY-MM-DD)
end_date (str): 取得終了日(YYYY-MM-DD)
out_path (str): 保存先ファイルパス
Returns:
out_path (str): 保存した画像ファイルパス
"""
# Sentinel-2画像コレクション取得
point = ee.Geometry.Point([lon, lat])
collection = ee.ImageCollection('COPERNICUS/S2') \
.filterBounds(point) \
.filterDate(start_date, end_date) \
.sort('CLOUD_COVER')
image = collection.first()
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code assumes at least one image exists in the collection, but if no Sentinel-2 images are found for the specified location and date range, collection.first() will return null, causing subsequent operations to fail.

Suggested change
image = collection.first()
image = collection.first()
if image is None:
raise ValueError("No Sentinel-2 images found for the specified location and date range.")

Copilot uses AI. Check for mistakes.
# NDVI計算
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
Comment on lines +30 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The collection.first() call might return None if no images are found for the given location and date range. This would cause an AttributeError when image.normalizedDifference is called. You should add a check to handle this case gracefully.

Suggested change
image = collection.first()
# NDVI計算
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
image = collection.first()
if not image:
raise ValueError("No image found for the specified criteria.")
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')

# 可視化パラメータ
vis_params = {'min': 0, 'max': 1, 'palette': ['blue', 'white', 'green']}
# 画像をエクスポート
url = ndvi.getThumbURL({'region': point.buffer(10000).bounds().getInfo(), 'dimensions': 512, 'format': 'png', **vis_params})
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The buffer distance (10000) and image dimensions (512) are magic numbers that should be defined as named constants or function parameters for better maintainability.

Copilot uses AI. Check for mistakes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value 10000 (presumably meters for the buffer) is a magic number. It's better to define it as a constant with a descriptive name at the top of the function or module, like REGION_BUFFER_METERS = 10000. This improves readability and makes it easier to change the value if needed.

# 画像ダウンロード
r = requests.get(url)
Copy link

Copilot AI Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTTP request lacks error handling. If the request fails or returns a non-200 status code, the function will still attempt to write the response content to file, potentially creating invalid image files.

Suggested change
r = requests.get(url)
r = requests.get(url)
try:
r.raise_for_status()
except requests.RequestException as e:
raise RuntimeError(f"Failed to download NDVI image: {e}")

Copilot uses AI. Check for mistakes.
with open(out_path, 'wb') as f:
Comment on lines +38 to +39

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 [Security Risk]: Writing files without path validation could lead to path traversal vulnerabilities1. Add validation for the out_path parameter.

Suggested change
r = requests.get(url)
with open(out_path, 'wb') as f:
# Validate and sanitize file path
safe_path = os.path.abspath(os.path.normpath(out_path))
if not safe_path.endswith('.png'):
raise ValueError("Output path must end with .png")
with open(safe_path, 'wb') as f:
f.write(r.content)

Footnotes

  1. CWE-22: Improper Limitation of a Pathname to a Restricted Directory - https://cwe.mitre.org/data/definitions/22.html

f.write(r.content)
Comment on lines +38 to +40

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The requests.get() call does not handle potential HTTP errors (e.g., 4xx or 5xx status codes). If the URL fetch fails, r.content might be an error page. It's good practice to check the response status. A simple way is to call r.raise_for_status() after the request, which will raise an HTTPError for bad responses.

Suggested change
r = requests.get(url)
with open(out_path, 'wb') as f:
f.write(r.content)
r = requests.get(url)
r.raise_for_status()
with open(out_path, 'wb') as f:
f.write(r.content)

return out_path

if __name__ == "__main__":
# サンプル座標(東京駅付近)
lat = 35.681236
lon = 139.767125
start_date = "2023-08-01"
end_date = "2023-08-31"
out_path = "ndvi_sample.png"
print("GEE認証・初期化中...")
initialize_gee()
print("NDVI画像生成中...")
result = get_ndvi_image(lat, lon, start_date, end_date, out_path)
print(f"画像保存先: {result}")
3 changes: 2 additions & 1 deletion functions/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
firebase_functions~=0.1.0
firebase_functions~=0.1.0
earthengine-api

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider pinning dependency versions for better reproducibility and security. Unpinned dependencies can lead to unexpected behavior or vulnerabilities if a package releases breaking changes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's a good practice to pin dependency versions to ensure reproducible builds and avoid unexpected breakages from upstream changes. Please specify a version for earthengine-api, for example earthengine-api==0.1.393 (the latest version as of writing).

earthengine-api==0.1.393

Loading