Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/venv/
src/__pycache__/
src/__pycache__/
build/
test-package.zip
output.json
79 changes: 62 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,73 @@
# TermLens_BE
약관 요약과 중요 조항에 대한 평가를 제공합니다.

# 아키텍처
AWS Lambda
- AWS Lambda
- AWS Bedrock
# 테스트
LocalStack의 실행을 위해 docker 설치가 필요합니다.
## LocalStack 설치
```bash
# pipx 설치
sudo apt update
sudo apt install pipx
# 잘 설치되었는지 확인
pipx --version

# Getting Started
## 개발환경 구축
bash를 기준으로 작성됨
# LocalStack, awslocal 설치
pipx install localstack --include-deps
pipx install awscli-local[ver1] --include-deps
source ~/.bashrc
# 잘 설치되었는지 확인
localstack --version
awslocal --version
```
wsl 환경에서 `pip install` 명령을 전역으로 쓸 수 없어 pipx를 사용합니다. 전역으로 localstack 및 awslocal을 설치하여 사용 가능한 경우 pipx의 설치가 필요하지 않습니다.
## 로컬 테스트
**docker가 실행된 상태에서** `localstack start` 명령으로 LocalStack을 구동합니다. 이후 터미널에 `Ready`가 나타나면 다른 터미널 창을 열고, 프로젝트 디렉토리에서 아래 작업을 수행합니다.

우선, LocalStack에 업로드할 zip파일을 생성합니다.
```bash
sudo apt install python3.12-venv
python3 -m venv venv # 가상환경 생성
source venv/bin/activate # 가상환경 사용
pip install -r requirements.txt -t build/ --upgrade
cp src/*.py build/
cd build
zip -r ../test-package.zip .
cd ..
```

이후부터 작업 시 `source venv/bin/activate` 명령으로 가상환경을 실행한 후 작업
- `(venv) 사용자명@컴퓨터명:~/.../TermLens_BE` 처럼 앞에 `(venv)`가 붙는지 확인
ARM 환경에서는 `pip install...` 명령 대신 아래의 명령을 사용해주세요. LocalStack 및 AWS Lambda 환경에서는 amd64(x86-64)을 기반으로 작동하나, ARM 기반 기기에서 해당 명령으로 설치하게 되면 ARM용 바이너리를 받아와 LocalStack에서 실행하지 못합니다. 아래 명령으로 생성된 `build/` 디렉토리 및 `test-package.zip` 파일은 삭제 시 root 권한이 필요합니다.
```bash
docker run --platform linux/amd64 --rm -v "$(pwd)":/var/task --entrypoint "" public.ecr.aws/lambda/python:3.12 /bin/sh -c "pip install -r requirements.txt -t build/ --upgrade && cp src/*.py build/ && cd build && dnf install -y zip && zip -r ../test-package.zip . && cd .."
```

## 컨벤션
### 커밋 메시지
커밋 메시지의 작성법은 [컨벤셔널 커밋](https://www.conventionalcommits.org/ko/v1.0.0/)을 따릅니다. `feat: `, `fix: `, `test: ` 등의 접두사 뒤에 설명을 덧붙이는 방식입니다. 한국어로 작성합니다.
그 다음 아래의 명령을 통해 람다 함수를 생성합니다.
```bash
awslocal lambda create-function \
--function-name analyzeTermsOfServices \
--runtime python3.12 \
--timeout 120 \
--zip-file fileb://test-package.zip \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::000000000000:role/lambda-role \
--environment Variables='{GEMINI_API_KEY=여기에_KEY값을_넣어주세요,LLM_PROVIDER=GEMINI}'
```

### 브랜칭 전략
[깃허브 플로우](https://docs.github.com/ko/get-started/using-github/github-flow)와 유사하게, 개별 작업마다 연관된 새로운 브랜치를 생성하고, 해당 브랜치에서 작업 후 main 브랜치에 병합하는 방식으로 개발을 진행합니다. 이때 브랜치의 이름은 `작업 종류/이슈번호-짧은-설명` 으로 합니다.
생성된 함수의 호출은 다음과 같이 할 수 있습니다.
```bash
awslocal lambda invoke --function-name analyzeTermsOfServices \
--payload '{"queryStringParameters": {"url" : "www.example.com"}, "body" : "약관 텍스트" }' output.json
```
이후 `output.json` 파일에서 응답을 확인할 수 있습니다.

# 테스트
로컬에서 `lambda_handler()` 메서드를 테스트해야하는 경우 `test_local.py`를 통해 실행
함수가 생성된 상태에서 변경하기 위해서는 `update-function-code`를 사용합니다.
```bash
awslocal lambda update-function-code \
--function-name analyzeTermsOfServices \
--zip-file fileb://test-package.zip
```
## AWS 환경에서 테스트
로컬에서는 작동만을 확인하고, 답변 품질에 대한 테스트는 AWS Lambda에, 테스트용 함수에 배포하여 수행합니다.
# 컨벤션
## 커밋 메시지
커밋 메시지의 작성법은 [컨벤셔널 커밋](https://www.conventionalcommits.org/ko/v1.0.0/)을 따릅니다. `feat: `, `fix: `, `test: ` 등의 접두사 뒤에 설명을 덧붙이는 방식입니다. 한국어로 작성합니다.
## 브랜칭 전략
[깃허브 플로우](https://docs.github.com/ko/get-started/using-github/github-flow)와 유사하게, 개별 작업마다 연관된 새로운 브랜치를 생성하고, 해당 브랜치에서 작업 후 main 브랜치에 병합하는 방식으로 개발을 진행합니다. 이때 브랜치의 이름은 `작업 종류/이슈번호-짧은-설명` 으로 합니다.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
boto3
google-genai
markdownify
7 changes: 5 additions & 2 deletions src/lambda_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from tos_summarize import tos_summarize
from tos_evaluate import tos_evaluate
from llm_client import LLMClient

def lambda_handler(event, context):
# url이 없거나 빈 문자열인 경우
Expand Down Expand Up @@ -40,11 +41,13 @@ def lambda_handler(event, context):

# TODO: 기존 URL 기반 캐싱 로직 구현

client = LLMClient()

# tos_content 문자열에서 중요 조항 위주로 약관 요약
summarized_tos = tos_summarize(tos_content)
summarized_tos = tos_summarize(tos_content, client)

# 약관 조항에 대해 분석 수행
evaluation_result = tos_evaluate(summarized_tos)
evaluation_result = tos_evaluate(summarized_tos, client)

return {
'statusCode': 200,
Expand Down
70 changes: 70 additions & 0 deletions src/llm_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

# 로컬 테스트 환경에서는 gemini 사용
# AWS Lambda 환경에서는 Bedrock의 Claude 사용

# temperature, top_p는 기본값 temperature 0.2, top_p 0.9로 사용
# 환각 억제 목적

import os
from google import genai
from google.genai import types
import boto3

class LLMClient:

# 환경에 따라 LLM 모델 선정 및 클라이언트 초기화
def __init__(self, temperature: float = 0.2, top_p: float = 0.9):

self.temperature = temperature
self.top_p = top_p

self.provider = os.getenv("LLM_PROVIDER")
if (self.provider == "GEMINI"):
# 로컬 테스트 환경 - gemini
self.client = genai.Client()
else:
# AWS Lambda 환경 - Bedrock Claude
self.client = boto3.client(
service_name="bedrock-runtime",
region_name="us-west-2"
)

# 응답 생성
# gemini와 bedrock claude 분기 처리
def generate_response(self, system_instruction: str, message: str) -> str:

if (self.provider == "GEMINI"):
return self._generate_response_gemini(system_instruction, message)
else:
return self._generate_response_bedrock_claude(system_instruction, message)

# gemini로부터 응답 생성
def _generate_response_gemini(self, system_instruction: str, message: str) -> str:

response = self.client.models.generate_content(
model="gemini-2.5-flash-lite",
config=types.GenerateContentConfig(
temperature=self.temperature,
top_p=self.top_p,
system_instruction=system_instruction
),
contents=message
)

return response.text

# bedrock Claude로부터 응답 생성
# claude 모델은 3.5 haiku 사용. 테스트에 걸리는 시간 줄이기 위함
def _generate_response_bedrock_claude(self, system_instruction: str, message: str) -> str:

response = self.client.converse(
modelId="us.anthropic.claude-3-5-haiku-20241022-v1:0",
inferenceConfig={
"temperature": self.temperature,
"topP": self.top_p
},
system=[{"text": system_instruction}],
messages=[{"role": "user", "content": [{"text": message}]}]
)

return response['output']['message']['content'][0]['text']
70 changes: 0 additions & 70 deletions src/test_local.py

This file was deleted.

33 changes: 8 additions & 25 deletions src/tos_evaluate.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import json
import boto3
from llm_client import LLMClient

def tos_evaluate(summarized_tos):
system_instruction=[{"text": """
def tos_evaluate(summarized_tos, client: LLMClient) -> dict:
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

The summarized_tos parameter is missing a type hint. For consistency with the client parameter, add a type hint (likely str based on usage).

Suggested change
def tos_evaluate(summarized_tos, client: LLMClient) -> dict:
def tos_evaluate(summarized_tos: str, client: LLMClient) -> dict:

Copilot uses AI. Check for mistakes.
system_instruction="""
당신은 전문적인 약관 분석 AI입니다. 주어진 약관 내용 및 각 조항을 평가합니다.
주어진 약관은 주요 조항을 위주로 요약된 내용입니다.
JSON 양식으로, 다음의 key값을 사용합니다.
Expand Down Expand Up @@ -39,33 +39,16 @@ def tos_evaluate(summarized_tos):
}
]
}
"""}]
client = boto3.client(
service_name="bedrock-runtime",
region_name="us-west-2"
)
"""

model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
messages = [{
"role": "user",
"content": [
{"text": summarized_tos}
]
}]

response = client.converse(
modelId=model_id,
system=system_instruction,
messages=messages,
)
response = client.generate_response(system_instruction, summarized_tos)

print("TOS Evaluation Response:")
print(response)

text = response['output']['message']['content'][0]['text']
start = text.find('{')
end = text.rfind('}') + 1
json_text = text[start:end]
start = response.find('{')
end = response.rfind('}') + 1
json_text = response[start:end]

Comment on lines +51 to 52
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

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

If the response doesn't contain '{' or '}', find() returns -1, which would cause incorrect string slicing. Add validation to check if start is -1 before proceeding with slicing and JSON parsing.

Suggested change
json_text = response[start:end]
if start == -1 or end == 0 or end <= start:
raise ValueError("Response does not contain a valid JSON object: " + repr(response))
json_text = response[start:end]

Copilot uses AI. Check for mistakes.
# response에서 JSON 파싱 후 반환
return json.loads(json_text)
29 changes: 6 additions & 23 deletions src/tos_summarize.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,15 @@
import boto3
from llm_client import LLMClient

def tos_summarize(tos_content):
system_instruction=[{"text": """
def tos_summarize(tos_content: str, client: LLMClient) -> str:
system_instruction="""
당신은 약관 분석 전문가입니다.
주어진 텍스트에서 주요 약관 내용을 요약합니다.
한국어로 응답합니다.
"""}]

client = boto3.client(
service_name="bedrock-runtime",
region_name="us-west-2"
)
"""

model_id = "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
messages = [{
"role": "user",
"content": [
{"text": tos_content}
]
}]

response = client.converse(
modelId=model_id,
system=system_instruction,
messages=messages,
)
response = client.generate_response(system_instruction, tos_content)

print("TOS Summarization Response:")
print(response)

return response['output']['message']['content'][0]['text']
return response