diff --git a/.gitignore b/.gitignore index f462c4d..12e6ee1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /venv/ -src/__pycache__/ \ No newline at end of file +src/__pycache__/ +build/ +test-package.zip +output.json \ No newline at end of file diff --git a/README.md b/README.md index d2aed78..35f10f1 100644 --- a/README.md +++ b/README.md @@ -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 브랜치에 병합하는 방식으로 개발을 진행합니다. 이때 브랜치의 이름은 `작업 종류/이슈번호-짧은-설명` 으로 합니다. diff --git a/requirements.txt b/requirements.txt index 8f97f3e..6cf392f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ boto3 +google-genai markdownify \ No newline at end of file diff --git a/src/lambda_function.py b/src/lambda_function.py index bbaec90..4638949 100644 --- a/src/lambda_function.py +++ b/src/lambda_function.py @@ -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이 없거나 빈 문자열인 경우 @@ -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, diff --git a/src/llm_client.py b/src/llm_client.py new file mode 100644 index 0000000..0fbe7c8 --- /dev/null +++ b/src/llm_client.py @@ -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'] diff --git a/src/test_local.py b/src/test_local.py deleted file mode 100644 index d00417e..0000000 --- a/src/test_local.py +++ /dev/null @@ -1,70 +0,0 @@ - -from lambda_function import lambda_handler - -def create_test_event(url: str, body: str) -> dict: - """테스트용 event 객체 생성""" - return { - 'queryStringParameters': { - 'url': url - }, - 'body': body, - } - - -def run_test(): - """Lambda 함수 로컬 테스트 실행""" - - # 테스트 데이터 - test_url = "https://example.com/terms" - test_body = """ - - -

서비스 약관

-

이용자는 본 약관에 동의함으로써 당 서비스를 이용할 수 있습니다.

-

당 회사는 이용자의 개인정보를 보호하기 위해 최선을 다합니다.

-

서비스 이용 중 발생하는 문제에 대해 당 회사는 책임을 지지 않습니다.

- - - """ - - # Event와 Context 생성 - event = create_test_event(test_url, test_body) - context = None - - print("=" * 60) - print("lambda_function 로컬 테스트 실행") - print("=" * 60) - - print("요청 데이터:") - print("-" * 60) - print(f"URL: {event['queryStringParameters']['url']}") - print(f"Body 크기: {len(test_body)} bytes") - print() - - try: - print("lambda_handler 실행 중...") - print() - response = lambda_handler(event, context) - - print("응답 데이터:") - print("-" * 60) - print(f"Status Code: {response.get('statusCode')}") - print() - - print("Body:") - print(response.get('body')) - - print() - print("=" * 60) - - except Exception as e: - print() - print(f"에러: {type(e).__name__}") - print(f" {str(e)}") - print() - import traceback - traceback.print_exc() - - -if __name__ == '__main__': - run_test() diff --git a/src/tos_evaluate.py b/src/tos_evaluate.py index 054eb2f..0fe45cf 100644 --- a/src/tos_evaluate.py +++ b/src/tos_evaluate.py @@ -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: + system_instruction=""" 당신은 전문적인 약관 분석 AI입니다. 주어진 약관 내용 및 각 조항을 평가합니다. 주어진 약관은 주요 조항을 위주로 요약된 내용입니다. JSON 양식으로, 다음의 key값을 사용합니다. @@ -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] # response에서 JSON 파싱 후 반환 return json.loads(json_text) \ No newline at end of file diff --git a/src/tos_summarize.py b/src/tos_summarize.py index 8263f67..a14859b 100644 --- a/src/tos_summarize.py +++ b/src/tos_summarize.py @@ -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