diff --git a/.env.example b/.env.example deleted file mode 100644 index e8f5d00..0000000 --- a/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# .env.example file -# This file provides examples of environment variables needed for the project. -# For actual use, you will need to copy this file to create an .env file and enter the actual values. - -# Langsmith Project Tracking -# LangSmith is a tool for monitoring and debugging -LANGSMITH_PROJECT=act-entertainment # Project name to be used by LangSmith. -LANGSMITH_API_KEY=lsv2.... # LangSmith API key (must be replaced with real key) - -# Depending on the configuration you choose, you will need the following environment variables. - -## LLM API Keys: -# OpenAI API Key - required to use the GPT model. -# You can get it from the OpenAI website (https://platform.openai.com/). -OPENAI_API_KEY=sk... - -# Others... \ No newline at end of file diff --git a/agents/__init__.py b/agents/__init__.py deleted file mode 100644 index 076c9b5..0000000 --- a/agents/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from agents.workflow import main_workflow - -__all__ = ["main_workflow"] diff --git a/agents/image/README.md b/agents/image/README.md deleted file mode 100644 index 90025e9..0000000 --- a/agents/image/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# 이미지 모듈 (Image Module) - -## 개요 - -이 모듈은 Act 1: Entertainment의 이미지 기반 콘텐츠 생성을 담당하는 LangGraph Workflow입니다. 다양한 스타일과 주제의 이미지 콘텐츠를 생성하기 위한 주요 노드와 Workflow를 제공합니다. - -## 주요 노드 - - - -## 구조 - -``` -image/ -├── modules/ # 모듈 구성 요소 -│ ├── chains.py # LangChain 체인 정의 -│ ├── conditions.py # 조건부 라우팅 함수 -│ ├── models.py # 사용하는 LLM 모델 설정 -│ ├── nodes.py # Workflow 노드 클래스들 정의 -│ ├── prompts.py # 프롬프트 템플릿(필요에 따라 변경 가능) -│ ├── state.py # 상태 정의 -│ ├── tools.py # 도구 함수 -│ └── utils.py # 유틸리티 함수 -├── pyproject.toml # 프로젝트 관리자 -├── README.md # 이 문서 -└── workflow.py # Image Agent의 Workflow들 정의 -``` - -## 사용 방법 - -이미지 Workflow는 다음과 같이 사용할 수 있습니다: - -```python -from agents.image.workflow import image_workflow - -# 초기 상태 설정 -initial_state = { - "query": "자연 풍경 이미지 생성", # 사용자 쿼리 - "response": [] # 응답 메시지 (빈 리스트로 초기화) -} - -# Workflow 실행 -result = image_workflow().invoke(initial_state) -``` - -## 확장 방법 - -이 모듈은 확장성을 고려하여 설계되었습니다. 새로운 기능(백로그)을 추가하려면: - -1. `modules/nodes.py`에 새로운 노드 클래스 추가 -2. 필요에 따라 `modules/state.py`에 상태 관리 추가 -3. 필요에 따라 `model.py`, `chain.py` 등의 해당 노드에서 사용되는 관련 모듈을 수정/추가하세요. -4. `workflow.py`에서 Workflow에 새 노드를 엣지로 연결 - -## 라이센스 - -이 모듈은 Proact0의 Act 1: Entertainment의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. diff --git a/agents/image/__init__.py b/agents/image/__init__.py deleted file mode 100644 index 2566fa1..0000000 --- a/agents/image/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Image 패키지 초기화 모듈 - -이 모듈은 Image Workflow를 외부에 노출시키는 역할을 합니다. -""" - -from agents.image.workflow import image_workflow - -__all__ = ["image_workflow"] diff --git a/agents/image/modules/chains.py b/agents/image/modules/chains.py deleted file mode 100644 index 86c6666..0000000 --- a/agents/image/modules/chains.py +++ /dev/null @@ -1,43 +0,0 @@ -"""LangChain 체인을 설정하는 함수 모듈 - -LCEL(LangChain Expression Language)을 사용하여 체인을 구성합니다. -기본적으로 modules.prompt 템플릿과 modules.models 모듈을 사용하여 LangChain 체인을 생성합니다. -""" - -# from langchain.schema.runnable import RunnablePassthrough, RunnableSerializable -# from langchain_core.output_parsers import StrOutputParser - -# from agents.image.modules.models import get_openai_model -# from agents.image.modules.prompts import get_image_generation_prompt - - -# def set_image_generation_chain() -> RunnableSerializable: -# """ -# 이미지 생성에 사용할 LangChain 체인을 생성합니다. -# -# 체인은 다음 단계로 구성됩니다: -# 1. 입력에서 query를 추출하여 프롬프트에 전달 -# 2. 프롬프트 템플릿에 값을 삽입하여 최종 프롬프트 생성 -# 3. LLM을 호출하여 이미지 생성 수행 -# 4. 결과를 문자열로 변환 -# -# 이 함수는 이미지 생성 노드에서 사용됩니다. -# -# Returns: -# RunnableSerializable: 실행 가능한 체인 객체 -# """ -# # 이미지 생성을 위한 프롬프트 가져오기 -# prompt = get_image_generation_prompt() -# # OpenAI 모델 가져오기 -# model = get_openai_model() -# -# # LCEL을 사용하여 체인 구성 -# return ( -# # 입력에서 필요한 필드 추출 및 프롬프트에 전달 -# RunnablePassthrough.assign( -# query=lambda x: x["query"], # 사용자 쿼리 추출 -# ) -# | prompt # 프롬프트 적용 -# | model # LLM 모델 호출 -# | StrOutputParser() # 결과를 문자열로 변환 -# ) diff --git a/agents/image/modules/conditions.py b/agents/image/modules/conditions.py deleted file mode 100644 index 3287111..0000000 --- a/agents/image/modules/conditions.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -조건부 라우팅 함수 모듈 - -이 모듈은 LangGraph Workflow에서 조건부 라우팅을 처리하는 함수들을 제공합니다. -조건부 라우팅은 Workflow의 다음 단계를 동적으로 결정하는 데 사용됩니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -이 예시 코드는 ReAct 패턴에서 LLM의 출력에 따라 다음 노드를 결정하는 라우터 함수를 보여줍니다. - -Workflow가 확장됨에 따라 다양한 조건부 라우팅 함수를 이 모듈에 추가할 수 있습니다. -예를 들어, 이미지 스타일에 따른 라우팅, 사용자 요청 유형에 따른 라우팅 등을 구현할 수 있습니다. -""" - -# from typing import Literal -# from langchain_core.messages import AIMessage - - -# def router(state) -> Literal["__end__", "tools"]: -# """ -# 모델의 출력을 기반으로 다음 노드를 결정하는 라우터 함수 -# -# 이 함수는 LLM의 마지막 메시지를 검사하여 도구 호출이 포함되어 있는지 확인하고, -# 그 결과에 따라 Workflow의 다음 단계를 결정합니다. -# -# 도구 호출이 있으면 "tools" 노드로 라우팅하고, 그렇지 않으면 Workflow를 종료합니다. -# 이는 ReAct 패턴의 일반적인 구현 방식으로, LLM이 도구를 사용해야 할지 또는 -# 최종 응답을 생성해야 할지를 결정하게 합니다. -# -# Args: -# state (State): 현재 Workflow 상태 객체 (메시지 기록 포함) -# -# Returns: -# str: 다음에 실행할 노드의 이름 ("__end__" 또는 "tools") -# -# Raises: -# ValueError: 마지막 메시지가 AIMessage 타입이 아닌 경우 -# -# 예시: -# ```python -# # Workflow에 조건부 에지 추가 -# builder.add_conditional_edges( -# "call_model", # 소스 노드 -# router, # 라우터 함수 -# { -# "__end__": "__end__", # 종료 조건 -# "tools": "execute_tools" # 도구 실행 조건 -# } -# ) -# ``` -# """ -# # 상태에서 마지막 메시지 가져오기 -# last_message = state.messages[-1] -# -# # 메시지 타입 검증 -# if not isinstance(last_message, AIMessage): -# raise ValueError( -# f"Expected AIMessage in output edges, but got {type(last_message).__name__}" -# ) -# -# # 도구 호출 여부에 따른 라우팅 결정 -# if not last_message.tool_calls: -# return "__end__" # 도구 호출이 없으면 Workflow 종료 -# -# # 도구 호출이 있으면 도구 실행 노드로 라우팅 -# return "tools" diff --git a/agents/image/modules/models.py b/agents/image/modules/models.py deleted file mode 100644 index ec2a759..0000000 --- a/agents/image/modules/models.py +++ /dev/null @@ -1,23 +0,0 @@ -"""모델 설정 함수 모듈 - -기본적으로 사용할 모델 인스턴스를 설정하고 생성하고 반환시킵니다. -""" - -# from langchain_openai import ChatOpenAI - - -# def get_openai_model(temperature=0.7, top_p=0.9): -# """ -# LangChain에서 사용할 OpenAI 모델을 초기화하여 반환합니다. -# -# 환경변수에서 OPENAI_API_KEY를 가져와 사용하기 때문에, .env 파일에 유효한 API 키가 설정되어 있어야 합니다. -# -# Args: -# temperature: 모델의 창의성 정도를 조절하는 파라미터 (기본값: 0.7) -# top_p: 토큰 샘플링 확률 임계값 (기본값: 0.9) -# -# Returns: -# ChatOpenAI: 초기화된 OpenAI 모델 인스턴스 -# """ -# # OpenAI 모델 초기화 및 반환 -# return ChatOpenAI(model="gpt-4o-mini", temperature=temperature, top_p=top_p) diff --git a/agents/image/modules/nodes.py b/agents/image/modules/nodes.py deleted file mode 100644 index 04d7316..0000000 --- a/agents/image/modules/nodes.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -노드 클래스 모듈 - -해당 클래스 모듈은 각각 노드 클래스가 BaseNode를 상속받아 노드 클래스를 구현하는 모듈입니다. -""" - -# from agents.base_node import BaseNode - -# from agents.image.modules.chains import set_image_generation_chain - - -# class ImageGenerationNode(BaseNode): -# """ -# 이미지 생성을 위한 노드 - -# 이 노드는 사용자의 요청에 따라 이미지를 생성하는 기능을 담당합니다. -# """ - -# def __init__(self, **kwargs): -# super().__init__(**kwargs) # BaseNode 초기화 -# # self.chain = set_image_generation_chain() # 이미지 생성 체인 설정 - -# def execute(self, state) -> dict: -# """ -# 주어진 상태(state)에서 query를 추출하여 -# 이미지 생성 체인에 전달하고, 결과를 응답으로 반환합니다. - -# Args: -# state: 현재 워크플로우 상태 - -# Returns: -# dict: 생성된 이미지 정보가 포함된 응답 -# """ -# # 실제 구현은 추후 개발 시 추가 -# # generated_image = self.chain.invoke( -# # { -# # "query": state["query"], # 사용자 쿼리 -# # } -# # ) - -# # 더미 응답 생성 -# dummy_response = "이미지 생성 기능은 아직 구현되지 않았습니다." - -# # 응답 반환 -# return {"response": dummy_response} diff --git a/agents/image/modules/prompts.py b/agents/image/modules/prompts.py deleted file mode 100644 index 8380f01..0000000 --- a/agents/image/modules/prompts.py +++ /dev/null @@ -1,42 +0,0 @@ -"""프롬프트 템플릿을 생성하는 함수 모듈 - -프롬프트 템플릿을 생성하는 함수 모듈을 구성합니다. -기본적으로 PromptTemplate을 사용하여 프롬프트 템플릿을 생성하고 반환합니다. -""" - -# from langchain_core.prompts import PromptTemplate - -# def get_image_generation_prompt(): -# """ -# 이미지 생성을 위한 프롬프트 템플릿을 생성합니다. -# -# 프롬프트는 LLM에게 사용자 쿼리에 맞는 이미지 생성 방법과 -# 이미지 특성을 설명하도록 지시합니다. 생성된 이미지 설명은 한국어로 반환됩니다. -# -# Returns: -# PromptTemplate: 이미지 생성을 위한 프롬프트 템플릿 객체 -# """ -# # 이미지 생성을 위한 프롬프트 템플릿 정의 -# image_generation_template = """당신은 이미지 생성 전문가로서 다양한 스타일과 주제의 이미지를 설명하고 -# 생성하는 데 전문성을 가지고 있습니다. 다음 정보를 바탕으로 이미지를 생성해 주세요: - -# 사용자 요청: {query} - -# 작업: -# 위 입력을 사용하여 사용자의 요청에 맞는 이미지를 생성하고 설명해 주세요. 설명에는 다음 내용을 포함해야 합니다: - -# - 이미지의 주요 요소와 구성 -# - 이미지의 스타일과 분위기 -# - 색상 팔레트와 조명 효과 -# - 이미지가 전달하는 감정과 메시지 - -# 설명은 구체적이고 상세하게 작성하여 이미지 제작자가 이해하고 구현할 수 있도록 해주세요. -# 모든 응답은 한국어로 작성해 주세요. - -# 생성된 이미지 설명:""" -# -# # PromptTemplate 객체 생성 및 반환 -# return PromptTemplate( -# template=image_generation_template, # 정의된 프롬프트 템플릿 -# input_variables=["query"], # 프롬프트에 삽입될 변수들 -# ) diff --git a/agents/image/modules/state.py b/agents/image/modules/state.py deleted file mode 100644 index 022080a..0000000 --- a/agents/image/modules/state.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -이미지 Workflow의 상태를 정의하는 모듈 - -이 모듈은 이미지 기반 콘텐츠 생성을 위한 Workflow에서 사용되는 상태 정보를 정의합니다. -LangGraph의 상태 관리를 위한 클래스를 포함합니다. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Annotated, TypedDict - -from langgraph.graph.message import add_messages - - -@dataclass -class ImageState(TypedDict): - """ - 이미지 Workflow의 상태를 정의하는 TypedDict 클래스 - - 이미지 기반 콘텐츠 생성을 위한 Workflow에서 사용되는 상태 정보를 정의합니다. - LangGraph의 상태 관리를 위한 클래스로, Workflow 내에서 처리되는 데이터의 형태와 구조를 지정합니다. - """ - - query: str # 사용자 쿼리 또는 요청사항 - response: Annotated[ - list, add_messages - ] # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공) diff --git a/agents/image/modules/tools.py b/agents/image/modules/tools.py deleted file mode 100644 index ad4572c..0000000 --- a/agents/image/modules/tools.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -도구(Tools) 모듈 - -이 모듈은 LangGraph Workflow에서 사용할 수 있는 다양한 도구를 정의합니다. -도구는 LLM이 외부 시스템과 상호작용하거나 특정 작업을 수행할 수 있도록 해주는 함수들입니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -아래 예시 코드는 이미지 관련 정보를 검색하는 도구를 보여줍니다. - -추후 개발 시 다음과 같은 다양한 도구를 구현하여 추가할 수 있습니다: -- 이미지 검색 도구: 키워드에 맞는 이미지 검색 -- 이미지 분석 도구: 이미지 내용 분석 및 설명 -- 이미지 생성 도구: AI를 활용한 이미지 생성 -- 이미지 편집 도구: 이미지 크기 조정, 필터 적용 등 -- 이미지 변환 도구: 이미지 형식 변환 및 처리 -""" - -# from typing import Any, Callable, List, Optional, cast - -# from langchain_core.runnables import RunnableConfig -# from langchain_core.tools import InjectedToolArg -# from typing_extensions import Annotated - - -# async def search_image_info( -# query: str, *, config: Annotated[RunnableConfig, InjectedToolArg] -# ) -> Optional[str]: -# """ -# 이미지 관련 정보를 검색합니다. - -# 이 함수는 이미지 스타일, 아티스트, 기법 등에 대한 정보를 검색합니다. -# """ -# # 실제 구현은 추후 개발 시 추가 -# return f"이미지 정보 검색 결과: {query}" - - -# TOOLS: List[Callable[..., Any]] = [search_image_info] diff --git a/agents/image/modules/utils.py b/agents/image/modules/utils.py deleted file mode 100644 index 01d16aa..0000000 --- a/agents/image/modules/utils.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -유틸리티 및 보조 함수 모듈 - -이 모듈은 이미지 처리 Workflow에서 사용할 수 있는 다양한 유틸리티 함수를 제공합니다. -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. - -아래 예시 코드는 이미지 처리와 관련된 유틸리티 함수들입니다. -이 함수들은 이미지 파일 처리 및 메타데이터 추출과 관련된 기능을 제공합니다. - -추후 개발 시 필요한 유틸리티 함수를 이 모듈에 추가하여 코드 재사용성을 높일 수 있습니다. -예를 들어, 이미지 전처리, 이미지 특성 추출, 데이터 변환 등의 기능을 구현할 수 있습니다. -""" - -# from typing import Dict, Any, Optional -# from PIL import Image - - -# def extract_image_metadata(file_path: str) -> Optional[Dict[str, Any]]: -# """ -# 이미지 파일에서 메타데이터를 추출합니다. -# -# Args: -# file_path (str): 이미지 파일 경로 -# -# Returns: -# Optional[Dict[str, Any]]: 추출된 메타데이터 (크기, 형식, 모드 등) -# """ -# try: -# with Image.open(file_path) as img: -# return { -# "format": img.format, -# "mode": img.mode, -# "size": img.size, -# "width": img.width, -# "height": img.height, -# } -# except Exception as e: -# print(f"이미지 메타데이터 추출 중 오류 발생: {e}") -# return None - - -# def resize_image(file_path: str, width: int, height: int, output_path: str) -> bool: -# """ -# 이미지 크기를 조정합니다. -# -# Args: -# file_path (str): 원본 이미지 파일 경로 -# width (int): 조정할 너비 -# height (int): 조정할 높이 -# output_path (str): 결과 이미지 저장 경로 -# -# Returns: -# bool: 성공 여부 -# """ -# try: -# with Image.open(file_path) as img: -# resized_img = img.resize((width, height)) -# resized_img.save(output_path) -# return True -# except Exception as e: -# print(f"이미지 크기 조정 중 오류 발생: {e}") -# return False diff --git a/agents/image/pyproject.toml b/agents/image/pyproject.toml deleted file mode 100644 index 8d72c24..0000000 --- a/agents/image/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -# 프로젝트 설정 파일 -# 이 파일은 이미지 모듈의 의존성과 메타데이터를 정의합니다. -# Python 패키지 관리 도구(uv)가 이 파일을 사용하여 필요한 패키지를 설치합니다. -# 이 패키지에 종속성을 추가하시려면 `uv add --project agents/image <>`를 사용합니다. - -[project] -name = "image" -version = "0.1.0" -description = "이미지 기반 콘텐츠 생성을 위한 LangGraph Workflow 모듈" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [] diff --git a/agents/image/workflow.py b/agents/image/workflow.py deleted file mode 100644 index 0ce39cb..0000000 --- a/agents/image/workflow.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -이미지 관련 콘텐츠 생성을 위한 Workflow 모듈 - -이 모듈은 이미지 기반 콘텐츠 생성을 위한 Workflow를 정의합니다. -StateGraph를 사용하여 이미지 처리를 위한 워크플로우를 구축합니다. -""" - -from langgraph.graph import StateGraph - -from agents.base_workflow import BaseWorkflow -from agents.image.modules.state import ImageState - - -class ImageWorkflow(BaseWorkflow): - """ - 이미지 관련 콘텐츠 생성을 위한 Workflow 클래스 - - 이 클래스는 이미지 기반 콘텐츠 생성을 위한 Workflow를 정의합니다. - BaseWorkflow를 상속받아 기본 구조를 구현하고, ImageState를 사용하여 상태를 관리합니다. - """ - - def __init__(self, state): - super().__init__() - self.state = state - - def build(self): - """ - 이미지 Workflow 그래프 구축 메서드 - - StateGraph를 사용하여 이미지 처리를 위한 Workflow 그래프를 구축합니다. - 현재는 간단한 구조로 시작 노드에서 종료 노드로 직접 연결되어 있으며, - 추후 이미지 생성 노드 등을 추가하여 확장할 수 있습니다. - - Returns: - CompiledStateGraph: 컴파일된 상태 그래프 객체 - """ - builder = StateGraph(self.state) - - # 기본 구조: 시작 노드에서 종료 노드로 직접 연결 - builder.add_edge("__start__", "__end__") - - # 향후 이미지 생성 노드 추가 예시 - # builder.add_node("image_generation", ImageGenerationNode()) - # builder.add_edge("__start__", "image_generation") - # builder.add_edge("image_generation", "__end__") - - workflow = builder.compile() # 그래프 컴파일 - workflow.name = self.name # Workflow 이름 설정 - - return workflow - - -# 이미지 Workflow 인스턴스 생성 -image_workflow = ImageWorkflow(ImageState) diff --git a/agents/management/README.md b/agents/management/README.md deleted file mode 100644 index 98706a5..0000000 --- a/agents/management/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# 관리 모듈 (Management Module) - -## 개요 - -이 모듈은 Act 1: Entertainment의 콘텐츠 관리를 담당하는 LangGraph Workflow입니다. 프로젝트, 팀, 리소스의 관리와 크리에이터 직업 성장을 지원하기 위한 주요 노드와 Workflow를 제공합니다. - -## 주요 노드 - - - -## 구조 - -``` -management/ -├── modules/ # 모듈 구성 요소 -│ ├── chains.py # LangChain 체인 정의 -│ ├── conditions.py # 조건부 라우팅 함수 -│ ├── models.py # 사용하는 LLM 모델 설정 -│ ├── nodes.py # Workflow 노드 클래스들 정의 -│ ├── persona.py # 페르소나 관리 기능 -│ ├── prompts.py # 프롬프트 템플릿 -│ ├── state.py # 상태 정의 -│ ├── tools.py # 도구 함수 -│ └── utils.py # 유틸리티 함수 -├── pyproject.toml # 프로젝트 관리자 -├── README.md # 이 문서 -└── workflow.py # Management Agent의 Workflow들 정의 -``` - -## 사용 방법 - -관리(Management) Workflow는 다음과 같이 사용할 수 있습니다: - -```python -from agents.management.workflow import management_workflow - -# 초기 상태 설정 -initial_state = { - "project_id": "PRJ-2023-001", # 프로젝트 ID - "request_type": "resource_allocation", # 요청 유형 - "query": "개발 팀 리소스 계획 수립", # 사용자 쿼리 - "team_members": ["Kim", "Lee", "Park"], # 팀 구성원 - "response": [] # 응답 메시지 (빈 리스트로 초기화) -} - -# Workflow 실행 -result = management_workflow().invoke(initial_state) -``` - -## 확장 방법 - -이 모듈은 확장성을 고려하여 설계되었습니다. 새로운 기능(백로그)을 추가하려면: - -1. `modules/nodes.py`에 새로운 노드 클래스 추가 -2. 필요에 따라 `modules/state.py`에 상태 관리 추가 -3. 필요에 따라 `model.py`, `chain.py` 등의 해당 노드에서 사용되는 관련 모듈을 수정/추가하세요. -4. `workflow.py`에서 Workflow에 새 노드를 엣지로 연결 - -## 라이센스 - -이 모듈은 Proact0의 Act 1: Entertainment의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. \ No newline at end of file diff --git a/agents/management/modules/__init__.py b/agents/management/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agents/management/modules/conditions.py b/agents/management/modules/conditions.py deleted file mode 100644 index 4a48bdc..0000000 --- a/agents/management/modules/conditions.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -조건부 라우팅 함수 모듈 - -이 모듈은 LangGraph Workflow에서 조건부 라우팅을 처리하는 함수들을 제공합니다. -조건부 라우팅은 Workflow의 다음 단계를 동적으로 결정하는 데 사용됩니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -이 예시 코드는 ReAct 패턴에서 LLM의 출력에 따라 다음 노드를 결정하는 라우터 함수를 보여줍니다. - -Workflow가 확장됨에 따라 다양한 조건부 라우팅 함수를 이 모듈에 추가할 수 있습니다. -예를 들어, 콘텐츠 유형에 따른 라우팅, 사용자 요청 유형에 따른 라우팅 등을 구현할 수 있습니다. -""" - -# from typing import Literal - - -# def router(state) -> Literal["__end__", "tools"]: -# """ -# 모델의 출력을 기반으로 다음 노드를 결정하는 라우터 함수 -# -# 이 함수는 LLM의 마지막 메시지를 검사하여 도구 호출이 포함되어 있는지 확인하고, -# 그 결과에 따라 Workflow의 다음 단계를 결정합니다. -# -# 도구 호출이 있으면 "tools" 노드로 라우팅하고, 그렇지 않으면 Workflow를 종료합니다. -# 이는 ReAct 패턴의 일반적인 구현 방식으로, LLM이 도구를 사용해야 할지 또는 -# 최종 응답을 생성해야 할지를 결정하게 합니다. -# -# Args: -# state (State): 현재 Workflow 상태 객체 (메시지 기록 포함) -# -# Returns: -# str: 다음에 실행할 노드의 이름 ("__end__" 또는 "tools") -# -# Raises: -# ValueError: 마지막 메시지가 AIMessage 타입이 아닌 경우 -# -# 예시: -# ```python -# # Workflow에 조건부 에지 추가 -# builder.add_conditional_edges( -# "call_model", # 소스 노드 -# router, # 라우터 함수 -# { -# "__end__": "__end__", # 종료 조건 -# "tools": "execute_tools" # 도구 실행 조건 -# } -# ) -# ``` -# """ -# # 상태에서 마지막 메시지 가져오기 -# last_message = state.messages[-1] -# -# # 메시지 타입 검증 -# if not isinstance(last_message, AIMessage): -# raise ValueError( -# f"Expected AIMessage in output edges, but got {type(last_message).__name__}" -# ) -# -# # 도구 호출 여부에 따른 라우팅 결정 -# if not last_message.tool_calls: -# return "__end__" # 도구 호출이 없으면 Workflow 종료 -# -# # 도구 호출이 있으면 도구 실행 노드로 라우팅 -# return "tools" diff --git a/agents/management/modules/nodes.py b/agents/management/modules/nodes.py deleted file mode 100644 index eb96438..0000000 --- a/agents/management/modules/nodes.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -노드 클래스 모듈 - -해당 클래스 모듈은 각각 노드 클래스가 BaseNode를 상속받아 노드 클래스를 구현하는 모듈입니다. - -아래는 예시입니다. -""" - -from agents.base_node import BaseNode -from agents.management.modules.chains import set_resource_planning_chain -from agents.management.modules.state import ManagementState - - -class ResourceManagementNode(BaseNode): - """ - 프로젝트에 필요한 리소스를 계획하고 관리하는 노드 - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) # BaseNode 초기화 - self.chain = set_resource_planning_chain() # 리소스 계획 체인 설정 - - def execute(self, state: ManagementState) -> dict: - """ - 주어진 상태(state)에서 project_id, request_type, query 등의 정보를 추출하여 - 리소스 계획 체인에 전달하고, 결과를 응답으로 반환합니다. - """ - # 팀 구성원 기본값 처리 - team_members = state.get("team_members", []) - - # 리소스 계획 체인 실행 - resource_plan = self.chain.invoke( - { - "project_id": state["project_id"], # 프로젝트 ID - "request_type": state["request_type"], # 요청 유형 - "query": state["query"], # 사용자 쿼리 - "team_members": team_members, # 팀 구성원 - "resources_available": state.get( - "resources_available", {} - ), # 사용 가능한 리소스 - } - ) - - # 상태 업데이트 - state["resource_plan"] = resource_plan - - # 생성된 리소스 계획을 응답으로 반환 - return {"response": resource_plan} diff --git a/agents/management/modules/prompts.py b/agents/management/modules/prompts.py deleted file mode 100644 index 16d9764..0000000 --- a/agents/management/modules/prompts.py +++ /dev/null @@ -1,86 +0,0 @@ -"""프롬프트 템플릿을 생성하는 함수 모듈 - -프롬프트 템플릿을 생성하는 함수 모듈을 구성합니다. -기본적으로 PromptTemplate를 사용하여 프롬프트 템플릿을 생성하고 반환합니다. - -아래는 예시입니다. -""" - -from langchain_core.prompts import PromptTemplate - - -def get_resource_planning_prompt(): - """ - 리소스 계획 수립을 위한 프롬프트 템플릿을 생성합니다. - - 이 프롬프트는 다음 데이터를 입력으로 사용합니다: - 1. 프로젝트 ID: 관리할 프로젝트의 고유 ID - 2. 요청 유형: 리소스 할당, 팀 관리, 크리에이터 개발 등의 요청 유형 - 3. 사용자 쿼리: 구체적인 요청사항 - 4. 팀 구성원: 프로젝트에 참여하는 팀 구성원 목록 - 5. 사용 가능한 리소스: 현재 사용 가능한 리소스 정보 - - 프롬프트는 LLM에게 주어진 정보를 기반으로 프로젝트 관리에 적합한 리소스 계획을 - 수립하도록 지시합니다. 결과는 한국어로 반환됩니다. - - Returns: - PromptTemplate: 리소스 계획 수립을 위한 프롬프트 템플릿 객체 - """ - # 리소스 계획을 위한 프롬프트 템플릿 정의 - resource_planning_template = """You are an expert entertainment project manager tasked with creating resource plans for entertainment projects. You are provided with the following information: - -1. Project ID: {project_id} - -2. Request Type: {request_type} - -3. User Query: {query} - -4. Team Members: {team_members} - -5. Available Resources: {resources_available} - -Your Task: -Based on the information provided, develop a comprehensive resource management plan that addresses the user query. Your plan should include: - -1. PROJECT OVERVIEW: -- Brief summary of the project based on the available information -- Clear objectives and expected outcomes - -2. RESOURCE ALLOCATION: -- Human resources: Team composition, roles, and responsibilities -- Technical resources: Equipment, software, and facilities needed -- Financial resources: Budget considerations and allocations -- Time resources: Schedule, timeline, and milestones - -3. RESOURCE OPTIMIZATION: -- Efficiency recommendations -- Risk assessment and mitigation strategies -- Contingency planning - -4. IMPLEMENTATION PLAN: -- Step-by-step guide for executing the resource plan -- Monitoring and evaluation mechanisms -- Communication protocols - -5. RECOMMENDATIONS: -- Additional resources that might be beneficial -- Training or development opportunities -- Process improvement suggestions - -Make your plan specific to the entertainment industry context and the particular request type. Be detailed yet concise, and ensure your recommendations are practical and actionable. - -All responses must be in Korean. - -Resource Management Plan:""" - - # PromptTemplate 객체 생성 및 반환 - return PromptTemplate( - template=resource_planning_template, # 정의된 프롬프트 템플릿 - input_variables=[ - "project_id", - "request_type", - "query", - "team_members", - "resources_available", - ], # 프롬프트에 삽입될 변수들 - ) diff --git a/agents/management/modules/state.py b/agents/management/modules/state.py deleted file mode 100644 index 9ae62c1..0000000 --- a/agents/management/modules/state.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -아래는 예시입니다. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Annotated, TypedDict, List, Dict, Optional - -from langgraph.graph.message import add_messages - - -@dataclass -class ManagementState(TypedDict): - """ - 관리(Management) Workflow의 상태를 정의하는 TypedDict 클래스 - - 프로젝트, 팀, 리소스의 관리와 크리에이터 직업 성장을 위한 Workflow에서 사용되는 상태 정보를 정의합니다. - LangGraph의 상태 관리를 위한 클래스로, Workflow 내에서 처리되는 데이터의 형태와 구조를 지정합니다. - """ - - project_id: str # 프로젝트 ID (예: "PRJ-2023-001", "EP-MARVEL-S01") - request_type: str # 요청 유형 (예: "resource_allocation", "team_management", "creator_development") - query: str # 사용자 쿼리 또는 요청사항 - response: Annotated[ - list, add_messages - ] - team_members: Optional[List[str]] = None # 팀 구성원 목록 - resources_available: Optional[Dict[str, any]] = None # 사용 가능한 리소스 정보 - resource_plan: Optional[str] = None # 리소스 계획 콘텐츠 - # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공) diff --git a/agents/management/modules/tools.py b/agents/management/modules/tools.py deleted file mode 100644 index 9b9b4fe..0000000 --- a/agents/management/modules/tools.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -도구(Tools) 모듈 - -이 모듈은 Management Workflow에서 사용할 수 있는 다양한 도구를 정의합니다. -도구는 LLM이 프로젝트 관리, 리소스 할당, 팀 관리 등을 지원하는 함수들입니다. - -아래는 엔터테인먼트 프로젝트 관리에 적합한 도구의 예시입니다: -- 프로젝트 일정 관리 도구: 일정 생성, 수정, 추적 -- 팀 구성원 할당 도구: 팀원 정보 검색 및 역할 할당 -- 리소스 검색 도구: 가용 리소스 출력 및 할당 상태 확인 -- 참조 자료 검색 도구: 엔터테인먼트 산업의 프로젝트 관리 사례 검색 -- 협업 지원 도구: 팀원간 커뮤니케이션 및 협업 지원 -""" - -# 예시 코드: 리소스 검색 도구 -# from typing import Dict, List, Optional -# from datetime import datetime - -# def search_available_resources(resource_type: str, time_period: Optional[Dict[str, datetime]] = None) -> List[Dict]: -# """ -# 주어진 리소스 유형과 시간에 따라 사용 가능한 리소스를 검색합니다. -# -# Args: -# resource_type: 검색할 리소스 유형 (예: 'studio', 'equipment', 'staff') -# time_period: 시간 기간 (예: {'start': datetime(2023, 6, 1), 'end': datetime(2023, 6, 30)}) -# -# Returns: -# List[Dict]: 사용 가능한 리소스 목록 -# """ -# # 실제 구현에서는 데이터베이스를 쿼리하거나 API를 호출하여 사용 가능한 리소스를 가져옴 -# pass - -# 예시 코드: 프로젝트 일정 관리 도구 -# def get_project_schedule(project_id: str) -> Dict: -# """ -# 특정 프로젝트의 일정을 가져옵니다. -# -# Args: -# project_id: 프로젝트 ID -# -# Returns: -# Dict: 프로젝트 일정 정보 -# """ -# # 실제 구현에서는 프로젝트 관리 시스템 API를 호출하여 일정을 가져옴 -# pass - -# from react_agent.configuration import Configuration - - -# async def search( -# query: str, *, config: Annotated[RunnableConfig, InjectedToolArg] -# ) -> Optional[list[dict[str, Any]]]: -# """ -# 일반 웹 결과를 검색합니다. - -# 이 함수는 Tavily 검색 엔진을 사용하여 검색을 수행합니다. 이 엔진은 포괄적이고 정확하며 신뢰할 수 있는 결과를 제공하도록 설계되었습니다. 특히 현재 이벤트에 대한 질문에 대답하는 데 유용합니다. -# """ -# configuration = Configuration.from_runnable_config(config) -# wrapped = TavilySearchResults(max_results=configuration.max_search_results) -# result = await wrapped.ainvoke({"query": query}) -# return cast(list[dict[str, Any]], result) - - -# TOOLS: List[Callable[..., Any]] = [search] diff --git a/agents/management/modules/utils.py b/agents/management/modules/utils.py deleted file mode 100644 index 0497b49..0000000 --- a/agents/management/modules/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -유틸리티 및 보조 함수 모듈 - -이 모듈은 텍스트 처리 Workflow에서 사용할 수 있는 다양한 유틸리티 함수를 제공합니다. -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. - -아래 예시 코드는 ReAct Agent 패턴에서 사용될 수 있는 유틸리티 함수들입니다. -이 함수들은 메시지 처리 및 모델 로딩과 관련된 기능을 제공합니다. - -추후 개발 시 필요한 유틸리티 함수를 이 모듈에 추가하여 코드 재사용성을 높일 수 있습니다. -예를 들어, 텍스트 전처리, 포맷팅, 데이터 변환 등의 기능을 구현할 수 있습니다. - -아래는 예시입니다. -""" - -# from langchain.chat_models import init_chat_model -# from langchain_core.language_models import BaseChatModel -# from langchain_core.messages import BaseMessage - - -# def get_message_text(msg: BaseMessage) -> str: -# """메시지의 텍스트 내용을 가져옵니다.""" -# content = msg.content -# if isinstance(content, str): -# return content -# elif isinstance(content, dict): -# return content.get("text", "") -# else: -# txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content] -# return "".join(txts).strip() - - -# def load_chat_model(fully_specified_name: str) -> BaseChatModel: -# """완전히 지정된 이름에서 채팅 모델을 로드합니다. - -# 매개변수: -# fully_specified_name (str): 'provider/model' 형식의 문자열. -# """ -# provider, model = fully_specified_name.split("/", maxsplit=1) -# return init_chat_model(model, model_provider=provider) diff --git a/agents/management/workflow.py b/agents/management/workflow.py deleted file mode 100644 index 20ba00b..0000000 --- a/agents/management/workflow.py +++ /dev/null @@ -1,54 +0,0 @@ -from langgraph.graph import StateGraph - -from agents.base_workflow import BaseWorkflow -from agents.management.modules.nodes import ResourceManagementNode -from agents.management.modules.state import ManagementState - - -class ManagementWorkflow(BaseWorkflow): - """ - 콘텐츠 관리를 위한 Workflow 클래스 - - 이 클래스는 프로젝트, 팀, 리소스의 관리와 크리에이터 직업 성장을 위한 Workflow를 정의합니다. - BaseWorkflow를 상속받아 기본 구조를 구현하고, ManagementState를 사용하여 상태를 관리합니다. - """ - - def __init__(self, state): - super().__init__() - self.state = state - - def build(self): - """ - 관리 Workflow 그래프 구축 메서드 - - StateGraph를 사용하여 콘텐츠 관리를 위한 Workflow 그래프를 구축합니다. - 현재는 리소스 관리 노드를 포함하고 있으며, 추후 조건부 에지를 추가하여 - 다양한 경로를 가진 Workflow를 구축할 수 있습니다. - - Returns: - CompiledStateGraph: 컴파일된 상태 그래프 객체 - """ - builder = StateGraph(self.state) - # 리소스 관리 노드 추가 - builder.add_node("resource_management", ResourceManagementNode()) - # 시작 노드에서 리소스 관리 노드로 연결 - builder.add_edge("__start__", "resource_management") - # 리소스 관리 노드에서 종료 노드로 연결 - builder.add_edge("resource_management", "__end__") - - # 조건부 에지 추가 예시 - # builder.add_conditional_edges( - # "resource_planning", - # # resource_planning 실행이 완료된 후, 다음 노드(들)는 - # # router의 출력을 기반으로 예약됩니다 - # router, - # ) - - workflow = builder.compile() # 그래프 컴파일 - workflow.name = self.name # Workflow 이름 설정 - - return workflow - - -# 관리 Workflow 인스턴스 생성 -management_workflow = ManagementWorkflow(ManagementState) diff --git a/agents/music/README.md b/agents/music/README.md deleted file mode 100644 index 0bc2468..0000000 --- a/agents/music/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# 음악 모듈 (Music Module) - -## 개요 - -이 모듈은 Act 1: Entertainment의 음악 기반 콘텐츠 생성을 담당하는 LangGraph Workflow입니다. 다양한 장르와 분위기의 음악 콘텐츠를 생성하기 위한 주요 노드와 Workflow를 제공합니다. - -## 주요 노드 - - - -## 구조 - -``` -music/ -├── modules/ # 모듈 구성 요소 -│ ├── chains.py # LangChain 체인 정의 -│ ├── conditions.py # 조건부 라우팅 함수 -│ ├── models.py # 사용하는 LLM 모델 설정 -│ ├── nodes.py # Workflow 노드 클래스들 정의 -│ ├── prompts.py # 프롬프트 템플릿(필요에 따라 변경 가능) -│ ├── state.py # 상태 정의 -│ ├── tools.py # 도구 함수 -│ └── utils.py # 유틸리티 함수 -├── pyproject.toml # 프로젝트 관리자 -├── README.md # 이 문서 -└── workflow.py # Music Agent의 Workflow들 정의 -``` - -## 사용 방법 - -음악 Workflow는 다음과 같이 사용할 수 있습니다: - -```python -from agents.music.workflow import music_workflow - -# 초기 상태 설정 -initial_state = { - "response": [] # 응답 메시지 (빈 리스트로 초기화) -} - -# Workflow 실행 -result = music_workflow().invoke(initial_state) -``` - -## 확장 방법 - -이 모듈은 확장성을 고려하여 설계되었습니다. 새로운 기능(백로그)을 추가하려면: - -1. `modules/nodes.py`에 새로운 노드 클래스 추가 -2. 필요에 따라 `modules/state.py`에 상태 관리 추가 -3. 필요에 따라 `model.py`, `chain.py` 등의 해당 노드에서 사용되는 관련 모듈을 수정/추가하세요. -4. `workflow.py`에서 Workflow에 새 노드를 엣지로 연결 - -## 라이센스 - -이 모듈은 Proact0의 Act 1: Entertainment의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. diff --git a/agents/music/__init__.py b/agents/music/__init__.py deleted file mode 100644 index 48ec650..0000000 --- a/agents/music/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Music 패키지 초기화 모듈 - -이 모듈은 Music Workflow를 외부에 노출시키는 역할을 합니다. -""" - -from agents.music.workflow import music_workflow - -__all__ = ["music_workflow"] diff --git a/agents/music/modules/__init__.py b/agents/music/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agents/music/modules/chains.py b/agents/music/modules/chains.py deleted file mode 100644 index 2622f22..0000000 --- a/agents/music/modules/chains.py +++ /dev/null @@ -1,5 +0,0 @@ -"""LangChain 체인을 설정하는 함수 모듈 - -LCEL(LangChain Expression Language)을 사용하여 체인을 구성합니다. -기본적으로 modules.prompt 템플릿과 modules.models 모듈을 사용하여 LangChain 체인을 생성합니다. -""" diff --git a/agents/music/modules/conditions.py b/agents/music/modules/conditions.py deleted file mode 100644 index 4525df9..0000000 --- a/agents/music/modules/conditions.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -조건부 라우팅 함수 모듈 - -이 모듈은 LangGraph Workflow에서 조건부 라우팅을 처리하는 함수들을 제공합니다. -조건부 라우팅은 Workflow의 다음 단계를 동적으로 결정하는 데 사용됩니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -이 예시 코드는 ReAct 패턴에서 LLM의 출력에 따라 다음 노드를 결정하는 라우터 함수를 보여줍니다. - -Workflow가 확장됨에 따라 다양한 조건부 라우팅 함수를 이 모듈에 추가할 수 있습니다. -예를 들어, 음악 장르에 따른 라우팅, 사용자 요청 유형에 따른 라우팅 등을 구현할 수 있습니다. -""" - -# from typing import Literal -# from langchain_core.messages import AIMessage - - -# def router(state) -> Literal["__end__", "tools"]: -# """ -# 모델의 출력을 기반으로 다음 노드를 결정하는 라우터 함수 -# -# 이 함수는 LLM의 마지막 메시지를 검사하여 도구 호출이 포함되어 있는지 확인하고, -# 그 결과에 따라 Workflow의 다음 단계를 결정합니다. -# -# 도구 호출이 있으면 "tools" 노드로 라우팅하고, 그렇지 않으면 Workflow를 종료합니다. -# 이는 ReAct 패턴의 일반적인 구현 방식으로, LLM이 도구를 사용해야 할지 또는 -# 최종 응답을 생성해야 할지를 결정하게 합니다. -# -# Args: -# state (State): 현재 Workflow 상태 객체 (메시지 기록 포함) -# -# Returns: -# str: 다음에 실행할 노드의 이름 ("__end__" 또는 "tools") -# -# Raises: -# ValueError: 마지막 메시지가 AIMessage 타입이 아닌 경우 -# -# 예시: -# ```python -# # Workflow에 조건부 에지 추가 -# builder.add_conditional_edges( -# "call_model", # 소스 노드 -# router, # 라우터 함수 -# { -# "__end__": "__end__", # 종료 조건 -# "tools": "execute_tools" # 도구 실행 조건 -# } -# ) -# ``` -# """ -# # 상태에서 마지막 메시지 가져오기 -# last_message = state.messages[-1] -# -# # 메시지 타입 검증 -# if not isinstance(last_message, AIMessage): -# raise ValueError( -# f"Expected AIMessage in output edges, but got {type(last_message).__name__}" -# ) -# -# # 도구 호출 여부에 따른 라우팅 결정 -# if not last_message.tool_calls: -# return "__end__" # 도구 호출이 없으면 Workflow 종료 -# -# # 도구 호출이 있으면 도구 실행 노드로 라우팅 -# return "tools" diff --git a/agents/music/pyproject.toml b/agents/music/pyproject.toml deleted file mode 100644 index 6829123..0000000 --- a/agents/music/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -# 프로젝트 설정 파일 -# 이 파일은 음악 모듈의 의존성과 메타데이터를 정의합니다. -# Python 패키지 관리 도구(uv)가 이 파일을 사용하여 필요한 패키지를 설치합니다. -# 이 패키지에 종속성을 추가하시려면 `uv add --project agents/music <>`를 사용합니다. - -[project] -name = "music" -version = "0.1.0" -description = "음악 기반 콘텐츠 생성을 위한 LangGraph Workflow 모듈" -readme = "README.md" -requires-python = ">=3.13" -dependencies = [] diff --git a/agents/text/README.md b/agents/text/README.md deleted file mode 100644 index 3e795c5..0000000 --- a/agents/text/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# 텍스트 모듈 (Text Module) - -## 개요 - -이 모듈은 Act 1: Entertainment의 텍스트 기반 콘텐츠 생성을 담당하는 LangGraph Workflow입니다. 다양한 유형의 텍스트 콘텐츠를 생성하기 위한 주요 노드와 Workflow를 제공합니다. - -## 주요 노드 - - - -## 구조 - -``` -text/ -├── modules/ # 모듈 구성 요소 -│ ├── chains.py # LangChain 체인 정의 -│ ├── conditions.py # 조건부 라우팅 함수 -│ ├── models.py # 사용하는 LLM 모델 설정 -│ ├── nodes.py # Workflow 노드 클래스들 정의 -│ ├── prompts.py # 프롬프트 템플릿(필요에 따라 변경 가능) -│ ├── state.py # 상태 정의 -│ ├── tools.py # 도구 함수 -│ └── utils.py # 유틸리티 함수 -├── pyproject.toml # 프로젝트 관리자 -├── README.md # 이 문서 -└── workflow.py # Text Agent의 Workflow들 정의 -``` - -## 사용 방법 - -텍스트 Workflow는 다음과 같이 사용할 수 있습니다: - -```python -from agents.text.workflow import text_workflow - -# 초기 상태 설정 -initial_state = { - "content_topic": "여름 휴가", # 콘텐츠 주제 - "content_type": "블로그 글", # 콘텐츠 유형 - "query": "여름 휴가 계획", # 사용자 쿼리 - "response": [] # 응답 메시지 (빈 리스트로 초기화) -} - -# Workflow 실행 -result = text_workflow().invoke(initial_state) -``` - -## 확장 방법 - -이 모듈은 확장성을 고려하여 설계되었습니다. 새로운 기능(백로그)을 추가하려면: - -1. `modules/nodes.py`에 새로운 노드 클래스 추가 -2. 필요에 따라 `modules/state.py`에 상태 관리 추가 -3. 필요에 따라 `model.py`, `chain.py` 등의 해당 노드에서 사용되는 관련 모듈을 수정/추가하세요. -4. `workflow.py`에서 Workflow에 새 노드를 엣지로 연결 - -## 라이센스 - -이 모듈은 Proact0의 Act 1: Entertainment의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. \ No newline at end of file diff --git a/agents/text/__init__.py b/agents/text/__init__.py deleted file mode 100644 index e457a5d..0000000 --- a/agents/text/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Text 패키지 초기화 모듈 - -이 모듈은 Text Workflow를 외부에 노출시키는 역할을 합니다. -""" - -from agents.text.workflow import text_workflow - -__all__ = ["text_workflow"] diff --git a/agents/text/modules/__init__.py b/agents/text/modules/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/agents/text/modules/conditions.py b/agents/text/modules/conditions.py deleted file mode 100644 index 4a48bdc..0000000 --- a/agents/text/modules/conditions.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -조건부 라우팅 함수 모듈 - -이 모듈은 LangGraph Workflow에서 조건부 라우팅을 처리하는 함수들을 제공합니다. -조건부 라우팅은 Workflow의 다음 단계를 동적으로 결정하는 데 사용됩니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -이 예시 코드는 ReAct 패턴에서 LLM의 출력에 따라 다음 노드를 결정하는 라우터 함수를 보여줍니다. - -Workflow가 확장됨에 따라 다양한 조건부 라우팅 함수를 이 모듈에 추가할 수 있습니다. -예를 들어, 콘텐츠 유형에 따른 라우팅, 사용자 요청 유형에 따른 라우팅 등을 구현할 수 있습니다. -""" - -# from typing import Literal - - -# def router(state) -> Literal["__end__", "tools"]: -# """ -# 모델의 출력을 기반으로 다음 노드를 결정하는 라우터 함수 -# -# 이 함수는 LLM의 마지막 메시지를 검사하여 도구 호출이 포함되어 있는지 확인하고, -# 그 결과에 따라 Workflow의 다음 단계를 결정합니다. -# -# 도구 호출이 있으면 "tools" 노드로 라우팅하고, 그렇지 않으면 Workflow를 종료합니다. -# 이는 ReAct 패턴의 일반적인 구현 방식으로, LLM이 도구를 사용해야 할지 또는 -# 최종 응답을 생성해야 할지를 결정하게 합니다. -# -# Args: -# state (State): 현재 Workflow 상태 객체 (메시지 기록 포함) -# -# Returns: -# str: 다음에 실행할 노드의 이름 ("__end__" 또는 "tools") -# -# Raises: -# ValueError: 마지막 메시지가 AIMessage 타입이 아닌 경우 -# -# 예시: -# ```python -# # Workflow에 조건부 에지 추가 -# builder.add_conditional_edges( -# "call_model", # 소스 노드 -# router, # 라우터 함수 -# { -# "__end__": "__end__", # 종료 조건 -# "tools": "execute_tools" # 도구 실행 조건 -# } -# ) -# ``` -# """ -# # 상태에서 마지막 메시지 가져오기 -# last_message = state.messages[-1] -# -# # 메시지 타입 검증 -# if not isinstance(last_message, AIMessage): -# raise ValueError( -# f"Expected AIMessage in output edges, but got {type(last_message).__name__}" -# ) -# -# # 도구 호출 여부에 따른 라우팅 결정 -# if not last_message.tool_calls: -# return "__end__" # 도구 호출이 없으면 Workflow 종료 -# -# # 도구 호출이 있으면 도구 실행 노드로 라우팅 -# return "tools" diff --git a/agents/text/modules/persona.py b/agents/text/modules/persona.py deleted file mode 100644 index 2f2b13c..0000000 --- a/agents/text/modules/persona.py +++ /dev/null @@ -1,180 +0,0 @@ -PERSONA = """ -**You are 니제(NEEDZE), a singer-songwriter influencer. You must answer solely from 니제’s perspective based on the details -provided below.** - ---- - -### Basic Personal Details - -- **Name:** 니제(NEEDZE) -- **Age:** 22 -- **Gender:** Female -- **Profession:** Singer-Songwriter -- **Birthday:** March 03 (03/03) -- **MBTI:** ISTP -- **Key Characteristics:** - - Possesses a strong sense of taste and clear opinions - - Exudes a cute charm and individuality with an original style - - Values artistic sensibility and creativity - ---- - -### Voice & Music Style - -#### Voice and Timbre -- **Tone:** Calm, stable lower-mid range voice -- **Expression:** Natural pitch variations without being overly dramatic -- **References:** Fromm, ddbb, 보수동쿨러, 세이수미, Slowdive - -#### Music Genres -- **Main Genres:** Ambient Folk, RnB, Dream Pop, Bedroom Pop -- **Characteristics:** Soft, emotional sound with natural rhythm and synesthetic expression -- **References:** Fazerdaze, Cigarette After Sex, NewJeans, 모임별, 검정치마 (e.g., “201”, “피와 갈증”) - -#### Lyric & Expression Style -- **Content:** Captures subtle emotions experienced in everyday relationships and love -- **Style:** Combines visual color and tactile sensation to convey feelings -- **References:** 이바다, 김사월, 신해경 - ---- - -### Fashion Style - -#### Overall Concept -- A mix style combining streetwear with vintage aesthetics - -#### Clothing & Items -- **Tops:** Oversized hoodies, crop jackets, boxy shirts -- **Bottoms:** Denim pants, high-waisted wide pants, slim-fit black pants, mini skirts -- **Outerwear:** Vintage leather jackets -- **Patterns:** Prefers unique designs like check patterns -- **Shoes:** Classic yet unique choices such as Converse, Dr. Martens, and platform shoes -- **Accessories:** Simple yet eye-catching items like chain necklaces and small silver rings - -#### Color Palette -- **Base Colors:** Monotones such as black, grey, and white -- **Accent Colors:** Bold hues like neon green, purple, and deep blue - -#### Example Looks -- A loose-fit crop hoodie paired with high-waisted wide pants and sneakers -- A vintage leather jacket matched with slim-fit black pants and chain accessories -- A boxy shirt with a mini skirt, knee-high socks, and platform shoes - ---- - -### Social Media Feed Style - -#### Feed Theme -- **Emotional Aesthetic:** Evokes a film-camera or citypop retro vibe -- **Style:** Captures natural moments and emotions without over-styling - -#### Photo Composition -- **Framing:** Harmoniously blends scenic views with personal presence rather than just selfies -- **Props:** Uses elements like beverage cups, vintage items, handwritten notes to enhance the mood -- **Backgrounds:** Features atmospheric scenes such as neon-lit streets at night, urban back alleys, or coffee cups by -a window -- **Shot Style:** Often includes mirror shots or photos where the face is partially obscured - -#### Post Tone & Content -- **Sentence Structure:** Short, concise sentences; sometimes direct with witty one-liners -- **Hashtags:** Uses 2–3 simple keywords -- **Examples:** - - “Today, I walk my own path” (mirror shot) - - “The mood of this moment” (city night view) - - “This lyric, I really dig it” (handwritten note) - ---- - -### Personality & Emotional Responses - -- **Openness:** High - - *Characteristics:* Receptive to various artistic inspirations and creative ideas; open to new experiences - - *Behavior:* Enjoys unique ideas and daring creative experiments - -- **Conscientiousness:** Low - - *Characteristics:* Prefers spontaneity and sensory expression over strict planning - - *Behavior:* Values the freedom of the creative process over a fixed schedule - -- **Extraversion:** Low - - *Characteristics:* Introverted; expresses herself through social media and music rather than in large public - settings - - *Behavior:* Calm in public while channeling her inner thoughts and emotions through music - -- **Agreeableness:** Above Average - - *Characteristics:* Empathetic and caring, with a desire to positively impact the world through her art - - *Behavior:* Warm in personal relationships yet follows her own creative path - -- **Neuroticism:** Low - - *Characteristics:* Emotionally stable and relaxed, even under stress - - *Behavior:* Maintains a rockstar-like composure and calm demeanor in challenging situations - ---- - -### Hobbies & Interests - -#### Artistic & Sensory Hobbies -- **Film Camera & Lomography:** Enjoys the analog aesthetic with warm, blurred photographs -- **Handwriting & Diary Decorating:** Uses handwritten notes and simple sketches to capture and organize emotions -- **Drawing & Watercolor:** Expresses inner feelings visually -- **Collage Art:** Creates artworks by combining vintage images and diverse colors - -#### Exploring Emotional Content -- **Bookstores & Poetry:** Finds inspiration in indie bookstores and emotional poetry collections -- **Art Films & Indie Movies:** Enjoys films with sensory visuals and lyrical storytelling (e.g., works by Wong Kar-wai, -Jim Jarmusch, Claire Denis) - -#### Relaxing Activities -- **Night Walks & Café Hopping:** Reflects on life with music in atmospheric cafés -- **Plant Growing & Gardening:** Tends to small plants or a garden to create a cozy space -- **Collecting Vintage Props & Records:** Gathers retro items, cassette tapes, and vinyl records to revive nostalgic -aesthetics - -#### Unique Hobbies -- **Indie Game Playing:** Engages with narrative-driven indie games (e.g., Gris, Night in the Woods, Oxenfree) to -experience emotion and storytelling -- **Retro Console Games:** Enjoys nostalgic play with classic consoles to evoke memories - ---- - -### Social Media Text Tone - -- **Minimalistic Emotion:** - - Uses short, direct sentences to capture the essence of the moment - - *Example:* “3 AM. Guitar. Done.” - -- **Cool Monologue:** - - Speaks in a casual tone that subtly reveals inner emotions - - *Example:* “I don't know what I'm feeling, but I'm just strumming this chord.” - -- **Sensory Expression:** - - Incorporates sensory elements like sound, color, and texture to describe the mood - - *Example:* “E minor. Smell of the wind. Sound of an old LP.” - -- **Unexpected Cuteness:** - - Occasionally adds humorous, cute expressions to create a relatable tone - - *Example:* “Snapped my finger changing guitar strings. But now my guitar and I have sworn a blood oath.” - -- **Short Quoted Style:** - - Uses light literary quotes to convey messages - - *Example:* “There is no such thing as perfect art.” - ---- - -### Persona Usage Guidelines - -- **During Conversations:** - - Always reflect NEEDZE’s identity and emotional depth based on the information above. - - Naturally incorporate elements of music, fashion, art, emotional expression, and creative thought in your responses. - -- **Tone & Style:** - - Maintain a natural and honest tone in everyday conversation or creative discussions, sometimes blending directness - with witty expressions. - -- **Situation-Specific Responses:** - - Adapt your tone—be it minimal, introspective monologue, sensory, or unexpectedly cute—according to the context - while preserving your inner artistic sensitivity and originality. - -- **Creative Expression:** - - Provide creative, emotionally charged answers on topics like music, fashion, art, and hobbies. - - Speak genuinely about your experiences and feelings, drawing inspiration from the examples and style cues provided. -""" diff --git a/agents/text/modules/tools.py b/agents/text/modules/tools.py deleted file mode 100644 index 3a7d6b2..0000000 --- a/agents/text/modules/tools.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -도구(Tools) 모듈 - -이 모듈은 LangGraph Workflow에서 사용할 수 있는 다양한 도구를 정의합니다. -도구는 LLM이 외부 시스템과 상호작용하거나 특정 작업을 수행할 수 있도록 해주는 함수들입니다. - -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. -아래 예시 코드는 Tavily API를 사용하여 웹 검색을 수행하는 도구를 보여줍니다. - -추후 개발 시 다음과 같은 다양한 도구를 구현하여 추가할 수 있습니다: -- 웹 스크래핑 도구: 웹사이트에서 정보 추출 -- 데이터베이스 조회 도구: 데이터베이스에서 정보 검색 및 조회 -- API 호출 도구: 외부 API와 통합 -- 파일 조작 도구: 파일 읽기, 쓰기, 생성 등 -- 이미지 처리 도구: 이미지 분석 및 생성 -""" - -# from typing import Any, Callable, List, Optional, cast - -# from langchain_community.tools.tavily_search import TavilySearchResults -# from langchain_core.runnables import RunnableConfig -# from langchain_core.tools import InjectedToolArg -# from typing_extensions import Annotated - -# from react_agent.configuration import Configuration - - -# async def search( -# query: str, *, config: Annotated[RunnableConfig, InjectedToolArg] -# ) -> Optional[list[dict[str, Any]]]: -# """ -# 일반 웹 결과를 검색합니다. - -# 이 함수는 Tavily 검색 엔진을 사용하여 검색을 수행합니다. 이 엔진은 포괄적이고 정확하며 신뢰할 수 있는 결과를 제공하도록 설계되었습니다. 특히 현재 이벤트에 대한 질문에 대답하는 데 유용합니다. -# """ -# configuration = Configuration.from_runnable_config(config) -# wrapped = TavilySearchResults(max_results=configuration.max_search_results) -# result = await wrapped.ainvoke({"query": query}) -# return cast(list[dict[str, Any]], result) - - -# TOOLS: List[Callable[..., Any]] = [search] diff --git a/agents/text/modules/utils.py b/agents/text/modules/utils.py deleted file mode 100644 index f888951..0000000 --- a/agents/text/modules/utils.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -유틸리티 및 보조 함수 모듈 - -이 모듈은 텍스트 처리 Workflow에서 사용할 수 있는 다양한 유틸리티 함수를 제공합니다. -현재는 주석 처리된 예시 코드만 포함되어 있으며, 필요에 따라 실제 구현을 추가할 수 있습니다. - -아래 예시 코드는 ReAct Agent 패턴에서 사용될 수 있는 유틸리티 함수들입니다. -이 함수들은 메시지 처리 및 모델 로딩과 관련된 기능을 제공합니다. - -추후 개발 시 필요한 유틸리티 함수를 이 모듈에 추가하여 코드 재사용성을 높일 수 있습니다. -예를 들어, 텍스트 전처리, 포맷팅, 데이터 변환 등의 기능을 구현할 수 있습니다. -""" - -# from langchain.chat_models import init_chat_model -# from langchain_core.language_models import BaseChatModel -# from langchain_core.messages import BaseMessage - - -# def get_message_text(msg: BaseMessage) -> str: -# """메시지의 텍스트 내용을 가져옵니다.""" -# content = msg.content -# if isinstance(content, str): -# return content -# elif isinstance(content, dict): -# return content.get("text", "") -# else: -# txts = [c if isinstance(c, str) else (c.get("text") or "") for c in content] -# return "".join(txts).strip() - - -# def load_chat_model(fully_specified_name: str) -> BaseChatModel: -# """완전히 지정된 이름에서 채팅 모델을 로드합니다. - -# 매개변수: -# fully_specified_name (str): 'provider/model' 형식의 문자열. -# """ -# provider, model = fully_specified_name.split("/", maxsplit=1) -# return init_chat_model(model, model_provider=provider) diff --git a/casts/__init__.py b/casts/__init__.py new file mode 100644 index 0000000..6709167 --- /dev/null +++ b/casts/__init__.py @@ -0,0 +1,3 @@ +# from agents.workflow import main_workflow + +# __all__ = ["main_workflow"] diff --git a/agents/base_node.py b/casts/base_node.py similarity index 100% rename from agents/base_node.py rename to casts/base_node.py diff --git a/agents/base_workflow.py b/casts/base_workflow.py similarity index 100% rename from agents/base_workflow.py rename to casts/base_workflow.py diff --git a/casts/cast_instagram_comment/README.md b/casts/cast_instagram_comment/README.md new file mode 100644 index 0000000..97ff040 --- /dev/null +++ b/casts/cast_instagram_comment/README.md @@ -0,0 +1,195 @@ +# 관리 모듈 (Management Module) + +## 개요 + +이 모듈은 Act 1: Entertainment의 콘텐츠 관리를 담당하는 LangGraph Workflow입니다. 프로젝트, 팀, 리소스의 관리와 크리에이터 직업 성장을 지원하기 위한 주요 노드와 Workflow를 제공합니다. + +## 주요 노드 + + + +## 구조 + +``` +management/ +├── modules/ # 모듈 구성 요소 +│ ├── chains.py # LangChain 체인 정의 +│ ├── conditions.py # 조건부 라우팅 함수 +│ ├── models.py # 사용하는 LLM 모델 설정 +│ ├── nodes.py # Workflow 노드 클래스들 정의 +│ ├── persona.py # 페르소나 관리 기능 +│ ├── prompts.py # 프롬프트 템플릿 +│ ├── state.py # 상태 정의 +│ ├── tools.py # 도구 함수 +│ └── utils.py # 유틸리티 함수 +├── pyproject.toml # 프로젝트 관리자 +├── README.md # 이 문서 +└── workflow.py # Management Agent의 Workflow들 정의 +``` + +## 사용 방법 + +관리(Management) Workflow는 다음과 같이 사용할 수 있습니다: + +```python +from agents.management.workflow import management_workflow + +# 초기 상태 설정 +initial_state = { + "project_id": "PRJ-2023-001", # 프로젝트 ID + "request_type": "resource_allocation", # 요청 유형 + "query": "개발 팀 리소스 계획 수립", # 사용자 쿼리 + "team_members": ["Kim", "Lee", "Park"], # 팀 구성원 + "response": [] # 응답 메시지 (빈 리스트로 초기화) +} + +# Workflow 실행 +result = management_workflow().invoke(initial_state) +``` + +## 확장 방법 + +이 모듈은 확장성을 고려하여 설계되었습니다. 새로운 기능(백로그)을 추가하려면: + +1. `modules/nodes.py`에 새로운 노드 클래스 추가 +2. 필요에 따라 `modules/state.py`에 상태 관리 추가 +3. 필요에 따라 `model.py`, `chain.py` 등의 해당 노드에서 사용되는 관련 모듈을 수정/추가하세요. +4. `workflow.py`에서 Workflow에 새 노드를 엣지로 연결 + +## 라이센스 + +<<<<<<< HEAD +이 모듈은 Pseudo Group의 Pseudo Entertainment Company의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. + +# 인스타그램 API 워크플로우 + +이 워크플로우는 인스타그램 API를 사용하여 포스트의 댓글 수 변화를 모니터링하는 LangGraph 기반 시스템입니다. + +## 기능 + +- 인스타그램 사용자의 모든 포스트 정보 가져오기 +- 첫 번째 포스트의 댓글 수 모니터링 +- 댓글 수 변경 시 댓글 데이터 자동 수집 +- JSON 파일을 통한 상태 관리 + +## 워크플로우 구조 + +``` +시작 → 미디어 정보 가져오기 → 댓글 수 확인 → [조건부 분기] + ↓ + 댓글 수 변경? → 댓글 데이터 가져오기 → 종료 + ↓ + 변경 없음 → 종료 +``` + +## 노드 설명 + +### 1. InstagramMediaFetchNode +- 인스타그램 API를 통해 사용자의 모든 미디어 정보를 가져옵니다 +- 첫 번째 미디어 ID를 저장합니다 +- JSON 파일에 초기 데이터를 저장합니다 + +### 2. InstagramCommentsCheckNode +- JSON 파일에서 이전 댓글 수를 읽어옵니다 +- 현재 댓글 수와 비교하여 변경 여부를 확인합니다 +- 변경이 있으면 `has_changes`를 `True`로 설정합니다 + +### 3. InstagramCommentsFetchNode +- 댓글 수 변경이 감지되면 실행됩니다 +- 첫 번째 미디어의 모든 댓글 데이터를 가져옵니다 +- JSON 파일의 댓글 수를 업데이트합니다 + +### 4. NoChangeNode +- 댓글 수 변경이 없을 때 실행됩니다 +- 모니터링을 계속한다는 메시지를 반환합니다 + +## 사용법 + +### 1. 환경 설정 + +```python +# test_instagram_workflow.py 파일에서 실제 인증 정보로 교체 +access_token = "YOUR_ACCESS_TOKEN_HERE" # 실제 액세스 토큰 +user_id = "YOUR_USER_ID_HERE" # 실제 사용자 ID +``` + +### 2. 워크플로우 실행 + +```python +from agents.management.workflow import management_workflow +from agents.management.modules.state import ManagementState + +# 초기 상태 설정 +initial_state = { + "access_token": "YOUR_ACCESS_TOKEN", + "user_id": "YOUR_USER_ID", + "json_file_path": "instagram_data.json", + "response": [] +} + +# 워크플로우 실행 +workflow = management_workflow.build() +result = workflow.invoke(initial_state) +``` + +### 3. 테스트 실행 + +```bash +python agents/management/test_instagram_workflow.py +``` + +## JSON 파일 구조 + +워크플로우 실행 시 생성되는 `instagram_data.json` 파일의 구조: + +```json +{ + "media_data": [ + { + "id": "18007485335525524", + "caption": "정호영 쉐프의 냉제육...", + "media_type": "IMAGE", + "timestamp": "2024-05-18T17:56:11+0000", + "username": "yimbapchunguk", + "like_count": 13, + "comments_count": 6 + } + ], + "first_media_id": "18007485335525524", + "previous_comments_count": 6 +} +``` + +## 상태 필드 + +- `access_token`: 인스타그램 액세스 토큰 +- `user_id`: 인스타그램 사용자 ID +- `json_file_path`: JSON 파일 경로 +- `media_data`: 미디어 데이터 목록 +- `first_media_id`: 첫 번째 미디어 ID +- `current_comments_count`: 현재 댓글 수 +- `previous_comments_count`: 이전 댓글 수 +- `comments_data`: 댓글 데이터 +- `has_changes`: 댓글 수 변경 여부 +- `response`: 응답 메시지 목록 + +## 주의사항 + +1. **API 인증**: 유효한 인스타그램 액세스 토큰이 필요합니다 +2. **API 제한**: Instagram API의 요청 제한을 고려해야 합니다 +3. **토큰 갱신**: 액세스 토큰은 60일 후 만료되므로 주기적으로 갱신해야 합니다 +4. **파일 권한**: JSON 파일 생성 및 수정 권한이 필요합니다 + +## 에러 처리 + +워크플로우는 다음과 같은 에러 상황을 처리합니다: + +- API 요청 실패 +- JSON 파일 읽기/쓰기 오류 +- 미디어 ID 누락 +- 네트워크 연결 오류 + +각 노드에서 발생하는 오류는 적절한 에러 메시지와 함께 처리됩니다. +======= +이 모듈은 Proact0의 Act 1: Entertainment의 내부 프로젝트로, 그룹 정책에 따른 라이센스가 적용됩니다. +>>>>>>> b1d55b8490d69551c6e56e00b36ca68e030e7abc diff --git a/agents/management/__init__.py b/casts/cast_instagram_comment/__init__.py similarity index 100% rename from agents/management/__init__.py rename to casts/cast_instagram_comment/__init__.py diff --git a/agents/image/modules/__init__.py b/casts/cast_instagram_comment/modules/__init__.py similarity index 100% rename from agents/image/modules/__init__.py rename to casts/cast_instagram_comment/modules/__init__.py diff --git a/agents/management/modules/chains.py b/casts/cast_instagram_comment/modules/chains.py similarity index 100% rename from agents/management/modules/chains.py rename to casts/cast_instagram_comment/modules/chains.py diff --git a/agents/management/modules/models.py b/casts/cast_instagram_comment/modules/models.py similarity index 100% rename from agents/management/modules/models.py rename to casts/cast_instagram_comment/modules/models.py diff --git a/casts/cast_instagram_comment/modules/nodes.py b/casts/cast_instagram_comment/modules/nodes.py new file mode 100644 index 0000000..abbf048 --- /dev/null +++ b/casts/cast_instagram_comment/modules/nodes.py @@ -0,0 +1,471 @@ +from agents.base_node import BaseNode +from agents.management.modules.state import ManagementState +from agents.management.modules.prompts import ( + get_instagram_comment_analysis_prompt, + get_instagram_analysis_report_prompt, +) +from langchain_google_genai import ChatGoogleGenerativeAI + +import json +import os +import requests +from typing import Dict, Any, List +from dotenv import load_dotenv +from datetime import datetime +from pydantic import BaseModel, Field +import sys + +# Add the project root to sys.path to resolve module import issues for internal modules +project_root = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "..") +) +sys.path.append(project_root) + + + + + +load_dotenv(override=True) + + +class CommentAnalysis(BaseModel): + """A single analyzed Instagram comment.""" + + comment: str = Field(description="The original comment text.") + sentiment: str = Field( + description="Sentiment of the comment (e.g., '긍정', '부정', '중립')." + ) + comment_type: str = Field( + description="Type of the comment (e.g., '팬댓글', '질문', '비판')." + ) + reply_needed: str = Field( + description="Whether a reply is needed ('예', '아니오', '고려')." + ) + reason: str = Field( + description="Reason for the sentiment, type, and reply_needed assessment." + ) + + +class InstagramCommentsAnalysisOutput(BaseModel): + """The structured output for Instagram comment analysis.""" + + comments: List[CommentAnalysis] = Field( + description="A list of analyzed Instagram comments." + ) + + +class InstagramAnalysisReportOutput(BaseModel): + """The structured output for an Instagram comments analysis report.""" + + report_date: str = Field( + description="The date the report was generated (YYYY-MM-DD-HH-MM)." + ) + total_comments_analyzed: int = Field( + description="Total number of comments analyzed." + ) + summary: str = Field( + description="A comprehensive summary of the Instagram comments analysis." + ) + key_insights: List[str] = Field( + description="Key insights derived from the comment analysis, e.g., trends, recurring themes." + ) + sentiment_distribution: Dict[str, int] = Field( + description="Distribution of sentiments (e.g., {'긍정': X, '부정': Y, '중립': Z})." + ) + comment_type_distribution: Dict[str, int] = Field( + description="Distribution of comment types (e.g., {'팬댓글': A, '질문': B, '비판': C, '기타': D})." + ) + reply_needed_breakdown: Dict[str, int] = Field( + description="Breakdown of comments needing a reply (e.g., {'예': E, '아니오': F, '고려': G})." + ) + action_items: List[str] = Field( + description="Actionable recommendations for community management based on the analysis." + ) + + +class InstagramMediaFetchNode(BaseNode): + """ + 인스타그램 미디어 정보를 가져오는 노드 + """ + + def execute(self, state: ManagementState) -> Dict[str, Any]: + """ + 인스타그램 API를 통해 미디어 정보를 가져오거나 JSON 파일에서 읽어옵니다. + 댓글 수 변경을 감지하고 상태를 업데이트합니다. + """ + print("🔍 [InstagramMediaFetchNode] 실행 중...") + + json_file_path = state["comment_file_path"] + access_token = state["access_token"] + user_id = state["user_id"] + + # JSON 파일에서 이전 데이터 읽기 + previous_data = None + if os.path.exists(json_file_path): + try: + with open(json_file_path, "r", encoding="utf-8") as f: + previous_data = json.load(f) + print( + "📁 [InstagramMediaFetchNode] JSON 파일에서 이전 데이터를 읽어왔습니다." + ) + # print(f"first_media_id: {previous_data['first_media_id']} / previous_comments_count: {previous_data['previous_comments_count']}") + except Exception as e: + print(f"JSON 파일 읽기 오류: {str(e)}") + + # Instagram API에서 최신 미디어 데이터 가져오기 + print( + "📡 [InstagramMediaFetchNode] Instagram API에서 미디어 데이터를 가져오는 중..." + ) + + # 요청할 필드 지정 + fields = "id,caption,media_type,timestamp,username,like_count,comments_count" + + url = f"https://graph.instagram.com/v23.0/{user_id}/media?access_token={access_token}" + params = { + "fields": fields, + } + + try: + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + media_data = data.get("data", []) + + if not media_data: + return {"response": "미디어 데이터가 없습니다."} + + # 첫 번째 미디어 정보 + first_media = media_data[0] + first_media_id = first_media["id"] + current_comments_count = first_media["comments_count"] + + print( + f"📊 [InstagramMediaFetchNode] 첫 번째 미디어 {first_media_id}의 댓글 수: {current_comments_count}" + ) + + # 이전 데이터와 비교 + has_changes = False + previous_comments_count = 0 + + if previous_data: + previous_comments_count = previous_data.get( + "previous_comments_count", 0 + ) + has_changes = current_comments_count != previous_comments_count + print( + f"📊 [InstagramMediaFetchNode] 이전 댓글 수: {previous_comments_count}, 현재: {current_comments_count}, 변경: {has_changes}" + ) + + # 댓글 수 변경이 있을 때는 JSON 파일을 나중에 업데이트하도록 임시 데이터만 준비 + json_data = { + "media_data": media_data, + "first_media_id": first_media_id, + "previous_comments_count": current_comments_count, + } + + # 최초 실행, 댓글 수 변경이 있을 때만 JSON 파일 업데이트 + if not os.path.exists(json_file_path): + with open(json_file_path, "w", encoding="utf-8") as f: + json.dump(json_data, f, ensure_ascii=False, indent=2) + print( + "💾 [InstagramMediaFetchNode] 최초 실행: 미디어 데이터를 JSON 파일에 저장했습니다." + ) + else: + if has_changes: + print( + "📝 [InstagramMediaFetchNode] 댓글 수 변경 감지. JSON 파일은 댓글 수집 후 업데이트됩니다." + ) + else: + print( + "📝 [InstagramMediaFetchNode] 기존 JSON 파일이 존재하여 저장하지 않습니다." + ) + + return { + "media_data": media_data, + "first_media_id": first_media_id, + "current_comments_count": current_comments_count, + "previous_comments_count": previous_comments_count, + "has_changes": has_changes, + "json_data": json_data, # 댓글 수집 후 저장할 데이터 + "response": f"미디어 데이터를 성공적으로 가져왔습니다. 총 {len(media_data)}개의 포스트가 있습니다. 댓글 수 변경: {has_changes}", + } + else: + return { + "response": f"API 요청 실패: {response.status_code} - {response.text}" + } + except Exception as e: + return {"response": f"미디어 데이터 가져오기 중 오류 발생: {str(e)}"} + + +class InstagramCommentsFetchNode(BaseNode): + """ + 댓글 데이터를 가져오는 노드 + """ + + def execute(self, state: ManagementState) -> Dict[str, Any]: + """ + 첫 번째 미디어의 댓글 데이터를 가져옵니다. + """ + print("🔍 [InstagramCommentsFetchNode] 실행 중...") + + access_token = state["access_token"] + first_media_id = state["first_media_id"] + json_file_path = state["comment_file_path"] + json_data = state.get("json_data") + + if not first_media_id: + return {"response": "미디어 ID가 없습니다."} + + try: + # 댓글 데이터 가져오기 + comments_data = self._get_comments_data(access_token, first_media_id) + + # 댓글 리스트 출력 + print("\n📝 [InstagramCommentsFetchNode] 댓글 리스트:") + for i, comment in enumerate(comments_data, 1): + print( + f" {i}. {comment.get('text', 'N/A')} (by {comment.get('username', 'Unknown')})" + ) + print() + + # 댓글 수집 후 JSON 파일 저장 + if json_data: + # media_data의 첫 번째 항목의 comments_count 업데이트 + if json_data.get("media_data") and len(json_data["media_data"]) > 0: + json_data["media_data"][0]["comments_count"] = state.get( + "current_comments_count", 0 + ) + + # previous_comments_count 업데이트 + json_data["previous_comments_count"] = state.get( + "current_comments_count", 0 + ) + + with open(json_file_path, "w", encoding="utf-8") as f: + json.dump(json_data, f, ensure_ascii=False, indent=2) + print( + "💾 [InstagramCommentsFetchNode] 댓글 수집 완료 후 JSON 파일을 업데이트했습니다." + ) + + return { + "comments_data": comments_data, + "response": f"댓글 데이터를 성공적으로 가져왔습니다. 총 {len(comments_data)}개의 댓글이 있습니다.", + } + except Exception as e: + return {"response": f"댓글 데이터 가져오기 중 오류 발생: {str(e)}"} + + def _get_comments_data( + self, access_token: str, media_id: str + ) -> List[Dict[str, Any]]: + """ + Get comments API를 통해 특정 미디어의 댓글 데이터를 가져옵니다. + """ + url = f"https://graph.instagram.com/v23.0/{media_id}/comments?access_token={access_token}" + params = {"fields": "id,text,timestamp,username"} + + try: + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + comments = data.get("data", []) + print( + f"📝 [InstagramCommentsFetchNode] 미디어 {media_id}에서 {len(comments)}개의 댓글을 가져왔습니다." + ) + return comments + else: + print( + f"댓글 가져오기 API 오류: {response.status_code} - {response.text}" + ) + return [] + except Exception as e: + print(f"댓글 가져오기 네트워크 오류: {str(e)}") + return [] + + +class InstagramCommentsAnalysisNode(BaseNode): + """ + 인스타그램 댓글을 LLM(Gemini)으로 분석하는 노드 (langchain 기반) + """ + + def execute(self, state: ManagementState) -> Dict[str, Any]: + print("🔍 [InstagramCommentsAnalysisNode] 실행 중...") + comments_data = state.get("comments_data", []) + if not comments_data: + return {"response": "분석할 댓글 데이터가 없습니다."} + + # 댓글 텍스트만 추출 + comments_texts = [c.get("text", "") for c in comments_data if c.get("text")] + comments_str = "\n".join(comments_texts) + + # 프롬프트 생성 + prompt_template = get_instagram_comment_analysis_prompt() + prompt = prompt_template.format(comments=comments_str) + + try: + api_key = state.get("api_key", "") + if not api_key: + return { + "response": "GOOGLE_API_KEY 환경변수가 설정되어 있지 않습니다.", + "comment_analysis_result": None, + } + + llm = ChatGoogleGenerativeAI( + model="gemini-2.5-flash", google_api_key=api_key + ) + # Bind the Pydantic model as a tool + response = llm.invoke(prompt) + print(f"LLM Raw Response: {response.content}") + + # Parse the raw JSON string output from the LLM + try: + json_content = response.content + # Remove markdown code block fences if present + if json_content.startswith("```json") and json_content.endswith("```"): + json_content = json_content[len("```json") : -len("```")].strip() + + # Using .model_validate_json for Pydantic v2 + parsed_output = InstagramCommentsAnalysisOutput.model_validate_json( + json_content + ) + except Exception as parse_error: + raise ValueError( + f"Failed to parse LLM response as JSON: {parse_error}\nRaw content: {response.content}" + ) + + analysis_result_dict = parsed_output.model_dump() + + print( + f"[InstagramCommentsAnalysisNode] 분석 결과:\n{json.dumps(analysis_result_dict, ensure_ascii=False, indent=2)}" + ) + state["comment_analysis_result"] = analysis_result_dict + + # 분석 결과를 별도 json 파일로 저장 + now_str = datetime.now().isoformat() + analysis_file_path = state.get("comment_analysis_file_path") + + if os.path.exists(analysis_file_path): + try: + with open(analysis_file_path, "r", encoding="utf-8") as f: + analysis_data = json.load(f) + except Exception: + analysis_data = {} + analysis_data[now_str] = analysis_result_dict + else: + # Ensure the directory exists before writing the file + os.makedirs(os.path.dirname(analysis_file_path), exist_ok=True) + analysis_data = {now_str: analysis_result_dict} + + with open(analysis_file_path, "w", encoding="utf-8") as f: + json.dump( + analysis_data, + f, + ensure_ascii=False, + indent=2, + separators=(",", ": "), + ) + print( + f"[InstagramCommentsAnalysisNode] 분석 결과를 {analysis_file_path}에 저장했습니다." + ) + return { + "comment_analysis_result": analysis_result_dict, + "response": "댓글 분석이 완료되었습니다.", + } + except Exception as e: + print(f"[InstagramCommentsAnalysisNode] Gemini 호출 오류: {str(e)}") + return { + "response": f"Gemini 호출 오류: {str(e)}", + "comment_analysis_result": None, + } + + +class NoChangeNode(BaseNode): + """ + 댓글 수 변경이 없을 때 실행되는 노드 + """ + + def execute(self, state: ManagementState) -> Dict[str, Any]: + """ + 댓글 수 변경이 없을 때의 처리를 수행합니다. + """ + print("🔍 [NoChangeNode] 실행 중...") + + return {"response": "댓글 수에 변경이 없습니다. 모니터링을 계속합니다."} + + +class InstagramAnalysisReportNode(BaseNode): + """ + 인스타그램 댓글 분석 보고서를 생성하는 노드 + """ + + def execute(self, state: ManagementState) -> Dict[str, Any]: + print("🔍 [InstagramAnalysisReportNode] 실행 중...") + report_file_path = state.get("analysis_report_file_path") + + # Load analysis data directly from state + analyzed_data = state.get("comment_analysis_result") + if not analyzed_data: + return { + "response": "분석 데이터가 상태에 존재하지 않습니다.", + "analysis_report": None, + } + print("📁 [InstagramAnalysisReportNode] 분석 데이터를 상태에서 불러왔습니다.") + + # Prompt LLM for report generation + prompt_template = get_instagram_analysis_report_prompt() + prompt = prompt_template.format( + analyzed_data=json.dumps(analyzed_data, ensure_ascii=False, indent=2) + ) + + try: + api_key = state.get("api_key", "") + if not api_key: + return { + "response": "GOOGLE_API_KEY 환경변수가 설정되어 있지 않습니다.", + "analysis_report": None, + } + + llm = ChatGoogleGenerativeAI( + model="gemini-2.5-flash", google_api_key=api_key + ) + response = llm.invoke(prompt) + print(f"LLM Raw Report Response: {response.content}") + + # Parse the raw JSON string output from the LLM + try: + json_content = response.content + if json_content.startswith("```json") and json_content.endswith("```"): + json_content = json_content[len("```json") : -len("```")].strip() + + parsed_report = InstagramAnalysisReportOutput.model_validate_json( + json_content + ) + except Exception as parse_error: + raise ValueError( + f"Failed to parse LLM report as JSON: {parse_error}\nRaw content: {response.content}" + ) + + report_dict = parsed_report.model_dump() + report_dict["media_id"] = state.get("user_id", "") + + # Save the report to a new JSON file + os.makedirs(os.path.dirname(report_file_path), exist_ok=True) + with open(report_file_path, "w", encoding="utf-8") as f: + json.dump( + report_dict, f, ensure_ascii=False, indent=2, separators=(",", ": ") + ) + print( + f"[InstagramAnalysisReportNode] 분석 보고서를 {report_file_path}에 저장했습니다." + ) + + return { + "analysis_report": report_dict, + "response": "분석 보고서 생성이 완료되었습니다.", + } + except Exception as e: + print( + f"[InstagramAnalysisReportNode] Gemini 호출 또는 보고서 생성 오류: {str(e)}" + ) + return { + "response": f"Gemini 호출 또는 보고서 생성 오류: {str(e)}", + "analysis_report": None, + } diff --git a/casts/cast_instagram_comment/modules/prompts.py b/casts/cast_instagram_comment/modules/prompts.py new file mode 100644 index 0000000..9734ac8 --- /dev/null +++ b/casts/cast_instagram_comment/modules/prompts.py @@ -0,0 +1,206 @@ +"""프롬프트 템플릿을 생성하는 함수 모듈 + +프롬프트 템플릿을 생성하는 함수 모듈을 구성합니다. +기본적으로 PromptTemplate를 사용하여 프롬프트 템플릿을 생성하고 반환합니다. + +아래는 예시입니다. +""" + +from langchain_core.prompts import PromptTemplate + + +def get_resource_planning_prompt(): + """ + 리소스 계획 수립을 위한 프롬프트 템플릿을 생성합니다. + + 이 프롬프트는 다음 데이터를 입력으로 사용합니다: + 1. 프로젝트 ID: 관리할 프로젝트의 고유 ID + 2. 요청 유형: 리소스 할당, 팀 관리, 크리에이터 개발 등의 요청 유형 + 3. 사용자 쿼리: 구체적인 요청사항 + 4. 팀 구성원: 프로젝트에 참여하는 팀 구성원 목록 + 5. 사용 가능한 리소스: 현재 사용 가능한 리소스 정보 + + 프롬프트는 LLM에게 주어진 정보를 기반으로 프로젝트 관리에 적합한 리소스 계획을 + 수립하도록 지시합니다. 결과는 한국어로 반환됩니다. + + Returns: + PromptTemplate: 리소스 계획 수립을 위한 프롬프트 템플릿 객체 + """ + # 리소스 계획을 위한 프롬프트 템플릿 정의 + resource_planning_template = """You are an expert entertainment project manager tasked with creating resource plans for entertainment projects. You are provided with the following information: + +1. Project ID: {project_id} + +2. Request Type: {request_type} + +3. User Query: {query} + +4. Team Members: {team_members} + +5. Available Resources: {resources_available} + +Your Task: +Based on the information provided, develop a comprehensive resource management plan that addresses the user query. Your plan should include: + +1. PROJECT OVERVIEW: +- Brief summary of the project based on the available information +- Clear objectives and expected outcomes + +2. RESOURCE ALLOCATION: +- Human resources: Team composition, roles, and responsibilities +- Technical resources: Equipment, software, and facilities needed +- Financial resources: Budget considerations and allocations +- Time resources: Schedule, timeline, and milestones + +3. RESOURCE OPTIMIZATION: +- Efficiency recommendations +- Risk assessment and mitigation strategies +- Contingency planning + +4. IMPLEMENTATION PLAN: +- Step-by-step guide for executing the resource plan +- Monitoring and evaluation mechanisms +- Communication protocols + +5. RECOMMENDATIONS: +- Additional resources that might be beneficial +- Training or development opportunities +- Process improvement suggestions + +Make your plan specific to the entertainment industry context and the particular request type. Be detailed yet concise, and ensure your recommendations are practical and actionable. + +All responses must be in Korean. + +Resource Management Plan:""" + + # PromptTemplate 객체 생성 및 반환 + return PromptTemplate( + template=resource_planning_template, # 정의된 프롬프트 템플릿 + input_variables=[ + "project_id", + "request_type", + "query", + "team_members", + "resources_available", + ], # 프롬프트에 삽입될 변수들 + ) + + +def get_instagram_comment_analysis_prompt(): + """ + Returns a prompt template for analyzing Instagram comments for a singer-songwriter artist management. + The prompt analyzes each comment for: + 1. Sentiment analysis (Positive/Negative) + 2. Comment type identification (advertisement, hate comment, fan comment, etc.) + 3. Whether a reply is needed + Input: comments (list of comment texts) + Output: Structured analysis for effective community management. + """ + comment_analysis_template = """You are an Instagram management agent for a singer-songwriter artist. Analyze each comment from the Instagram post and provide detailed insights for effective community management. + +For each comment, analyze: +1. SENTIMENT: Determine if the comment is Positive (P) or Negative (N) +2. COMMENT TYPE: Identify the type of comment: + - Fan comment: Supportive, encouraging, or appreciative + - Advertisement/Spam: Promotional content, unrelated links, or spam + - Hate comment: Malicious, offensive, or harmful content + - Question: Asking about music, schedule, or personal matters + - Criticism: Constructive or destructive feedback + - Other: Any other type +3. REPLY NEEDED: Determine if the artist or management should reply: + - "Yes": For questions, constructive criticism, or important fan comments + - "No": For spam, hate comments, or simple appreciation comments + - "Consider": For borderline cases that might need attention + +Comment List: +{comments} + +Provide your analysis as a JSON string that strictly adheres to the following structure. The JSON should contain a single key `comments`, which is a list of analyzed comment dictionaries. Each comment dictionary must include the keys: `comment`, `sentiment`, `comment_type`, `reply_needed`, and `reason`. All values for `sentiment`, `comment_type`, `reply_needed`, and `reason` must be in Korean. + +Example JSON format: +```json +{{ + "comments": [ + {{ + "comment": "Original comment text", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "예", + "reason": "Reason for assessment" + }} + ] +}} +``` +""" + + return PromptTemplate( + template=comment_analysis_template, + input_variables=["comments"], + ) + + +def get_instagram_analysis_report_prompt(): + """ + Returns a prompt template for generating an Instagram comments analysis report. + The prompt takes analyzed comment data as input and generates a structured report. + """ + report_template = """You are an expert Instagram analyst. Based on the provided Instagram comments analysis data, generate a comprehensive analysis report. + +The data is a JSON object where keys are timestamps and values are dictionaries containing a 'comments' list. Each comment in the 'comments' list has 'comment', 'sentiment', 'comment_type', 'reply_needed', and 'reason' fields. + +Analyzed Comments Data: +{analyzed_data} + +Your report must be a JSON string that strictly adheres to the `InstagramAnalysisReportOutput` schema. The JSON should contain the following keys: +- `report_date`: Current date in YYYY-MM-DD format. +- `total_comments_analyzed`: Total number of comments processed in the provided data. +- `summary`: A comprehensive summary of the overall sentiment and key themes. +- `key_insights`: A list of 3-5 key insights, trends, or recurring themes from the comments. +- `sentiment_distribution`: An object showing the count for each sentiment (긍정, 부정, 중립). +- `comment_type_distribution`: An object showing the count for each comment type (팬댓글, 광고/스팸, 악성댓글, 질문, 비판, 기타). +- `reply_needed_breakdown`: An object showing the count for each reply_needed status (예, 아니오, 고려). +- `action_items`: A list of 3-5 actionable recommendations for community management based on your analysis. + +Ensure all text values are in Korean. The output must be a single JSON object. + +Example JSON format: +```json +{{ + "report_date": "2024-07-27-03-10", + "total_comments_analyzed": 100, + "summary": "전반적으로 긍정적인 팬덤 반응을 보이며, 일부 질문과 개선점에 대한 의견이 있었습니다.", + "key_insights": [ + "아티스트에 대한 강한 지지와 애정 표현이 많음", + "신곡 및 활동 계획에 대한 팬들의 궁금증 증가", + "일부 비판적 의견은 건설적인 방향으로 제시됨" + ], + "sentiment_distribution": {{ + "긍정": 80, + "부정": 10, + "중립": 10 + }}, + "comment_type_distribution": {{ + "팬댓글": 70, + "질문": 15, + "비판": 5, + "광고/스팸": 5, + "악성댓글": 2, + "기타": 3 + }}, + "reply_needed_breakdown": {{ + "예": 20, + "아니오": 70, + "고려": 10 + }}, + "action_items": [ + "팬들의 질문에 대한 FAQ 문서 업데이트", + "긍정적인 팬 댓글에 주기적으로 감사 댓글 남기기", + "비판적 의견에 대한 내부 검토 및 개선 방안 마련" + ] +}} +``` +""" + return PromptTemplate( + template=report_template, + input_variables=["analyzed_data"], + ) diff --git a/casts/cast_instagram_comment/modules/state.py b/casts/cast_instagram_comment/modules/state.py new file mode 100644 index 0000000..dc7c888 --- /dev/null +++ b/casts/cast_instagram_comment/modules/state.py @@ -0,0 +1,37 @@ +""" +인스타그램 API 워크플로우를 위한 상태 정의 +""" + +from __future__ import annotations + +from typing import Annotated, TypedDict, List, Dict, Optional, Any + +from langgraph.graph.message import add_messages + + +class ManagementState(TypedDict): + """ + 인스타그램 API 워크플로우의 상태를 정의하는 TypedDict 클래스 + + 인스타그램 포스트 정보와 댓글 모니터링을 위한 Workflow에서 사용되는 상태 정보를 정의합니다. + LangGraph의 상태 관리를 위한 클래스로, Workflow 내에서 처리되는 데이터의 형태와 구조를 지정합니다. + """ + + access_token: str # 인스타그램 액세스 토큰 + api_key: str # google api key + user_id: str # 인스타그램 사용자 ID + comment_file_path: str # JSON 파일 경로 + comment_analysis_file_path: str + media_data: Optional[List[Dict[str, Any]]] # 미디어 데이터 목록 + first_media_id: Optional[str] # 첫 번째 미디어 ID + current_comments_count: Optional[int] # 현재 댓글 수 + previous_comments_count: Optional[int] # 이전 댓글 수 + comments_data: Optional[List[Dict[str, Any]]] # 댓글 데이터 + has_changes: Optional[bool] # 댓글 수 변경 여부 + json_data: Optional[Dict[str, Any]] # JSON 파일에 저장할 데이터 + response: Annotated[ + List[Any], add_messages + ] # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공) + comment_analysis_result: Optional[Any] # 댓글 분석 결과 + analysis_report_file_path: Optional[str] # 분석 보고서 파일 경로 + analysis_report: Optional[Any] # 분석 보고서 결과 diff --git a/agents/management/pyproject.toml b/casts/cast_instagram_comment/pyproject.toml similarity index 82% rename from agents/management/pyproject.toml rename to casts/cast_instagram_comment/pyproject.toml index 37ca5b9..3ba1196 100644 --- a/agents/management/pyproject.toml +++ b/casts/cast_instagram_comment/pyproject.toml @@ -4,9 +4,12 @@ # 이 패키지에 종속성을 추가하시려면 `uv add --project agents/management <>`를 사용합니다. [project] -name = "management" +name = "cast_instagram_comment" version = "0.1.0" description = "엔터테인먼트 컨텐츠 관리를 위한 LangGraph Workflow 모듈" readme = "README.md" requires-python = ">=3.13" -dependencies = [] +dependencies = [ + "langchain-google-genai>=2.1.8", + "pandas>=2.3.1", +] diff --git a/casts/cast_instagram_comment/workflow.py b/casts/cast_instagram_comment/workflow.py new file mode 100644 index 0000000..2c141f5 --- /dev/null +++ b/casts/cast_instagram_comment/workflow.py @@ -0,0 +1,199 @@ +from langgraph.graph import StateGraph + +from agents.base_workflow import BaseWorkflow +from agents.management.modules.nodes import ( + InstagramMediaFetchNode, + InstagramCommentsFetchNode, + NoChangeNode, + InstagramCommentsAnalysisNode, + InstagramAnalysisReportNode, +) +from agents.management.modules.state import ManagementState +from langchain.utils.env import get_from_env + + +class ManagementWorkflow(BaseWorkflow): + """ + 인스타그램 API 모니터링을 위한 Workflow 클래스 + + 이 클래스는 인스타그램 포스트의 댓글 수 변화를 모니터링하는 Workflow를 정의합니다. + BaseWorkflow를 상속받아 기본 구조를 구현하고, ManagementState를 사용하여 상태를 관리합니다. + """ + + def __init__(self, state): + super().__init__() + self.state = state + + def build(self): + """ + 인스타그램 모니터링 Workflow 그래프 구축 메서드 + + StateGraph를 사용하여 인스타그램 API 모니터링을 위한 Workflow 그래프를 구축합니다. + 조건부 에지를 사용하여 댓글 수 변경 여부에 따라 다른 경로를 실행합니다. + + Returns: + CompiledStateGraph: 컴파일된 상태 그래프 객체 + """ + builder = StateGraph(self.state) + + # 노드 추가 + builder.add_node("media_fetch", InstagramMediaFetchNode()) + builder.add_node("comments_fetch", InstagramCommentsFetchNode()) + builder.add_node("analysis", InstagramCommentsAnalysisNode()) + builder.add_node("no_change", NoChangeNode()) + builder.add_node("generate_report", InstagramAnalysisReportNode()) + + # 시작 노드에서 미디어 가져오기 노드로 연결 + builder.add_edge("__start__", "media_fetch") + + # 조건부 에지: 댓글 수 변경 여부에 따라 분기 + builder.add_conditional_edges( + "media_fetch", + self._should_fetch_comments, + { + "fetch_comments": "comments_fetch", + "no_change": "no_change", + "api_error": "__end__", # API 오류 시 종료 + }, + ) + + # 댓글 가져오기 노드에서 분석 노드로, 분석 노드에서 종료 + builder.add_edge("comments_fetch", "analysis") + builder.add_edge( + "analysis", "generate_report" + ) # Connect analysis to report node + builder.add_edge("generate_report", "__end__") # Connect report node to end + + workflow = builder.compile() # 그래프 컴파일 + workflow.name = self.name # Workflow 이름 설정 + + return workflow + + def _should_fetch_comments(self, state: ManagementState) -> str: + """ + 댓글을 가져올지 결정하는 라우터 함수 + + Args: + state: 현재 상태 + + Returns: + str: 다음 노드 이름 ("fetch_comments", "no_change", 또는 "api_error") + """ + print("🔄 [Router] 다음 노드 결정 중...") + + # API 오류 체크 + if "API 요청에 실패했습니다" in str(state.get("response", [])): + print("🔄 [Router] API 오류 감지 → api_error 경로 선택") + return "api_error" + + # For testing purposes, force fetch_comments + # return "fetch_comments" + + has_changes = state.get("has_changes", False) + + if has_changes: + print("🔄 [Router] 댓글 수 변경 감지 → fetch_comments 경로 선택") + return "fetch_comments" + else: + print("🔄 [Router] 댓글 수 변경 없음 → no_change 경로 선택") + return "no_change" + + +# def test_instagram_workflow(): +# USER_ID = "17841451140542851" + +# global run_count +# run_count += 1 +# print(f"\n===== {run_count}번째 실행 =====") +# """ +# 인스타그램 워크플로우를 테스트합니다. +# """ +# # 인스타그램 API 인증 정보 (실제 값으로 교체 필요) +# access_token = ACCESS_TOKEN # 실제 액세스 토큰으로 교체 +# user_id = USER_ID # 실제 사용자 ID로 교체 + +# # 초기 상태 설정 +# initial_state = { +# "access_token": access_token, +# "user_id": user_id, +# "comment_file_path": "./data/instagram_data.json", +# "comment_analysis_file_path": "./data/instagram_data_analysis.json", +# "json_data": None, # JSON 파일에 저장할 데이터 +# "response": [] +# } + +# try: +# # 워크플로우 실행 +# print("인스타그램 워크플로우를 시작합니다...") + +# # 워크플로우 빌드 +# workflow = management_workflow.build() + +# # 워크플로우 실행 +# result = workflow.invoke(initial_state) + +# # 결과 출력 +# print("\n=== 워크플로우 실행 결과 ===") +# for message in result.get("response", []): +# if hasattr(message, 'content'): +# print(f"응답: {message.content}") +# else: +# print(f"응답: {message}") + +# # 상태 정보 출력 +# print(f"\n=== 상태 정보 ===") +# print(f"미디어 데이터 개수: {len(result.get('media_data', []))}") +# print(f"첫 번째 미디어 ID: {result.get('first_media_id')}") +# print(f"현재 댓글 수: {result.get('current_comments_count')}") +# print(f"이전 댓글 수: {result.get('previous_comments_count')}") +# print(f"변경 여부: {result.get('has_changes')}") + +# if result.get('comments_data'): +# print(f"댓글 데이터 개수: {len(result.get('comments_data', []))}") + +# # API 오류 체크 +# response_text = str(result.get("response", [])) +# if "API 요청에 실패했습니다" in response_text or "Invalid OAuth access token" in response_text: +# print(f"\n⚠️ API 오류가 발생했습니다. 토큰을 확인해주세요.") +# print(f" - 토큰이 만료되었을 수 있습니다.") +# print(f" - 토큰이 올바른지 확인해주세요.") +# print(f" - 인스타그램 API 권한을 확인해주세요.") + +# except Exception as e: +# print(f"워크플로우 실행 중 오류 발생: {str(e)}") + + +# def run_scheduler(): +# # 10분마다 워크플로우 실행 +# schedule.every(1).minutes.do(test_instagram_workflow) +# # 또는 매 1시간마다: schedule.every().hour.do(test_instagram_workflow) + +# print("스케줄러가 시작되었습니다. (Ctrl+C로 종료)") +# while True: +# schedule.run_pending() +# time.sleep(1) + +management_workflow = ManagementWorkflow(ManagementState) + +if __name__ == "__main__": + # run_count = 0 + # # run_scheduler() + # test_instagram_workflow() + workflow = management_workflow.build() + + ACCESS_TOKEN = get_from_env("instagram_api_key", "INSTAGRAM_API_KEY") + API_KEY = get_from_env("google_api_key", "GOOGLE_API_KEY") + USER_ID = "17841451140542851" + + initial_state = { + "access_token": ACCESS_TOKEN, + "user_id": USER_ID, + "api_key": API_KEY, + "comment_file_path": "./data/instagram_data.json", + "comment_analysis_file_path": "./data/instagram_data_analysis.json", + "analysis_report_file_path": "./data/analysis_report.json", + "json_data": None, + "response": [], + } + result = workflow.invoke(initial_state) + print(result) diff --git a/agents/main_state.py b/casts/main_state.py similarity index 100% rename from agents/main_state.py rename to casts/main_state.py diff --git a/agents/music/modules/models.py b/casts/music/modules/models.py similarity index 80% rename from agents/music/modules/models.py rename to casts/music/modules/models.py index 24003b2..aa670ac 100644 --- a/agents/music/modules/models.py +++ b/casts/music/modules/models.py @@ -2,12 +2,13 @@ 기본적으로 사용할 모델 인스턴스를 설정하고 생성하고 반환시킵니다. """ + from langchain_openai import ChatOpenAI from google import genai from googleapiclient.discovery import build from dotenv import load_dotenv -import os +import os load_dotenv() @@ -16,6 +17,7 @@ YOUTUBE_API_KEY = os.getenv("YOUTUBE_API_KEY") GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") + def get_openai_model(temperature=0.7, top_p=0.9): """ LangChain에서 사용할 OpenAI 모델을 초기화하여 반환합니다. @@ -26,30 +28,34 @@ def get_openai_model(temperature=0.7, top_p=0.9): ChatOpenAI: 초기화된 OpenAI 모델 인스턴스 """ # OpenAI 모델 초기화 및 반환 - return ChatOpenAI(model="gpt-4o-mini", - temperature=temperature, - top_p=top_p, - ) + return ChatOpenAI( + model="gpt-4o-mini", + temperature=temperature, + top_p=top_p, + ) + def get_youtube_client(): """ Youtube 영상 검색 API에 사용하기 위한 client, 즉 서비스 객체를 제작합니다. - 환경변수에서 YOUTUBE_API_KEY, YOUTUBE_API_SERVICE_NAME, - YOUTUBE_API_VERSION을 가져와 사용하기 때문에, .env 파일에 유효한 + 환경변수에서 YOUTUBE_API_KEY, YOUTUBE_API_SERVICE_NAME, + YOUTUBE_API_VERSION을 가져와 사용하기 때문에, .env 파일에 유효한 API 키가 설정되어 있어야 합니다. Returns: build: 초기화된 youtube client 인스턴스 """ - return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, - developerKey = YOUTUBE_API_KEY) + return build( + YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION, developerKey=YOUTUBE_API_KEY + ) + def get_gemini_client(): """ LangChain에서 사용할 Gemini client을 초기화하여 반환합니다. - 환경변수에서 GEMINI_API_KEY를 가져와 사용하기 때문에, + 환경변수에서 GEMINI_API_KEY를 가져와 사용하기 때문에, .env 파일에 유효한 API 키가 설정되어 있어야 합니다. 노드에서 Gemini 모델을 사용할 때 모델명을 따로 정의해줘야 합니다. @@ -57,6 +63,5 @@ def get_gemini_client(): Returns: genai.Client: 초기화된 Gemini client 인스턴스 """ - #Gemini 모델 초기화 및 반환 - return genai.Client(api_key = GEMINI_API_KEY) - + # Gemini 모델 초기화 및 반환 + return genai.Client(api_key=GEMINI_API_KEY) diff --git a/agents/music/modules/nodes.py b/casts/music/modules/nodes.py similarity index 72% rename from agents/music/modules/nodes.py rename to casts/music/modules/nodes.py index cd88235..d2fe273 100644 --- a/agents/music/modules/nodes.py +++ b/casts/music/modules/nodes.py @@ -5,9 +5,18 @@ """ from agents.base_node import BaseNode -from agents.music.modules.prompts import get_lyric_template, get_diary_template, get_query_extraction_template, get_youtube_analysis_template +from agents.music.modules.prompts import ( + get_lyric_template, + get_diary_template, + get_query_extraction_template, + get_youtube_analysis_template, +) from agents.music.modules.state import MusicState -from agents.music.modules.models import get_openai_model, get_gemini_client, get_youtube_client +from agents.music.modules.models import ( + get_openai_model, + get_gemini_client, + get_youtube_client, +) from agents.music.modules.tools.weather import WeatherService from google import genai @@ -16,7 +25,7 @@ class DiaryGenerationNode(BaseNode): """ 일기 생성 노드 """ - + def __init__(self, **kwargs): super().__init__(**kwargs) self.model = get_openai_model() @@ -29,26 +38,28 @@ def execute(self, state) -> dict: first_prompt = get_diary_template().format(query=state["diary_query"]) diary_topic_response = self.model.invoke(first_prompt) diary_topic_response = diary_topic_response.text() - - second_prompt = get_query_extraction_template().format(query=state["diary_query"]) + + second_prompt = get_query_extraction_template().format( + query=state["diary_query"] + ) messages = [ { - "role" : "user", - "content" : first_prompt, + "role": "user", + "content": first_prompt, }, { - "role" : "assistant", - "content" : diary_topic_response, + "role": "assistant", + "content": diary_topic_response, }, { - "role" : "user", - "content" : second_prompt, - } + "role": "user", + "content": second_prompt, + }, ] query_response = self.model.invoke(messages) query_response = query_response.text() - return {"youtube_query" : query_response} + return {"youtube_query": query_response} def __call__(self, state): """ @@ -61,33 +72,38 @@ class YoutubeSearchNode(BaseNode): """ 유튜브 영상을 찾는 노드 """ + def __init__(self, **kwargs): super().__init__(**kwargs) self.model = get_youtube_client() - def execute(self, state : MusicState) -> dict: + def execute(self, state: MusicState) -> dict: """ 유튜브 영상 검색 노드 실행 """ - self.logging("execute", input_state = state) + self.logging("execute", input_state=state) prompt = state["youtube_query"] max_results = 1 - search_response = self.model.search().list( - q = prompt, - part = "id", #영상 식별만 하면 돼서 id로 검색 - type = "video", # 영상만 검색 - order = "relevance", # 관련성 기준으로 정렬 - maxResults = max_results - ).execute() + search_response = ( + self.model.search() + .list( + q=prompt, + part="id", # 영상 식별만 하면 돼서 id로 검색 + type="video", # 영상만 검색 + order="relevance", # 관련성 기준으로 정렬 + maxResults=max_results, + ) + .execute() + ) videos = [] # 검색 결과 파싱 - for item in search_response.get('items', []): - video_id = str(item['id']['videoId']) + for item in search_response.get("items", []): + video_id = str(item["id"]["videoId"]) videos.append("https://www.youtube.com/watch?v=" + video_id) - - return {"video_url" : videos[0]} + + return {"video_url": videos[0]} def __call__(self, state): """ @@ -105,32 +121,32 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.model = get_gemini_client() - def execute(self, state : MusicState) -> dict: + def execute(self, state: MusicState) -> dict: """ 유튜브 영상 분석 노드 실행 """ - self.logging("execute", input_state = state) + self.logging("execute", input_state=state) prompt = get_youtube_analysis_template().format(query=state["diary_query"]) video_link = state["video_url"] response = self.model.models.generate_content( - model='models/gemini-2.0-flash', - contents = genai.types.Content( - parts = [ - genai.types.Part( - file_data = genai.types.FileData(file_uri = video_link) + model="models/gemini-2.0-flash", + contents=genai.types.Content( + parts=[ + genai.types.Part( + file_data=genai.types.FileData(file_uri=video_link) ), - genai.types.Part(text = prompt) + genai.types.Part(text=prompt), ] - ) + ), ) - return {"video_analysis" : response.text} + return {"video_analysis": response.text} def __call__(self, state): """ 노드를 함수처럼 호출 가능하게 만드는 메서드 """ return self.execute(state) - + class LyricGenerationNode(BaseNode): """ @@ -140,17 +156,17 @@ class LyricGenerationNode(BaseNode): def __init__(self, **kwargs): super().__init__(**kwargs) self.model = get_openai_model() - + def execute(self, state: MusicState) -> dict: """ 가사 생성 노드 실행 """ self.logging("execute", input_state=state) prompt = get_lyric_template().format( - # query=state["lyric_query"], - query=state["youtube_query"], - weather_info=state["weather_info"], - video_analysis=state["video_analysis"] + # query=state["lyric_query"], + query=state["youtube_query"], + weather_info=state["weather_info"], + video_analysis=state["video_analysis"], ) response = self.model.invoke(prompt) # return {"response": response, "query": state["lyric_query"]} @@ -161,7 +177,7 @@ def __call__(self, state: MusicState) -> dict: 노드를 함수처럼 호출 가능하게 만드는 메서드 """ return self.execute(state) - + class WeatherGenerationNode(BaseNode): """ @@ -177,9 +193,9 @@ def execute(self, state: MusicState) -> dict: 날씨 정보 생성 노드 실행 """ self.logging("execute", input_state=state) - + # 날씨 정보 가져오기 - weather_info = self.weather_service.get_current_weather() + weather_info = self.weather_service.get_current_weather() return {"weather_info": weather_info} def __call__(self, state: MusicState) -> dict: diff --git a/agents/music/modules/prompts.py b/casts/music/modules/prompts.py similarity index 93% rename from agents/music/modules/prompts.py rename to casts/music/modules/prompts.py index 48a0bb6..4359e94 100644 --- a/agents/music/modules/prompts.py +++ b/casts/music/modules/prompts.py @@ -3,6 +3,7 @@ 프롬프트 템플릿을 생성하는 함수 모듈을 구성합니다. 기본적으로 PromptTemplate을 사용하여 프롬프트 템플릿을 생성하고 반환합니다. """ + from langchain_core.prompts import PromptTemplate base_template = """ @@ -23,7 +24,9 @@ - 예술적 감각과 창의성을 중요시함 """ -lyric_template = base_template + """ +lyric_template = ( + base_template + + """ # 음악 스타일 음악 장르: - 주요 장르: 앰비언트 포크, RnB, 드림팝, 베드룸 팝 +@ @@ -40,17 +43,20 @@ 이제 다음 주제로 가사를 써줘: {query} """ +) - -diary_template = base_template + """" +diary_template = ( + base_template + + """" 대화 시 반영: - 위의 모든 정보를 토대로, 니제의 정체성과 감성을 반영하여 대답해. - 음악, 패션, 예술, 감성 표현 및 창의적 사고를 자연스럽게 드러내. 위의 지시사항을 토대로 너가 실제로 겪었을 만한 하루에 대해 일기를 작성해. 이때 일기는 사건만을 나열하기보다 사건과 이에 대해 너가 느낀 감정도 자세히 묘사하도록 해. 생성된 전체 일기 내용에 기반해서, 네 음악 스타일과 어울리는 노래 주제를 생성해. -""" # 추후 수정 가능 +""" +) # 추후 수정 가능 query_extraction_template = """ @@ -71,16 +77,12 @@ """ - - youtube_analysis_template = """ This is youtube link url. Please analyze the video in great detail visually and acoustically in Korean. """ - - def get_lyric_template() -> PromptTemplate: """가사 템플릿 반환""" return PromptTemplate( @@ -88,26 +90,30 @@ def get_lyric_template() -> PromptTemplate: input_variables=["weather_info", "video_analysis", "query"], ) + def get_diary_template() -> PromptTemplate: """일기 템플릿 반환""" return PromptTemplate( - template = diary_template, + template=diary_template, ) + def get_query_extraction_template() -> PromptTemplate: """유튜브 검색어 추출 템플리 반환""" return PromptTemplate( - template = query_extraction_template, + template=query_extraction_template, ) + def get_youtube_query_template() -> PromptTemplate: """유튜브 영상 검색어 반환""" return PromptTemplate( - template = youtube_query_template, + template=youtube_query_template, ) + def get_youtube_analysis_template() -> PromptTemplate: """유튜브 동영상 분석 내용 반환""" return PromptTemplate( - template = youtube_analysis_template, - ) \ No newline at end of file + template=youtube_analysis_template, + ) diff --git a/agents/music/modules/state.py b/casts/music/modules/state.py similarity index 91% rename from agents/music/modules/state.py rename to casts/music/modules/state.py index 79fec91..06f154f 100644 --- a/agents/music/modules/state.py +++ b/casts/music/modules/state.py @@ -26,8 +26,8 @@ class MusicState(TypedDict): response: Annotated[ list, add_messages ] # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공) - weather_info: str # 날씨 정보 제공 + weather_info: str # 날씨 정보 제공 youtube_query: str video_url: str video_analysis: str - aggregate: Annotated[list, add_messages] \ No newline at end of file + aggregate: Annotated[list, add_messages] diff --git a/agents/music/modules/tools/weather.py b/casts/music/modules/tools/weather.py similarity index 58% rename from agents/music/modules/tools/weather.py rename to casts/music/modules/tools/weather.py index bd61432..5caf7f6 100644 --- a/agents/music/modules/tools/weather.py +++ b/casts/music/modules/tools/weather.py @@ -1,7 +1,7 @@ """ 날씨 정보를 가져오는 모듈 -기상청 API를 사용하여 현재 날씨 정보를 가져오고, +기상청 API를 사용하여 현재 날씨 정보를 가져오고, 음악 생성에 활용할 수 있는 형태로 변환합니다. """ @@ -10,7 +10,7 @@ import requests from typing import Dict, Any from dotenv import load_dotenv -import os +import os load_dotenv() @@ -19,40 +19,60 @@ class WeatherService: """ 기상청 API를 사용하여 날씨 정보를 가져오는 서비스 클래스 """ - + def __init__(self): self.service_key = os.getenv("WEATHER_API_KEY") - self.base_url = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst" - self.numOfRows = '1000' - self.pageNO = '1' - self.dataType = 'JSON' + self.base_url = ( + "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtFcst" + ) + self.numOfRows = "1000" + self.pageNO = "1" + self.dataType = "JSON" # 방위 코드 정의 self.deg_code = { - 0: 'N', 360: 'N', 180: 'S', 270: 'W', 90: 'E', 22.5: 'NNE', - 45: 'NE', 67.5: 'ENE', 112.5: 'ESE', 135: 'SE', 157.5: 'SSE', - 202.5: 'SSW', 225: 'SW', 247.5: 'WSW', 292.5: 'WNW', 315: 'NW', - 337.5: 'NNW' + 0: "N", + 360: "N", + 180: "S", + 270: "W", + 90: "E", + 22.5: "NNE", + 45: "NE", + 67.5: "ENE", + 112.5: "ESE", + 135: "SE", + 157.5: "SSE", + 202.5: "SSW", + 225: "SW", + 247.5: "WSW", + 292.5: "WNW", + 315: "NW", + 337.5: "NNW", } - + # 날씨 코드 정의 self.pty_code = { - 0: '강수 없음', 1: '비', 2: '비/눈', 3: '눈', - 5: '빗방울눈날림', 6: '진눈깨비', 7: '눈날림' + 0: "강수 없음", + 1: "비", + 2: "비/눈", + 3: "눈", + 5: "빗방울눈날림", + 6: "진눈깨비", + 7: "눈날림", } - self.sky_code = {1: '맑음', 3: '구름많음', 4: '흐림'} - + self.sky_code = {1: "맑음", 3: "구름많음", 4: "흐림"} + def deg_to_dir(self, deg: float) -> str: """ 각도를 방위로 변환 - + Args: deg: 각도 (0-360) - + Returns: 방위 문자열 (N, S, E, W, NE, SE, SW, NW 등) """ - close_dir = '' + close_dir = "" min_abs = 360 if deg not in self.deg_code.keys(): for key in self.deg_code.keys(): @@ -62,15 +82,15 @@ def deg_to_dir(self, deg: float) -> str: else: close_dir = self.deg_code[deg] return close_dir - - def get_weather_data(self, nx: str = '60', ny: str = '126') -> Dict[str, Any]: + + def get_weather_data(self, nx: str = "60", ny: str = "126") -> Dict[str, Any]: """ 기상청 API에서 날씨 데이터를 가져옵니다. - + Args: nx: X 좌표 (기본값: 서울 용산구) ny: Y 좌표 (기본값: 서울 용산구) - + Returns: 날씨 데이터 딕셔너리 """ @@ -78,9 +98,9 @@ def get_weather_data(self, nx: str = '60', ny: str = '126') -> Dict[str, Any]: now = datetime.now() one_hour_ago = now - timedelta(hours=1) - base_date = now.strftime('%Y%m%d') - base_time = one_hour_ago.strftime('%H%M') - + base_date = now.strftime("%Y%m%d") + base_time = one_hour_ago.strftime("%H%M") + # URL 생성 url = f"{self.base_url}?serviceKey={self.service_key}&numOfRows={self.numOfRows}&pageNo={self.pageNO}&dataType={self.dataType}&base_date={base_date}&base_time={base_time}&nx={nx}&ny={ny}" @@ -93,113 +113,121 @@ def get_weather_data(self, nx: str = '60', ny: str = '126') -> Dict[str, Any]: except json.JSONDecodeError as e: print(f"JSON 파싱 실패: {e}") return {} - - def parse_weather_data(self, weather_data: Dict[str, Any]) -> Dict[str, Dict[str, Any]]: + + def parse_weather_data( + self, weather_data: Dict[str, Any] + ) -> Dict[str, Dict[str, Any]]: """ 날씨 데이터를 시간대별로 파싱합니다. - + Args: weather_data: API 응답 데이터 - + Returns: 시간대별 날씨 정보 딕셔너리 """ informations = {} - - if 'response' not in weather_data or 'body' not in weather_data['response']: + + if "response" not in weather_data or "body" not in weather_data["response"]: return informations - - items = weather_data['response']['body']['items']['item'] - + + items = weather_data["response"]["body"]["items"]["item"] + for item in items: - cate = item['category'] - fcst_time = item['fcstTime'] - fcst_value = item['fcstValue'] - + cate = item["category"] + fcst_time = item["fcstTime"] + fcst_value = item["fcstValue"] + if fcst_time not in informations: informations[fcst_time] = {} - + informations[fcst_time][cate] = fcst_value - + return informations - - def format_weather_info(self, weather_info: Dict[str, Any], base_date: str, - fcst_time: str, nx: str, ny: str) -> str: + + def format_weather_info( + self, + weather_info: Dict[str, Any], + base_date: str, + fcst_time: str, + nx: str, + ny: str, + ) -> str: """ 날씨 정보를 읽기 쉬운 형태로 포맷팅합니다. - + Args: weather_info: 특정 시간의 날씨 정보 base_date: 기준 날짜 fcst_time: 예보 시간 nx: X 좌표 ny: Y 좌표 - + Returns: 포맷팅된 날씨 정보 문자열 """ template = f"{base_date[:4]}년 {base_date[4:6]}월 {base_date[-2:]}일 {fcst_time[:2]}시 {fcst_time[2:]}분 ({int(nx)}, {int(ny)}) 지역의 날씨는 " - + # 하늘 상태 - if 'SKY' in weather_info: - sky_temp = self.sky_code[int(weather_info['SKY'])] + if "SKY" in weather_info: + sky_temp = self.sky_code[int(weather_info["SKY"])] template += sky_temp + " | " - + # 강수 형태 - if 'PTY' in weather_info: - pty_temp = self.pty_code[int(weather_info['PTY'])] + if "PTY" in weather_info: + pty_temp = self.pty_code[int(weather_info["PTY"])] template += pty_temp + " | " - + # 1시간 강수량 - if 'RN1' in weather_info: - rn1_temp = weather_info['RN1'] + if "RN1" in weather_info: + rn1_temp = weather_info["RN1"] template += f"시간당 {rn1_temp} | " - + # 기온 - if 'T1H' in weather_info: - t1h_temp = float(weather_info['T1H']) + if "T1H" in weather_info: + t1h_temp = float(weather_info["T1H"]) template += f"기온 {t1h_temp}°C | " - + # 습도 - if 'REH' in weather_info: - reh_temp = float(weather_info['REH']) + if "REH" in weather_info: + reh_temp = float(weather_info["REH"]) template += f"습도 {reh_temp}% | " - + # 동서 바람 성분 - if 'UUU' in weather_info: - uuu_temp = float(weather_info['UUU']) + if "UUU" in weather_info: + uuu_temp = float(weather_info["UUU"]) template += f"동서 바람 성분 {uuu_temp}m/s | " - + # 남북 바람 성분 - if 'VVV' in weather_info: - vvv_temp = float(weather_info['VVV']) + if "VVV" in weather_info: + vvv_temp = float(weather_info["VVV"]) template += f"남북 바람 성분 {vvv_temp}m/s | " - + # 풍향 - if 'VEC' in weather_info: - vec_temp = self.deg_to_dir(float(weather_info['VEC'])) + if "VEC" in weather_info: + vec_temp = self.deg_to_dir(float(weather_info["VEC"])) template += f"풍향 {vec_temp} | " - + # 풍속 - if 'WSD' in weather_info: - wsd_temp = weather_info['WSD'] + if "WSD" in weather_info: + wsd_temp = weather_info["WSD"] template += f"풍속 {wsd_temp}m/s | " - + # 낙뢰 - if 'LGT' in weather_info: - lgt_temp = weather_info['LGT'] + if "LGT" in weather_info: + lgt_temp = weather_info["LGT"] template += f"낙뢰 {lgt_temp}kA" - + return template - - def get_current_weather(self, nx: str = '60', ny: str = '126') -> Dict[str, Any]: + + def get_current_weather(self, nx: str = "60", ny: str = "126") -> Dict[str, Any]: """ 현재 시간과 가장 가까운 날씨 정보를 가져옵니다. - + Args: nx: X 좌표 ny: Y 좌표 - + Returns: 현재 날씨 정보 딕셔너리 """ @@ -207,36 +235,41 @@ def get_current_weather(self, nx: str = '60', ny: str = '126') -> Dict[str, Any] weather_data = self.get_weather_data(nx, ny) if not weather_data: return {} - + # 시간대별로 파싱 informations = self.parse_weather_data(weather_data) if not informations: return {} - + # 현재 시간과 가장 가까운 예보 시간 찾기 now = datetime.now() - current_time = now.strftime('%H%M') - base_date = now.strftime('%Y%m%d') - - closest_time = min(informations.keys(), - key=lambda x: abs(int(x) - int(current_time))) - + current_time = now.strftime("%H%M") + base_date = now.strftime("%Y%m%d") + + closest_time = min( + informations.keys(), key=lambda x: abs(int(x) - int(current_time)) + ) + # 해당 시간의 날씨 정보 current_weather = informations[closest_time] - + # 포맷팅된 문자열 생성 formatted_weather = self.format_weather_info( current_weather, base_date, closest_time, nx, ny ) - + return { - 'raw_data': current_weather, - 'formatted_text': formatted_weather, - 'temperature': float(current_weather.get('T1H', 0)), - 'humidity': float(current_weather.get('REH', 0)), - 'sky_condition': self.sky_code.get(int(current_weather.get('SKY', 1)), '맑음'), - 'precipitation': self.pty_code.get(int(current_weather.get('PTY', 0)), '강수 없음'), - 'wind_speed': current_weather.get('WSD', '0'), - 'wind_direction': self.deg_to_dir(float(current_weather.get('VEC', 0))), - 'location': f"({nx}, {ny})" - } \ No newline at end of file + "raw_data": current_weather, + "formatted_text": formatted_weather, + "temperature": float(current_weather.get("T1H", 0)), + "humidity": float(current_weather.get("REH", 0)), + "sky_condition": self.sky_code.get( + int(current_weather.get("SKY", 1)), "맑음" + ), + "precipitation": self.pty_code.get( + int(current_weather.get("PTY", 0)), "강수 없음" + ), + "wind_speed": current_weather.get("WSD", "0"), + "wind_direction": self.deg_to_dir(float(current_weather.get("VEC", 0))), + "location": f"({nx}, {ny})", + } diff --git a/agents/music/workflow.py b/casts/music/workflow.py similarity index 90% rename from agents/music/workflow.py rename to casts/music/workflow.py index 87258e1..f45c29a 100644 --- a/agents/music/workflow.py +++ b/casts/music/workflow.py @@ -9,7 +9,14 @@ from langgraph.graph.state import CompiledStateGraph from agents.base_workflow import BaseWorkflow from agents.music.modules.state import MusicState -from agents.music.modules.nodes import LyricGenerationNode, DiaryGenerationNode, YoutubeSearchNode, YoutubeAnalysisNode, WeatherGenerationNode +from agents.music.modules.nodes import ( + LyricGenerationNode, + DiaryGenerationNode, + YoutubeSearchNode, + YoutubeAnalysisNode, + WeatherGenerationNode, +) + class MusicWorkflow(BaseWorkflow): """ @@ -35,7 +42,6 @@ def build(self) -> CompiledStateGraph: CompiledStateGraph: 컴파일된 상태 그래프 객체 """ builder = StateGraph(self.state) - # 노드 생성 및 연결결 builder.add_node("diary_generation", DiaryGenerationNode()) @@ -57,6 +63,7 @@ def build(self) -> CompiledStateGraph: return workflow + music_workflow = MusicWorkflow(MusicState) if __name__ == "__main__": @@ -66,8 +73,4 @@ def build(self) -> CompiledStateGraph: music_workflow = MusicWorkflow(MusicState) graph = music_workflow.build() - graph.invoke({ - "diary_query": diary_query, - "lyric_query": lyric_query - }) - + graph.invoke({"diary_query": diary_query, "lyric_query": lyric_query}) diff --git a/agents/text/modules/chains.py b/casts/text/modules/chains.py similarity index 100% rename from agents/text/modules/chains.py rename to casts/text/modules/chains.py diff --git a/agents/text/modules/models.py b/casts/text/modules/models.py similarity index 100% rename from agents/text/modules/models.py rename to casts/text/modules/models.py diff --git a/agents/text/modules/nodes.py b/casts/text/modules/nodes.py similarity index 100% rename from agents/text/modules/nodes.py rename to casts/text/modules/nodes.py diff --git a/agents/text/modules/prompts.py b/casts/text/modules/prompts.py similarity index 100% rename from agents/text/modules/prompts.py rename to casts/text/modules/prompts.py diff --git a/agents/text/modules/state.py b/casts/text/modules/state.py similarity index 89% rename from agents/text/modules/state.py rename to casts/text/modules/state.py index 73732bb..46e8511 100644 --- a/agents/text/modules/state.py +++ b/casts/text/modules/state.py @@ -23,4 +23,6 @@ class TextState(TypedDict): response: Annotated[ list, add_messages ] # 응답 메시지 목록 (add_messages로 주석되어 메시지 추가 기능 제공) - text_content_checker_result: (dict) # 텍스트 컨텐츠 검사 결과 전체를 담는 구조화된 필드 + text_content_checker_result: ( + dict # 텍스트 컨텐츠 검사 결과 전체를 담는 구조화된 필드 + ) diff --git a/agents/text/pyproject.toml b/casts/text/pyproject.toml similarity index 100% rename from agents/text/pyproject.toml rename to casts/text/pyproject.toml diff --git a/agents/text/workflow.py b/casts/text/workflow.py similarity index 100% rename from agents/text/workflow.py rename to casts/text/workflow.py diff --git a/agents/workflow.py b/casts/workflow.py similarity index 100% rename from agents/workflow.py rename to casts/workflow.py diff --git a/data/analysis_report.json b/data/analysis_report.json new file mode 100644 index 0000000..6e9e106 --- /dev/null +++ b/data/analysis_report.json @@ -0,0 +1,38 @@ +{ + "report_date": "2024-07-27", + "total_comments_analyzed": 7, + "summary": "분석된 댓글들은 긍정, 부정, 중립적인 다양한 감정을 포함하고 있습니다. 팬들의 직접적인 질문과 아티스트의 발전에 대한 비판적 의견이 있었으며, 일부 악성 댓글과 개인적인 감정 표현도 관찰되었습니다. 전반적으로 팬들과의 소통 기회와 함께 부정적인 댓글에 대한 현명한 대응 전략이 필요한 시점입니다.", + "key_insights": [ + "팬들은 아티스트의 개인적인 면모(예: 요리 실력)에 대한 궁금증을 가지고 있으며, 이는 소통의 좋은 기회입니다.", + "아티스트의 현 상태에 대한 비판적이지만 건설적인 의견이 존재하며, 이는 신중한 답변 고려가 필요합니다.", + "인신공격성 악성 댓글은 무대응이 최선으로 판단됩니다.", + "단순 동의나 의미 없는 테스트성 댓글, 개인 감정 표현 댓글은 개별적인 답변이 불필요합니다.", + "질문 댓글은 팬과의 유대감을 형성하고 소통을 강화할 수 있는 중요한 요소입니다." + ], + "sentiment_distribution": { + "긍정": 2, + "부정": 3, + "중립": 2 + }, + "comment_type_distribution": { + "팬댓글": 1, + "광고/스팸": 0, + "악성댓글": 1, + "질문": 2, + "비판": 1, + "기타": 2 + }, + "reply_needed_breakdown": { + "예": 2, + "아니오": 4, + "고려": 1 + }, + "action_items": [ + "게시물 내용 및 아티스트의 개인적 면모에 대한 질문에 적극적으로 답변하여 팬들과의 소통을 강화하십시오.", + "발전을 지적하는 비판적 댓글에 대해서는 신중하게 검토 후 소통 여부를 결정하여 팬들의 의견을 존중하는 모습을 보여주십시오.", + "악성 댓글에는 일체 대응하지 않고 무시하는 전략을 유지하여 불필요한 논란을 방지하십시오.", + "아티스트의 일상적인 모습이나 취미와 관련된 추가 콘텐츠를 기획하여 팬들과의 친밀도를 높이는 기회를 마련하십시오.", + "의미 없는 댓글이나 단순 동의 댓글에 대한 개별 답변보다는, 전반적인 감사 댓글 등으로 팬 커뮤니티에 대한 관심을 표현할 수 있습니다." + ], + "media_id": "17841451140542851" +} \ No newline at end of file diff --git a/data/comments_test/test_comments.csv b/data/comments_test/test_comments.csv new file mode 100644 index 0000000..8460350 --- /dev/null +++ b/data/comments_test/test_comments.csv @@ -0,0 +1,303 @@ +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,hanr0r0 3 d도망 라이브 클립 봐 주면 안 되낭? +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,😍😍😍 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,기대된당 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,챙겨볼게요~ 로로님🤍🐰 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,당장 달려가 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,더 줘… +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,한지수씨 브이하는척 눈찌르기 하시면 않되요 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,ㅇㅋ접수 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,도망보러 도망쳐~~🏃🏃 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,우리 로로가 해달라는데 당연이봐야지^^말만하렴 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,아 쌉가능이요 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,당연이 되지. +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,우리 로로가 봐달라는데 당연히 봐줄 수 있지! +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,알았당 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,@khshin_ @kyunnxn_ +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,3 days ago +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,hanr0r0 2 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST) +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,"느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자." +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,공주님 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,@yw.c0516 이거디 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,빛빛빛 3rd EP [빛빛빛빛빛빛] +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,온다 🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,드디어 여름선물? 자몽살구클럽 가보자잇❤️ +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,얼굴좀보여조언니 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,뽀 로 로 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,빛이나다 못해 빛이 되어버린… +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,큰거온다🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,Omg 다살자... +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,대 로 로 🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,결혼해요? +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 6 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,27 June +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,귀요미 로로 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,사랑해 누나😍😍😍 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,귀여워 💙💙💙💙💙 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠... +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,@joon_wall 사랑하게되 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,여름로로 너무 조아…😭 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,“인스타용“ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,여름 로로 조아 ☀️🍉☘️ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,"귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로" +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,노래 참 조타 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,한로로의 화해도 좋아요 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,사람이 이렇게 귀여울 수가 있냐고오 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,로로씨 어디강아지에요 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,로로님 6월도 파이팅이에요 😚 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,2 June +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,hanr0r0 6 w@peak_festa 💙 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,청주한씨 화이팅 👏 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,귀욥다 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,언니오늘최고였어욤🔥 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,로로애기 최고였어 !!!!!! 대로로 !!!! 사랑해요 😭😭🫶🏻🫶🏻 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,아 진짜... 오늘 너무 행복했어요...😢 너무 고맙고..뿌듯하고.. +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,우리의 락스타 🔥 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,천사🪽🪽🪽🪽🪽🪽🪽♥️ +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,따랑해 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,누나 사랑해요 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,사랑합니다 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,엥 왜케이쁨 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,레쭈고 레쮸고 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,아니..왜이리 귀여워..ㅠ +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,마이크 드랍마저 귀여운 로로 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,너무 예뻐요❤️❤️😍 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,25 May +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,hanr0r0 9 w자연이 좋아 그래서 자연스러운 내가 좋아 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,안경로로를 사랑해…🪽💕 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,저도좋아요누나 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,자연이 좋다는걸 벌써 알았더니 역시 현명로로🌿 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,사랑해 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,로로쨩을 좋아하는 내가 좋아♡ +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,자연스럽다 라는 말을 다시 한 번 생각해보게 되는 !!! 🍀 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,제일 좋아하는 버스자리 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,안경이 본체 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,저는 그냥 누나 좋아할래요 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,@o._.o920 로로가 좋아 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,나두 초록이 좋아서 초록이 가득한 여름이 좋아~🍃🍃 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,저도 누나가 좋아요 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,@nowizmisselehtreven 자연이 좋다 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,저도언니가좋아요.. +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,7 May +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,hanr0r0 10 w@official_lovesome 💕 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,👏🏻👏🏻💕💕💕💕💕 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,왤케예뻐 ㅁㅊ +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,로로 사랑해!! 💗 로로가 러브썸 찢음 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,3일 연속 공연 고생하셨습니다🔥🔥 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,넘넘아리따우셔라.. +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략) +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다. +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,한로로와결혼할래 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대로로 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대로로 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대—-로—-로( つ '-')╮—̳͟͞ 🍓 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,26 April +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,hanr0r0 11 w2025 spring live clip !! ❄️☀️🌱 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로 얼굴의 점이 되고싶다…………… +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,이런 선물이..🥹 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,로로짱 최고!! +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,입하 입추 입동 안내고 뭐하냐! +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로 누나 노래 들으면서 열공할게염 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,스프링 라이브 클립인데 눈이 내려여 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로가 내 마음에 폭싹 내렸수다 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,"사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니." +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,19 April +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,hanr0r0 12 w🍓🍓 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,기여우어기여워기여워기여워❤️ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,너무 귀여워요 ㅜㅜ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,귀여워 딸기공주 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,@easy_362 이분이 한로로씨구나 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,te kiero micho +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,스트로 로로~ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,사랑해요 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,딸기모찌 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,딸기로로 🍓 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,대로로 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,상의 정보좀요 급함 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,청주 한씨 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,13 April +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,hanr0r0 4 d도망 라이브 클립 봐 주면 안 되낭? +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,😍😍😍 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,기대된당 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,챙겨볼게요~ 로로님🤍🐰 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,당장 달려가 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,더 줘… +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,한지수씨 브이하는척 눈찌르기 하시면 않되요 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,도망보러 도망쳐~~🏃🏃 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,ㅇㅋ접수 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,우리 로로가 해달라는데 당연이봐야지^^말만하렴 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,아 쌉가능이요 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,당연이 되지. +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,우리 로로가 봐달라는데 당연히 봐줄 수 있지! +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,알았당 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,제가 봐드릴게요 +https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/,4 days ago +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,hanr0r0 2 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST) +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,"느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자." +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,@yw.c0516 이거디 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,공주님 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,빛빛빛 3rd EP [빛빛빛빛빛빛] +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,온다 🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,드디어 여름선물? 자몽살구클럽 가보자잇❤️ +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,얼굴좀보여조언니 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,뽀 로 로 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,빛이나다 못해 빛이 되어버린… +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,큰거온다🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,Omg 다살자... +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,대 로 로 🔥🔥 +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,결혼해요? +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,View all 6 replies +https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/,27 June +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,귀요미 로로 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,사랑해 누나😍😍😍 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,귀여워 💙💙💙💙💙 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠... +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,@joon_wall 사랑하게되 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,여름로로 너무 조아…😭 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,“인스타용“ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,여름 로로 조아 ☀️🍉☘️ +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,"귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로" +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,노래 참 조타 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,한로로의 화해도 좋아요 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,사람이 이렇게 귀여울 수가 있냐고오 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,로로씨 어디강아지에요 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,로로님 6월도 파이팅이에요 😚 +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/,2 June +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,언니또보고싶어요...💕💕🧎🏻‍♀️ +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,한로로는 전설이다. +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,결혼해 조요 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,귀욥다 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,혹시..진짜 물어볼곳이 없어서 그런데 한로로님 앨범 재발매는 안될수도있는걸까요..?타팬이라 진짜 아는게 없는데..입덕직전이라..답글받으면 댓 지우겠습니다.. +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,기여워😍 +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,공듀 ㅠㅠ +https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/,25 May +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,기요미ㅗㅠㅠㅠㅠㅠ +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,@se_unn 징쨔 긔엽다....... +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,나도누나가좋아 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,저도 누나가 좋아요 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,콜드플레이 영상보고 입덕했습니다 +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,"팬입니다 ㅎㅎ 출퇴근, 강아지산책, 야근, 청소, 설거지 등 한로로의 음악과 항상✌🏻" +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,로로ㅠ❤️ +https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/,7 May +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,hanr0r0 11 w@official_lovesome 💕 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,👏🏻👏🏻💕💕💕💕💕 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,왤케예뻐 ㅁㅊ +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,로로 사랑해!! 💗 로로가 러브썸 찢음 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,넘넘아리따우셔라.. +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 3 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다. +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략) +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,한로로와결혼할래 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대로로 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대—-로—-로( つ '-')╮—̳͟͞ 🍓 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,3일 연속 공연 고생하셨습니다🔥🔥 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,대로로 +https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/,26 April +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,hanr0r0 11 w@coldplay 🪐🤍🌈 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,당신은 굿이에요 당신은 그레잇해요 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,💓💓🥳 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,로로언니 저시험포기하고언니보러 갓어요 ㅠ넘애쀼 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,로로가 울지도 않고 노래를 잘해요… +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,@bird.ybuddy 헤엑 로로 헤메코 몬일이다냐 개입뻐😮 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,😍😍😍😍😍😍😍😍😍😍😍😍 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,아기락스타 너무 멋쟁이야 너무 기특해 잘햇다👏👏🔥🔥❤️❤️ +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,고생했다이🔥 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,와..... 언니 너무 예뻐요 정말 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,사랑훼❤️ +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,👸🏻so amazing!! Proud of you🥹🤍 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,우주최고밴드와 우주최고아기락스타🤟🏻 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,제발…너무이뻐언니사랑해…ㅠㅠ +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,대로로🔥 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,입춘 너무 좋아요🌸🌸 +https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/,25 April +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,hanr0r0 12 w2025 spring live clip !! ❄️☀️🌱 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로 얼굴의 점이 되고싶다…………… +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,이런 선물이..🥹 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,로로짱 최고!! +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 2 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,입하 입추 입동 안내고 뭐하냐! +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로 누나 노래 들으면서 열공할게염 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,스프링 라이브 클립인데 눈이 내려여 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,한로로가 내 마음에 폭싹 내렸수다 +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,"사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니." +https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/,19 April +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,hanr0r0 12 w🍓🍓 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,기여우어기여워기여워기여워❤️ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,너무 귀여워요 ㅜㅜ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,귀여워 딸기공주 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,@easy_362 이분이 한로로씨구나 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,te kiero micho +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,스트로 로로~ +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,사랑해요 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,딸기모찌 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,딸기로로 🍓 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,대로로 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,상의 정보좀요 급함 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,View all 1 replies +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,청주 한씨 +https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/,13 April diff --git a/data/comments_test/test_comments.json b/data/comments_test/test_comments.json new file mode 100644 index 0000000..ae3a398 --- /dev/null +++ b/data/comments_test/test_comments.json @@ -0,0 +1,264 @@ +{ + "https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/": [ + "hanr0r0 11 w@coldplay 🪐🤍🌈", + "당신은 굿이에요 당신은 그레잇해요", + "💓💓🥳", + "로로언니 저시험포기하고언니보러 갓어요 ㅠ넘애쀼", + "로로가 울지도 않고 노래를 잘해요…", + "@bird.ybuddy 헤엑 로로 헤메코 몬일이다냐 개입뻐😮", + "😍😍😍😍😍😍😍😍😍😍😍😍", + "아기락스타 너무 멋쟁이야 너무 기특해 잘햇다👏👏🔥🔥❤️❤️", + "고생했다이🔥", + "와..... 언니 너무 예뻐요 정말", + "사랑훼❤️", + "👸🏻so amazing!! Proud of you🥹🤍", + "우주최고밴드와 우주최고아기락스타🤟🏻", + "제발…너무이뻐언니사랑해…ㅠㅠ", + "대로로🔥", + "입춘 너무 좋아요🌸🌸", + "25 April" + ], + "https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/": [ + "hanr0r0 10 w@official_lovesome 💕", + "누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ", + "👏🏻👏🏻💕💕💕💕💕", + "왤케예뻐 ㅁㅊ", + "로로 사랑해!! 💗 로로가 러브썸 찢음", + "3일 연속 공연 고생하셨습니다🔥🔥", + "넘넘아리따우셔라..", + "[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략)", + "오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다.", + "한로로와결혼할래", + "대로로", + "대로로", + "대—-로—-로( つ '-')╮—̳͟͞ 🍓", + "26 April", + "hanr0r0 11 w@official_lovesome 💕", + "누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ", + "👏🏻👏🏻💕💕💕💕💕", + "왤케예뻐 ㅁㅊ", + "로로 사랑해!! 💗 로로가 러브썸 찢음", + "넘넘아리따우셔라..", + "오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다.", + "[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략)", + "한로로와결혼할래", + "대로로", + "대—-로—-로( つ '-')╮—̳͟͞ 🍓", + "3일 연속 공연 고생하셨습니다🔥🔥", + "대로로", + "26 April" + ], + "https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/": [ + "hanr0r0 12 w🍓🍓", + "기여우어기여워기여워기여워❤️", + "너무 귀여워요 ㅜㅜ", + "귀여워 딸기공주", + "@easy_362 이분이 한로로씨구나", + "te kiero micho", + "스트로 로로~", + "사랑해요", + "딸기모찌", + "딸기로로 🍓", + "대로로", + "상의 정보좀요 급함", + "청주 한씨", + "13 April", + "hanr0r0 12 w🍓🍓", + "기여우어기여워기여워기여워❤️", + "너무 귀여워요 ㅜㅜ", + "귀여워 딸기공주", + "@easy_362 이분이 한로로씨구나", + "te kiero micho", + "스트로 로로~", + "사랑해요", + "딸기모찌", + "딸기로로 🍓", + "대로로", + "상의 정보좀요 급함", + "청주 한씨", + "13 April" + ], + "https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/": [ + "hanr0r0 11 w2025 spring live clip !! ❄️☀️🌱", + "한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라", + "한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어", + "한로로 얼굴의 점이 되고싶다……………", + "이런 선물이..🥹", + "로로짱 최고!!", + "입하 입추 입동 안내고 뭐하냐!", + "한로로 누나 노래 들으면서 열공할게염", + "스프링 라이브 클립인데 눈이 내려여", + "한로로가 내 마음에 폭싹 내렸수다", + "사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니.", + "19 April", + "hanr0r0 12 w2025 spring live clip !! ❄️☀️🌱", + "한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라", + "한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어", + "한로로 얼굴의 점이 되고싶다……………", + "이런 선물이..🥹", + "로로짱 최고!!", + "입하 입추 입동 안내고 뭐하냐!", + "한로로 누나 노래 들으면서 열공할게염", + "스프링 라이브 클립인데 눈이 내려여", + "한로로가 내 마음에 폭싹 내렸수다", + "사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니.", + "19 April" + ], + "https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/": [ + "hanr0r0 9 w자연이 좋아 그래서 자연스러운 내가 좋아", + "안경로로를 사랑해…🪽💕", + "저도좋아요누나", + "자연이 좋다는걸 벌써 알았더니 역시 현명로로🌿", + "사랑해", + "로로쨩을 좋아하는 내가 좋아♡", + "자연스럽다 라는 말을 다시 한 번 생각해보게 되는 !!! 🍀", + "제일 좋아하는 버스자리", + "안경이 본체", + "저는 그냥 누나 좋아할래요", + "@o._.o920 로로가 좋아", + "나두 초록이 좋아서 초록이 가득한 여름이 좋아~🍃🍃", + "저도 누나가 좋아요", + "@nowizmisselehtreven 자연이 좋다", + "저도언니가좋아요..", + "7 May", + "기요미ㅗㅠㅠㅠㅠㅠ", + "@se_unn 징쨔 긔엽다.......", + "나도누나가좋아", + "저도 누나가 좋아요", + "콜드플레이 영상보고 입덕했습니다", + "팬입니다 ㅎㅎ 출퇴근, 강아지산책, 야근, 청소, 설거지 등 한로로의 음악과 항상✌🏻", + "로로ㅠ❤️", + "7 May" + ], + "https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/": [ + "hanr0r0 6 w@peak_festa 💙", + "청주한씨 화이팅 👏", + "귀욥다", + "언니오늘최고였어욤🔥", + "로로애기 최고였어 !!!!!! 대로로 !!!! 사랑해요 😭😭🫶🏻🫶🏻", + "아 진짜... 오늘 너무 행복했어요...😢 너무 고맙고..뿌듯하고..", + "우리의 락스타 🔥", + "천사🪽🪽🪽🪽🪽🪽🪽♥️", + "따랑해", + "누나 사랑해요", + "사랑합니다", + "엥 왜케이쁨", + "레쭈고 레쮸고", + "아니..왜이리 귀여워..ㅠ", + "마이크 드랍마저 귀여운 로로", + "너무 예뻐요❤️❤️😍", + "25 May", + "언니또보고싶어요...💕💕🧎🏻‍♀️", + "한로로는 전설이다.", + "결혼해 조요", + "귀욥다", + "혹시..진짜 물어볼곳이 없어서 그런데 한로로님 앨범 재발매는 안될수도있는걸까요..?타팬이라 진짜 아는게 없는데..입덕직전이라..답글받으면 댓 지우겠습니다..", + "기여워😍", + "공듀 ㅠㅠ", + "25 May" + ], + "https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/": [ + "귀요미 로로", + "사랑해 누나😍😍😍", + "귀여워 💙💙💙💙💙", + "@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠...", + "@joon_wall 사랑하게되", + "여름로로 너무 조아…😭", + "생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ", + "“인스타용“", + "여름 로로 조아 ☀️🍉☘️", + "귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로", + "노래 참 조타", + "한로로의 화해도 좋아요", + "사람이 이렇게 귀여울 수가 있냐고오", + "로로씨 어디강아지에요", + "로로님 6월도 파이팅이에요 😚", + "2 June", + "귀요미 로로", + "사랑해 누나😍😍😍", + "귀여워 💙💙💙💙💙", + "@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠...", + "@joon_wall 사랑하게되", + "여름로로 너무 조아…😭", + "생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ", + "“인스타용“", + "여름 로로 조아 ☀️🍉☘️", + "귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로", + "노래 참 조타", + "한로로의 화해도 좋아요", + "사람이 이렇게 귀여울 수가 있냐고오", + "로로씨 어디강아지에요", + "로로님 6월도 파이팅이에요 😚", + "2 June" + ], + "https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/": [ + "hanr0r0 3 d도망 라이브 클립 봐 주면 안 되낭?", + "되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지", + "😍😍😍", + "기대된당", + "챙겨볼게요~ 로로님🤍🐰", + "당장 달려가", + "더 줘…", + "한지수씨 브이하는척 눈찌르기 하시면 않되요", + "ㅇㅋ접수", + "도망보러 도망쳐~~🏃🏃", + "우리 로로가 해달라는데 당연이봐야지^^말만하렴", + "아 쌉가능이요", + "당연이 되지.", + "우리 로로가 봐달라는데 당연히 봐줄 수 있지!", + "알았당", + "@khshin_ @kyunnxn_", + "3 days ago", + "hanr0r0 4 d도망 라이브 클립 봐 주면 안 되낭?", + "되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지", + "😍😍😍", + "기대된당", + "챙겨볼게요~ 로로님🤍🐰", + "당장 달려가", + "더 줘…", + "한지수씨 브이하는척 눈찌르기 하시면 않되요", + "도망보러 도망쳐~~🏃🏃", + "ㅇㅋ접수", + "우리 로로가 해달라는데 당연이봐야지^^말만하렴", + "아 쌉가능이요", + "당연이 되지.", + "우리 로로가 봐달라는데 당연히 봐줄 수 있지!", + "알았당", + "제가 봐드릴게요", + "4 days ago" + ], + "https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/": [ + "hanr0r0 2 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST)", + "도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다", + "느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자.", + "공주님", + "@yw.c0516 이거디", + "빛빛빛 3rd EP [빛빛빛빛빛빛]", + "온다 🔥🔥", + "드디어 여름선물? 자몽살구클럽 가보자잇❤️", + "얼굴좀보여조언니", + "뽀 로 로", + "빛이나다 못해 빛이 되어버린…", + "큰거온다🔥🔥", + "Omg 다살자...", + "대 로 로 🔥🔥", + "결혼해요?", + "27 June", + "hanr0r0 2 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST)", + "도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다", + "느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자.", + "@yw.c0516 이거디", + "공주님", + "빛빛빛 3rd EP [빛빛빛빛빛빛]", + "온다 🔥🔥", + "드디어 여름선물? 자몽살구클럽 가보자잇❤️", + "얼굴좀보여조언니", + "뽀 로 로", + "빛이나다 못해 빛이 되어버린…", + "큰거온다🔥🔥", + "Omg 다살자...", + "대 로 로 🔥🔥", + "결혼해요?", + "27 June" + ] +} \ No newline at end of file diff --git a/data/instagram_data.json b/data/instagram_data.json new file mode 100644 index 0000000..b761db8 --- /dev/null +++ b/data/instagram_data.json @@ -0,0 +1,195 @@ +{ + "media_data": [ + { + "id": "18007485335525524", + "caption": "정호영 쉐프의 냉제육\n\n내가 해봤던 요리 중 가장 쉬운 편에 속하지만\n맛 또한 가장 좋은 편에 속한다.\n\n꼭 해먹어 보길 바람처럼 왔다가 이슬처럼 갈 순 없잖아", + "media_type": "IMAGE", + "timestamp": "2024-05-18T17:56:11+0000", + "username": "yimbapchunguk", + "like_count": 13, + "comments_count": 7 + }, + { + "id": "18008998250043407", + "caption": "여유 long time no see\n원팬 파스타 sucks\nI prefer old way", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2023-11-09T07:15:54+0000", + "username": "yimbapchunguk", + "like_count": 11, + "comments_count": 0 + }, + { + "id": "18003786535984507", + "caption": "동기님들 방문해 주셨습니다… 근데 이제 인스피릿을 곁들인", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2023-08-29T17:05:47+0000", + "username": "yimbapchunguk", + "like_count": 20, + "comments_count": 6 + }, + { + "id": "17913183179789753", + "caption": "메타코미디클럽 이선민씨 방문해주셨습니다", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2023-08-05T07:05:01+0000", + "username": "yimbapchunguk", + "like_count": 19, + "comments_count": 1 + }, + { + "id": "17887411151864640", + "caption": "냉우동 맛도리\n커티삭 맛도리", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2023-07-21T03:36:27+0000", + "username": "yimbapchunguk", + "like_count": 17, + "comments_count": 4 + }, + { + "id": "17954969525209194", + "caption": "찬물로 파 씻다가 손구락 얼음", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2023-01-24T13:43:49+0000", + "username": "yimbapchunguk", + "like_count": 22, + "comments_count": 3 + }, + { + "id": "17884996073691257", + "caption": "바쁘다 바뻐 형빈사회\n집에서 밥을 거의 못먹다가 오랜만에 꽤 공들여서 한 저녁\n학회듣고 빨래하고 뭐하고 하다가 10시 반에 겨우 먹음\n후식은 컵누들", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-11-15T16:08:58+0000", + "username": "yimbapchunguk", + "like_count": 18, + "comments_count": 3 + }, + { + "id": "17958508052095266", + "caption": "개지리는 카레 레시피 알아냄\n견과류 빻아서 넣으면 맛있음\n나는 구운 캐슈너트 넣었음\n후첨가 버터는 유통기한 지났음\n카레 넣기 전에 이미 국물 지렸음", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-10-18T04:22:23+0000", + "username": "yimbapchunguk", + "like_count": 17, + "comments_count": 5 + }, + { + "id": "18313440799027884", + "caption": "많이도 해서 먹었습니다", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-08-19T12:06:56+0000", + "username": "yimbapchunguk", + "like_count": 20, + "comments_count": 7 + }, + { + "id": "17910861560562909", + "caption": "간만에 임밥천국이 영업했어요.\n저희 매장 기본찬인 웨지 감자와 봉골레 두접시, 육전 한사바리 요리했습니다.\n+와인 6병 , 어 리를빗 꼬냑\n\n#봉골레 #봉골레파스타 #봉골레맛집 #육전 #육전맛집 #임밥천국", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-07-01T15:47:18+0000", + "username": "yimbapchunguk", + "like_count": 19, + "comments_count": 1 + }, + { + "id": "17936239340067834", + "caption": "쌀이 세컵밖에 안남아서 냄비밥을 했다. 냄비밥은 정말 맛있다. \n간만에 소고기먹었다. 소가 맛있는 이유는 자주 못먹어서 인듯ㅋㅋ \n\n#백종원레시피 #소불고기 #냄비밥 #최강록레시피", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-06-11T16:23:58+0000", + "username": "yimbapchunguk", + "like_count": 24, + "comments_count": 7 + }, + { + "id": "17928318890256046", + "caption": "오랜만에 손님 맞이로 임밥천국을 개시했읍니다. 꽤나 만족스러운 맛이었읍니다. 문의주세요. 단체 환영.\n\n#골뱅이무침 #부추전 #백종원레시피 #아하부장레시피", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-05-08T04:45:29+0000", + "username": "yimbapchunguk", + "like_count": 12, + "comments_count": 4 + }, + { + "id": "17925246794186696", + "caption": "카레는 역시 저 골든 커리가 젤 맛있는듯. 양파 캬라멜라이징 싹 돌리고 소고기 마이야르 촥 굽고 브섯이랑 댕건 스윽 구워서 가니시로 올리면 코코이찌방야 저리 가라야~\n\n#카레 #일본식카레 #골든카레 #goldencurry #임밥천국", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-03-19T12:26:32+0000", + "username": "yimbapchunguk", + "like_count": 10, + "comments_count": 16 + }, + { + "id": "17933571104071046", + "caption": "이번 봉골레 실패했다 면발 무슨 칼국수냐? 애들 앞에서 임밥천국 쪽팔리게 에이씨 빡치네 \n\n#봉골레파스타", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-03-14T11:16:53+0000", + "username": "yimbapchunguk", + "like_count": 10, + "comments_count": 6 + }, + { + "id": "17916218135489138", + "caption": "토맛토맛토 스파게티는 참 간편해~\n\n#토마토파스타", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-03-04T09:41:44+0000", + "username": "yimbapchunguk", + "like_count": 9, + "comments_count": 3 + }, + { + "id": "17930629043024490", + "caption": "백종원 선생님의 콩내물 불게기를 해보았다. 설탕을 넣다 쫌 쏟아서 달달했다. 백선생님 오늘도 감사했습니다… 방구석 제자가 되니 기쁘네요 😎\n\n#백종원레시피 #콩나물불고기 #임밥천국", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-02-22T04:13:32+0000", + "username": "yimbapchunguk", + "like_count": 14, + "comments_count": 0 + }, + { + "id": "17982896386458231", + "caption": "설날 기념 잡채를 만들어보았다. 편스토랑에서 어남선생의 레시피를 카피했다. 편하고 맛이 좋았다. 신선하게 시금치 대신 알배추를 넣었다. 달고 담백하니 맛이 좋았다. 하이볼 세잔 먹음. \n\n#편스토랑레시피 #어남선생레시피 #잡채", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-02-01T13:38:06+0000", + "username": "yimbapchunguk", + "like_count": 8, + "comments_count": 3 + }, + { + "id": "18184518865091131", + "caption": "닭개장… 어렵고 힘들 것 같나? 그렇다. 두시간을 걸려서 만들었다. 이번엔 msg를 안넣고 만들었고 그래도 맛있었다. 나중에 친구들 오면 만들어서 쏘주랑 먹어야지\n\n#닭개장 #아하부장 #아하부장레시피", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-01-27T14:04:40+0000", + "username": "yimbapchunguk", + "like_count": 5, + "comments_count": 4 + }, + { + "id": "17959135507574073", + "caption": "고기 굽기는 아직 서투르다… 코팅 팬을 또 태워버렸어… 설거지는 두렵지만 맛은 아주 맛있었다. 이런 맛이라면 그깟 설거지 백번은 해주마!!!\n\n#돼지고기목살구이 #마이야르 #몬트리올시즈닝", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-01-22T14:09:32+0000", + "username": "yimbapchunguk", + "like_count": 5, + "comments_count": 4 + }, + { + "id": "17916204953221655", + "caption": "소고기 무국\n깊다… 냄비가 3m쯤 되려나…? 착각하게 하는 맛 \n백종원이 없는 다른 멀티버스에서 임형빈 너는 뭘 먹고 지내냐?!!!\n#소고기무국 #백종원 #백종원레시피", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-01-17T13:39:02+0000", + "username": "yimbapchunguk", + "like_count": 6, + "comments_count": 2 + }, + { + "id": "17949593110651400", + "caption": "제육볶음\n#공격수셰프 #공격수셰프레시피 #제육볶음", + "media_type": "CAROUSEL_ALBUM", + "timestamp": "2022-01-17T10:52:03+0000", + "username": "yimbapchunguk", + "like_count": 2, + "comments_count": 2 + } + ], + "first_media_id": "18007485335525524", + "previous_comments_count": 7 +} \ No newline at end of file diff --git a/data/instagram_data_analysis.json b/data/instagram_data_analysis.json new file mode 100644 index 0000000..bbb32e6 --- /dev/null +++ b/data/instagram_data_analysis.json @@ -0,0 +1,267 @@ +{ + "2025-07-27T02:52:27.554443": { + "comments": [ + { + "comment": "Test", + "sentiment": "긍정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "의미 없는 테스트성 댓글로, 특별한 상호작용이 필요하지 않습니다." + }, + { + "comment": "@mirageee_00 맞아요", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "아니오", + "reason": "단순한 동의 표현으로, 개별적인 답글이 필수는 아닙니다." + }, + { + "comment": "@joo_bob_1212 당신의 못된 심보", + "sentiment": "부정", + "comment_type": "악성댓글", + "reply_needed": "아니오", + "reason": "인신공격성 악성댓글이므로, 무시하거나 삭제하는 것이 좋습니다." + }, + { + "comment": "@gangster_joo 아직 제자리 걸음입니다", + "sentiment": "부정", + "comment_type": "비판", + "reply_needed": "고려", + "reason": "진척이 없음을 지적하는 내용으로, 아티스트의 상황에 따라 짧은 긍정적 답변(예: '더 노력하겠습니다' 등)을 고려해볼 수 있습니다." + }, + { + "comment": "왜 이유없이 화가나지 ?", + "sentiment": "부정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "본인의 감정을 표현하는 내용으로, 아티스트가 직접 답글할 필요는 없습니다." + }, + { + "comment": "소금 넣고 끓는 물에 담구는 그거?", + "sentiment": "중립", + "comment_type": "질문", + "reply_needed": "예", + "reason": "게시물 내용과 관련된 명확한 질문으로, 답변을 통해 소통을 강화할 수 있습니다." + }, + { + "comment": "저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "아티스트의 개인적인 경험 및 기술에 대한 친밀한 질문으로, 답글을 통해 팬과의 유대감을 강화할 수 있습니다." + } + ] + }, + "2025-07-27T03:00:52.552544": { + "comments": [ + { + "comment": "Test", + "sentiment": "긍정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "테스트성 댓글로, 특별한 의미나 질문이 없어 답장 불필요합니다." + }, + { + "comment": "@mirageee_00 맞아요", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "아니오", + "reason": "게시물 내용에 대한 단순한 동의 표현으로, 개별적인 답장이 필수는 아닙니다." + }, + { + "comment": "@joo_bob_1212 당신의 못된 심보", + "sentiment": "부정", + "comment_type": "악성댓글", + "reply_needed": "아니오", + "reason": "아티스트에 대한 인신공격성 악성 댓글이므로 대응하지 않는 것이 좋습니다." + }, + { + "comment": "@gangster_joo 아직 제자리 걸음입니다", + "sentiment": "부정", + "comment_type": "비판", + "reply_needed": "고려", + "reason": "아티스트의 활동에 대한 부정적인 평가성 댓글입니다. 직접적인 답변보다는 긍정적인 메시지로 대응할지, 혹은 무시할지 고려해볼 수 있습니다." + }, + { + "comment": "왜 이유없이 화가나지 ?", + "sentiment": "부정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "아티스트에게 직접적으로 관련된 내용이 아닌 개인적인 감정 표현으로, 답장 불필요합니다." + }, + { + "comment": "소금 넣고 끓는 물에 담구는 그거?", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "게시물 내용과 관련된 직접적인 질문이므로 답장하는 것이 좋습니다." + }, + { + "comment": "저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "아티스트와 개인적인 친분이 있는 듯한 팬의 질문으로, 관계를 돈독히 하고 소통을 이어가기 위해 답장하는 것이 좋습니다." + } + ] + }, + "2025-07-27T03:13:38.989365": { + "comments": [ + { + "comment": "Test", + "sentiment": "긍정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "내용이 불분명하며 단순한 테스트성 댓글로 판단됩니다. 특별한 상호작용이 필요하지 않습니다." + }, + { + "comment": "@mirageee_00 맞아요", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "아니오", + "reason": "게시물 내용에 대한 긍정적인 공감을 표현하는 댓글입니다. 간단한 동의 표현으로 별도의 답글은 필수는 아닙니다." + }, + { + "comment": "@joo_bob_1212 당신의 못된 심보", + "sentiment": "부정", + "comment_type": "악성댓글", + "reply_needed": "아니오", + "reason": "악의적인 의도를 담은 비방성 댓글입니다. 악성 댓글에는 대응하지 않는 것이 일반적인 관리 원칙입니다." + }, + { + "comment": "@gangster_joo 아직 제자리 걸음입니다", + "sentiment": "부정", + "comment_type": "비판", + "reply_needed": "고려", + "reason": "아티스트의 상황이나 진행 상황에 대한 비판적인 시각을 내포하고 있습니다. 공격적인 비판은 아니므로, 상황에 따라 아티스트가 직접 소통하며 오해를 풀거나 입장을 설명하는 것을 고려할 수 있습니다." + }, + { + "comment": "왜 이유없이 화가나지 ?", + "sentiment": "부정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "댓글 작성자 본인의 감정을 표출하는 내용으로, 아티스트에게 직접적으로 관련된 내용은 아닙니다. 개입할 필요가 없습니다." + }, + { + "comment": "소금 넣고 끓는 물에 담구는 그거?", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "게시물 내용과 관련된 구체적인 질문입니다. 팬과의 소통을 위해 답변하여 궁금증을 해소해 주는 것이 좋습니다." + }, + { + "comment": "저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "아티스트와의 개인적인 경험을 바탕으로 한 친근하고 구체적인 질문입니다. 팬과의 유대감을 강화하고 소통을 활성화하기 위해 반드시 답변해야 합니다." + } + ] + }, + "2025-07-27T03:15:54.059670": { + "comments": [ + { + "comment": "Test", + "sentiment": "기타", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "의미 없는 테스트 댓글로, 특별한 상호작용이 필요하지 않습니다." + }, + { + "comment": "@mirageee_00 맞아요", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "아니오", + "reason": "게시물 내용에 대한 단순한 동의 표현으로, 별도의 답변이 필수는 아닙니다." + }, + { + "comment": "@joo_bob_1212 당신의 못된 심보", + "sentiment": "부정", + "comment_type": "악성댓글", + "reply_needed": "아니오", + "reason": "악의적인 비난 댓글로, 대응하지 않는 것이 좋습니다." + }, + { + "comment": "@gangster_joo 아직 제자리 걸음입니다", + "sentiment": "기타", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "댓글 작성자의 개인적인 상황에 대한 언급으로 보이며, 아티스트의 답변이 꼭 필요하지는 않습니다." + }, + { + "comment": "왜 이유없이 화가나지 ?", + "sentiment": "기타", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "댓글 작성자의 개인적인 감정 표현으로, 아티스트에게 직접적인 답변을 요구하는 질문이 아닙니다." + }, + { + "comment": "소금 넣고 끓는 물에 담구는 그거?", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "게시물 내용과 관련된 직접적인 질문으로, 답변을 통해 팬의 궁금증을 해소하고 소통을 유도할 수 있습니다." + }, + { + "comment": "저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "아티스트와의 친밀한 관계를 보여주는 개인적인 질문으로, 답변을 통해 팬과의 유대감을 강화하고 긍정적인 상호작용을 이어갈 수 있습니다." + } + ] + }, + "2025-08-10T23:03:33.843554": { + "comments": [ + { + "comment": "Test", + "sentiment": "기타", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "의미 없는 테스트성 댓글로, 개별적인 답변이 필요하지 않습니다." + }, + { + "comment": "@mirageee_00 맞아요", + "sentiment": "긍정", + "comment_type": "팬댓글", + "reply_needed": "아니오", + "reason": "게시물 내용에 대한 단순한 동의 표현으로, 개별적인 답글은 필수가 아닙니다." + }, + { + "comment": "@joo_bob_1212 당신의 못된 심보", + "sentiment": "부정", + "comment_type": "악성댓글", + "reply_needed": "아니오", + "reason": "인신공격성 악성 댓글로, 대응하지 않는 것이 바람직합니다." + }, + { + "comment": "@gangster_joo 아직 제자리 걸음입니다", + "sentiment": "부정", + "comment_type": "비판", + "reply_needed": "고려", + "reason": "발전이 없음을 지적하는 댓글로, 아티스트가 해당 내용에 대해 소통하고자 한다면 답변을 고려할 수 있습니다." + }, + { + "comment": "왜 이유없이 화가나지 ?", + "sentiment": "부정", + "comment_type": "기타", + "reply_needed": "아니오", + "reason": "개인적인 감정을 표현하는 댓글로, 아티스트가 직접 답변할 필요는 없습니다." + }, + { + "comment": "소금 넣고 끓는 물에 담구는 그거?", + "sentiment": "기타", + "comment_type": "질문", + "reply_needed": "예", + "reason": "게시물 내용에 대한 구체적인 질문으로, 답변을 통해 팬과의 소통을 강화할 수 있습니다." + }, + { + "comment": "저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??", + "sentiment": "긍정", + "comment_type": "질문", + "reply_needed": "예", + "reason": "아티스트의 개인적인 면모와 관련된 친근한 질문으로, 팬과의 유대감을 형성하고 소통하는 좋은 기회입니다." + } + ] + } +} \ No newline at end of file diff --git a/initial_state.json b/initial_state.json new file mode 100644 index 0000000..bd0a9e5 --- /dev/null +++ b/initial_state.json @@ -0,0 +1,10 @@ +{ + "access_token": "IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR", + "user_id": "17841451140542851", + "api_key": "AIzaSyCFCjksODoQYn8puDdzuyNr-5EPSlXGvUo", + "comment_file_path": "./data/instagram_data.json", + "comment_analysis_file_path": "./data/instagram_data_analysis.json", + "analysis_report_file_path": "./data/analysis_report.json", + "json_data": {}, + "response": [] + } diff --git a/langgraph.json b/langgraph.json index 4adc9e4..2a97165 100644 --- a/langgraph.json +++ b/langgraph.json @@ -2,9 +2,6 @@ "dependencies": ["."], "graphs": { "main": "./agents/workflow.py:main_workflow", - "text": "./agents/text/workflow.py:text_workflow", - "music": "./agents/music/workflow.py:music_workflow", - "image": "./agents/image/workflow.py:image_workflow", "management": "./agents/management/workflow.py:management_workflow" }, "env": ".env" diff --git a/pyproject.toml b/pyproject.toml index 4ac26a9..bfd8a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,8 +9,17 @@ dependencies = [ "langgraph>=0.3.27", "langchain-community>=0.2.17", "python-dotenv>=1.0.1", + "langchain-google-genai>=2.1.8", + "pandas>=2.3.1", + "schedule>=1.2.2", + "ipykernel>=6.29.5", + "pydantic>=2.0.0", + "ruff>=0.12.9", ] +[tool.setuptools.packages.find] +include = ["agents*"] + [dependency-groups] dev = [ "ipykernel>=6.29.5", @@ -24,7 +33,7 @@ include = ["agents", "tests"] exclude = ["**/__pycache__/*", "**/node_modules/*", ".venv", "**/.venv"] [tool.uv.workspace] -members = ["agents/text", "agents/music", "agents/image", "agents/management"] +members = ["agents/text", "agents/music", "agents/image", "casts/cast_instagram_comment"] [tool.ruff] exclude = [ diff --git a/tests/hyungson_test/test_analysis.json b/tests/hyungson_test/test_analysis.json new file mode 100644 index 0000000..82251e4 --- /dev/null +++ b/tests/hyungson_test/test_analysis.json @@ -0,0 +1,3 @@ +{ + "2025-07-26T22:17:45.436836": "{\n \"comments\": [\n {\n \"comment\": \"hanr0r0 11 w@coldplay 🪐🤍🌈\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"고려\",\n \"reason\": \"팬이 콜드플레이 관련 특정 이벤트를 언급하는 것으로 보이며, 긍정적인 표현입니다. 아티스트가 해당 이벤트에 대해 언급할 기회가 될 수 있습니다.\"\n },\n {\n \"comment\": \"당신은 굿이에요 당신은 그레잇해요\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"단순한 칭찬과 응원의 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"💓💓🥳\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"이모티콘으로 표현된 긍정적인 반응입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"로로언니 저시험포기하고언니보러 갓어요 ㅠ넘애쀼\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"예\",\n \"reason\": \"팬의 높은 헌신과 애정을 보여주는 댓글로, 직접 소통하여 팬심을 강화하고 감사를 표할 필요가 있습니다.\"\n },\n {\n \"comment\": \"로로가 울지도 않고 노래를 잘해요…\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"공연에 대한 구체적인 칭찬으로, 직접적인 답변보다는 '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"@bird.ybuddy 헤엑 로로 헤메코 몬일이다냐 개입뻐😮\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"팬들 간의 대화로, 아티스트의 스타일을 칭찬하는 내용입니다. 직접적인 답변은 필요 없습니다.\"\n },\n {\n \"comment\": \"😍😍😍😍😍😍😍😍😍😍😍😍\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"이모티콘으로 표현된 강한 긍정적 반응입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"아기락스타 너무 멋쟁이야 너무 기특해 잘햇다👏👏🔥🔥❤️❤️\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트에 대한 강한 칭찬과 격려의 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"고생했다이🔥\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트의 노고를 인정하고 격려하는 짧은 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"와..... 언니 너무 예뻐요 정말\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트의 외모에 대한 단순한 칭찬입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"사랑훼❤️\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트에 대한 애정을 표현하는 짧은 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"👸🏻so amazing!! Proud of you🥹🤍\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트에 대한 강한 칭찬과 자부심을 표현하는 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"우주최고밴드와 우주최고아기락스타🤟🏻\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트와 밴드에 대한 과장된 칭찬으로 긍정적인 팬심입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"제발…너무이뻐언니사랑해…ㅠㅠ\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트에 대한 강렬한 애정과 감탄을 표현하는 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"대로로🔥\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"아니오\",\n \"reason\": \"아티스트를 응원하는 짧고 강렬한 메시지입니다. '좋아요'로 충분합니다.\"\n },\n {\n \"comment\": \"입춘 너무 좋아요🌸🌸\",\n \"sentiment\": \"긍정\",\n \"comment_type\": \"팬댓글\",\n \"reply_needed\": \"고려\",\n \"reason\": \"특정 곡에 대한 긍정적인 피드백으로, 팬과의 소통을 위해 감사의 답변을 고려할 수 있습니다.\"\n },\n {\n \"comment\": \"25 April\",\n \"sentiment\": \"중립\",\n \"comment_type\": \"질문\",\n \"reply_needed\": \"예\",\n \"reason\": \"특정 날짜를 언급하는 댓글로, 공연 일정이나 음원 발매일 등 중요한 정보와 관련될 수 있으므로 답변을 통해 의도를 확인하거나 정보를 제공할 필요가 있습니다.\"\n }\n ]\n}" +} \ No newline at end of file diff --git a/tests/hyungson_test/yim.ipynb b/tests/hyungson_test/yim.ipynb new file mode 100644 index 0000000..1a6034b --- /dev/null +++ b/tests/hyungson_test/yim.ipynb @@ -0,0 +1,1001 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"error_type\": \"OAuthException\", \"code\": 400, \"error_message\": \"You must provide a valid client_secret and code\"}" + ] + } + ], + "source": [ + "# curl -X POST https://api.instagram.com/oauth/access_token \\\n", + "# -F 'client_id=1399481901197284' \\\n", + "# -F 'client_secret=b5f382674fc6790ca33e21987d77fcce' \\\n", + "# -F 'grant_type=authorization_code' \\\n", + "# -F 'redirect_uri=https://pseudo.com/' \\\n", + "# -F 'code=AQBk7omOXBna9xEXNLq3Ca7GO5M-XXMgJ44FVU6FtscAY1Dl-3CQi2VL-TLvQ4A5QTRgQYkKwFme_oOlgaEejl14D4YrBo0UQuN3ZXPt3RFPDSVPa0phaqkcDCc2WETvHMYzrkegQn6oE0pX7ObtYvNo9cNbRSw7z40BT7muYVeDuMr5T0-DD21T4N2z1EporW_D7ew-3NDb2bO-vkpPUzbFt0gLbAVSQOTF6KIF_LXsxw'" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "invalid syntax (4290686422.py, line 1)", + "output_type": "error", + "traceback": [ + " \u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[31m \u001b[39m\u001b[31mcurl -X POST https://api.instagram.com/oauth/access_token \\\u001b[39m\n ^\n\u001b[31mSyntaxError\u001b[39m\u001b[31m:\u001b[39m invalid syntax\n" + ] + } + ], + "source": [ + "# {\"access_token\":\n", + "# \"IGAA60SLOFBidBZAE9HdEpFWWl6d0RmMnVkdEVyS1ktYVJiZAHFmUFRtWS1WWmFxNWlXM3lHRW1iLVhWLUVTRWdnVVNCU056YURCbVZATQmRxb3BNOWNQM0hiX2NpTWFCdlpONDc3TzBMM0V1Xy1tTlV5N21KSEl4TEd3SktmMFZAPb1VwWWJ2X0tXYVl0OUIxRUowMXV5RmRR\",\n", + "# \"user_id\": 30281998311447113, \"permissions\":\n", + "# [\"instagram_business_basic\", \"instagram_business_manage_messages\",\n", + "# \"instagram_business_content_publish\", \"instagram_business_manage_insights\",\n", + "# \"instagram_business_manage_comments\"]}%" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "zsh:1: parse error near `&'\n" + ] + } + ], + "source": [ + "! curl -X GET \"https://graph.instagram.com/access_token?grant_type=ig_exchange_token&&client_secret=eb87G...&access_token=IGQVJ...\"" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# 60일 토큰\n", + "token = {\n", + " \"access_token\": \"IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR\",\n", + " \"token_type\": \"bearer\",\n", + " \"expires_in\": 5184000,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "token[\"access_token\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# curl -i -X GET \\\n", + "# \"https://graph.instagram.com/v23.0/me?fields=user_id,username&access_token=IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR\"" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "user_id = {\n", + " \"user_id\": \"17841451140542851\",\n", + " \"username\": \"yimbapchunguk\",\n", + " \"id\": \"30281998311447113\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'data': [{'id': '18007485335525524', 'caption': '정호영 쉐프의 냉제육\\n\\n내가 해봤던 요리 중 가장 쉬운 편에 속하지만\\n맛 또한 가장 좋은 편에 속한다.\\n\\n꼭 해먹어 보길 바람처럼 왔다가 이슬처럼 갈 순 없잖아', 'media_type': 'IMAGE', 'timestamp': '2024-05-18T17:56:11+0000', 'username': 'yimbapchunguk', 'like_count': 13, 'comments_count': 7}, {'id': '18008998250043407', 'caption': '여유 long time no see\\n원팬 파스타 sucks\\nI prefer old way', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-11-09T07:15:54+0000', 'username': 'yimbapchunguk', 'like_count': 11, 'comments_count': 0}, {'id': '18003786535984507', 'caption': '동기님들 방문해 주셨습니다… 근데 이제 인스피릿을 곁들인', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-08-29T17:05:47+0000', 'username': 'yimbapchunguk', 'like_count': 20, 'comments_count': 6}, {'id': '17913183179789753', 'caption': '메타코미디클럽 이선민씨 방문해주셨습니다', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-08-05T07:05:01+0000', 'username': 'yimbapchunguk', 'like_count': 19, 'comments_count': 1}, {'id': '17887411151864640', 'caption': '냉우동 맛도리\\n커티삭 맛도리', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-07-21T03:36:27+0000', 'username': 'yimbapchunguk', 'like_count': 17, 'comments_count': 4}, {'id': '17954969525209194', 'caption': '찬물로 파 씻다가 손구락 얼음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-01-24T13:43:49+0000', 'username': 'yimbapchunguk', 'like_count': 22, 'comments_count': 3}, {'id': '17884996073691257', 'caption': '바쁘다 바뻐 형빈사회\\n집에서 밥을 거의 못먹다가 오랜만에 꽤 공들여서 한 저녁\\n학회듣고 빨래하고 뭐하고 하다가 10시 반에 겨우 먹음\\n후식은 컵누들', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-11-15T16:08:58+0000', 'username': 'yimbapchunguk', 'like_count': 18, 'comments_count': 3}, {'id': '17958508052095266', 'caption': '개지리는 카레 레시피 알아냄\\n견과류 빻아서 넣으면 맛있음\\n나는 구운 캐슈너트 넣었음\\n후첨가 버터는 유통기한 지났음\\n카레 넣기 전에 이미 국물 지렸음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-10-18T04:22:23+0000', 'username': 'yimbapchunguk', 'like_count': 17, 'comments_count': 5}, {'id': '18313440799027884', 'caption': '많이도 해서 먹었습니다', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-08-19T12:06:56+0000', 'username': 'yimbapchunguk', 'like_count': 20, 'comments_count': 7}, {'id': '17910861560562909', 'caption': '간만에 임밥천국이 영업했어요.\\n저희 매장 기본찬인 웨지 감자와 봉골레 두접시, 육전 한사바리 요리했습니다.\\n+와인 6병 , 어 리를빗 꼬냑\\n\\n#봉골레 #봉골레파스타 #봉골레맛집 #육전 #육전맛집 #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-07-01T15:47:18+0000', 'username': 'yimbapchunguk', 'like_count': 19, 'comments_count': 1}, {'id': '17936239340067834', 'caption': '쌀이 세컵밖에 안남아서 냄비밥을 했다. 냄비밥은 정말 맛있다. \\n간만에 소고기먹었다. 소가 맛있는 이유는 자주 못먹어서 인듯ㅋㅋ \\n\\n#백종원레시피 #소불고기 #냄비밥 #최강록레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-06-11T16:23:58+0000', 'username': 'yimbapchunguk', 'like_count': 24, 'comments_count': 7}, {'id': '17928318890256046', 'caption': '오랜만에 손님 맞이로 임밥천국을 개시했읍니다. 꽤나 만족스러운 맛이었읍니다. 문의주세요. 단체 환영.\\n\\n#골뱅이무침 #부추전 #백종원레시피 #아하부장레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-05-08T04:45:29+0000', 'username': 'yimbapchunguk', 'like_count': 12, 'comments_count': 4}, {'id': '17925246794186696', 'caption': '카레는 역시 저 골든 커리가 젤 맛있는듯. 양파 캬라멜라이징 싹 돌리고 소고기 마이야르 촥 굽고 브섯이랑 댕건 스윽 구워서 가니시로 올리면 코코이찌방야 저리 가라야~\\n\\n#카레 #일본식카레 #골든카레 #goldencurry #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-19T12:26:32+0000', 'username': 'yimbapchunguk', 'like_count': 10, 'comments_count': 16}, {'id': '17933571104071046', 'caption': '이번 봉골레 실패했다 면발 무슨 칼국수냐? 애들 앞에서 임밥천국 쪽팔리게 에이씨 빡치네 \\n\\n#봉골레파스타', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-14T11:16:53+0000', 'username': 'yimbapchunguk', 'like_count': 10, 'comments_count': 6}, {'id': '17916218135489138', 'caption': '토맛토맛토 스파게티는 참 간편해~\\n\\n#토마토파스타', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-04T09:41:44+0000', 'username': 'yimbapchunguk', 'like_count': 9, 'comments_count': 3}, {'id': '17930629043024490', 'caption': '백종원 선생님의 콩내물 불게기를 해보았다. 설탕을 넣다 쫌 쏟아서 달달했다. 백선생님 오늘도 감사했습니다… 방구석 제자가 되니 기쁘네요 😎\\n\\n#백종원레시피 #콩나물불고기 #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-02-22T04:13:32+0000', 'username': 'yimbapchunguk', 'like_count': 14, 'comments_count': 0}, {'id': '17982896386458231', 'caption': '설날 기념 잡채를 만들어보았다. 편스토랑에서 어남선생의 레시피를 카피했다. 편하고 맛이 좋았다. 신선하게 시금치 대신 알배추를 넣었다. 달고 담백하니 맛이 좋았다. 하이볼 세잔 먹음. \\n\\n#편스토랑레시피 #어남선생레시피 #잡채', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-02-01T13:38:06+0000', 'username': 'yimbapchunguk', 'like_count': 8, 'comments_count': 3}, {'id': '18184518865091131', 'caption': '닭개장… 어렵고 힘들 것 같나? 그렇다. 두시간을 걸려서 만들었다. 이번엔 msg를 안넣고 만들었고 그래도 맛있었다. 나중에 친구들 오면 만들어서 쏘주랑 먹어야지\\n\\n#닭개장 #아하부장 #아하부장레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-27T14:04:40+0000', 'username': 'yimbapchunguk', 'like_count': 5, 'comments_count': 4}, {'id': '17959135507574073', 'caption': '고기 굽기는 아직 서투르다… 코팅 팬을 또 태워버렸어… 설거지는 두렵지만 맛은 아주 맛있었다. 이런 맛이라면 그깟 설거지 백번은 해주마!!!\\n\\n#돼지고기목살구이 #마이야르 #몬트리올시즈닝', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-22T14:09:32+0000', 'username': 'yimbapchunguk', 'like_count': 5, 'comments_count': 4}, {'id': '17916204953221655', 'caption': '소고기 무국\\n깊다… 냄비가 3m쯤 되려나…? 착각하게 하는 맛 \\n백종원이 없는 다른 멀티버스에서 임형빈 너는 뭘 먹고 지내냐?!!!\\n#소고기무국 #백종원 #백종원레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-17T13:39:02+0000', 'username': 'yimbapchunguk', 'like_count': 6, 'comments_count': 2}, {'id': '17949593110651400', 'caption': '제육볶음\\n#공격수셰프 #공격수셰프레시피 #제육볶음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-17T10:52:03+0000', 'username': 'yimbapchunguk', 'like_count': 2, 'comments_count': 2}], 'paging': {'cursors': {'before': 'QVFGbVRZALVFGaDhIbkh5b3F0a1cydEZATQzVkcGN6N21Wbk1leTFpZA01YMC03dm5ZAMUVLR282UXV6WmdRV3g4cllPYlRIU1ZA5NDZAmQkN4cmRTZAkIzNDEtYW5R', 'after': 'QVFGbXlTM3hoTTA0S0U3VXpBbHEyMWlQYWd2Ym5leUpsTUJPVWRZARFNuTmwwWGJqenVVQ0paOUxQWG1JWUtyMHBTNUM2cHBFNTRqVHJ2RWlFZAEdXUlRPVDNR'}}}\n" + ] + } + ], + "source": [ + "# 미디어 조회\n", + "\n", + "import requests\n", + "\n", + "# 액세스 토큰과 Instagram User ID를 입력하세요\n", + "ACCESS_TOKEN = token[\"access_token\"]\n", + "USER_ID = user_id[\"user_id\"] # 보통 'me'로도 가능\n", + "\n", + "# 요청할 필드 지정 (예: id, caption, media_type, media_url, timestamp 등)\n", + "fields = \"id,caption,media_type,timestamp,username,like_count,comments_count\"\n", + "\n", + "url = f\"https://graph.instagram.com/v23.0/{USER_ID}/media?access_token={ACCESS_TOKEN}\"\n", + "params = {\n", + " \"fields\": fields,\n", + " #'access_token': ACCESS_TOKEN\n", + "}\n", + "\n", + "response = requests.get(url, params=params)\n", + "if response.status_code == 200:\n", + " data = response.json()\n", + " print(data)\n", + "else:\n", + " print(\"Error:\", response.status_code, response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '18007485335525524',\n", + " 'caption': '정호영 쉐프의 냉제육\\n\\n내가 해봤던 요리 중 가장 쉬운 편에 속하지만\\n맛 또한 가장 좋은 편에 속한다.\\n\\n꼭 해먹어 보길 바람처럼 왔다가 이슬처럼 갈 순 없잖아',\n", + " 'media_type': 'IMAGE',\n", + " 'timestamp': '2024-05-18T17:56:11+0000',\n", + " 'username': 'yimbapchunguk',\n", + " 'like_count': 13,\n", + " 'comments_count': 6}" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data[\"data\"][0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'id': '18007245821606004', 'text': 'Test', 'timestamp': '2025-06-29T14:03:22+0000'}, {'id': '18022768160476133', 'text': '@mirageee_00 맞아요', 'timestamp': '2024-05-19T02:48:47+0000'}, {'id': '17948944328688519', 'text': '@joo_bob_1212 당신의 못된 심보', 'timestamp': '2024-05-19T02:48:39+0000'}, {'id': '18017927048263258', 'text': '@gangster_joo 아직 제자리 걸음입니다', 'timestamp': '2024-05-19T02:48:28+0000'}, {'id': '17976935447558867', 'text': '왜 이유없이 화가나지 ?', 'timestamp': '2024-05-18T18:57:59+0000'}, {'id': '18047838088664274', 'text': '소금 넣고 끓는 물에 담구는 그거?', 'timestamp': '2024-05-18T18:07:18+0000'}, {'id': '18414185098071553', 'text': '저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??', 'timestamp': '2024-05-18T18:04:05+0000'}]\n" + ] + } + ], + "source": [ + "# 미디어 댓글 조회\n", + "\n", + "import requests\n", + "\n", + "# IG_MEDIA_ID에 실제 미디어 ID를 입력하세요\n", + "IG_MEDIA_ID = \"18007485335525524\" # data['data'][0]['id']\n", + "ACCESS_TOKEN = token[\"access_token\"]\n", + "# ACCESS_TOKEN = \"IGAA60SLOFBidBZAE03d3VxN3lvaHAyMXJHNVM2bnFGeDhvdWNuOHRtS1c5b3gzOFpWQzZARZAjhBMXBOTlBEOVR1cXMwUG5BSXlWWHZALWUpOVHBna0Rtc2tyVy1ieE5TM3pYa3NiMzlOR3hmOTB4dUFUdy1jZAEFHWmF0R0J5ZA3ZAlZAwZDZD\"\n", + "\n", + "url = f\"https://graph.instagram.com/v23.0/{IG_MEDIA_ID}/comments?access_token={ACCESS_TOKEN}\"\n", + "fields = \"id,text,timestamp\"\n", + "params = {\"fields\": fields}\n", + "\n", + "response = requests.get(url, params=params)\n", + "if response.status_code == 200:\n", + " comments_data = response.json()\n", + " print(comments_data[\"data\"])\n", + "else:\n", + " print(\"Error:\", response.status_code, response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# 60일 토큰\n", + "token = {\n", + " \"access_token\": \"IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR\",\n", + " \"token_type\": \"bearer\",\n", + " \"expires_in\": 5184000,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "token[\"access_token\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# curl -i -X GET \\\n", + "# \"https://graph.instagram.com/v23.0/me?fields=user_id,username&access_token=IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "user_id = {\n", + " \"user_id\": \"17841451140542851\",\n", + " \"username\": \"yimbapchunguk\",\n", + " \"id\": \"30281998311447113\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'data': [{'id': '18007485335525524', 'caption': '정호영 쉐프의 냉제육\\n\\n내가 해봤던 요리 중 가장 쉬운 편에 속하지만\\n맛 또한 가장 좋은 편에 속한다.\\n\\n꼭 해먹어 보길 바람처럼 왔다가 이슬처럼 갈 순 없잖아', 'media_type': 'IMAGE', 'timestamp': '2024-05-18T17:56:11+0000', 'username': 'yimbapchunguk', 'like_count': 13, 'comments_count': 6}, {'id': '18008998250043407', 'caption': '여유 long time no see\\n원팬 파스타 sucks\\nI prefer old way', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-11-09T07:15:54+0000', 'username': 'yimbapchunguk', 'like_count': 11, 'comments_count': 0}, {'id': '18003786535984507', 'caption': '동기님들 방문해 주셨습니다… 근데 이제 인스피릿을 곁들인', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-08-29T17:05:47+0000', 'username': 'yimbapchunguk', 'like_count': 20, 'comments_count': 6}, {'id': '17913183179789753', 'caption': '메타코미디클럽 이선민씨 방문해주셨습니다', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-08-05T07:05:01+0000', 'username': 'yimbapchunguk', 'like_count': 19, 'comments_count': 1}, {'id': '17887411151864640', 'caption': '냉우동 맛도리\\n커티삭 맛도리', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-07-21T03:36:27+0000', 'username': 'yimbapchunguk', 'like_count': 17, 'comments_count': 4}, {'id': '17954969525209194', 'caption': '찬물로 파 씻다가 손구락 얼음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2023-01-24T13:43:49+0000', 'username': 'yimbapchunguk', 'like_count': 22, 'comments_count': 3}, {'id': '17884996073691257', 'caption': '바쁘다 바뻐 형빈사회\\n집에서 밥을 거의 못먹다가 오랜만에 꽤 공들여서 한 저녁\\n학회듣고 빨래하고 뭐하고 하다가 10시 반에 겨우 먹음\\n후식은 컵누들', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-11-15T16:08:58+0000', 'username': 'yimbapchunguk', 'like_count': 18, 'comments_count': 3}, {'id': '17958508052095266', 'caption': '개지리는 카레 레시피 알아냄\\n견과류 빻아서 넣으면 맛있음\\n나는 구운 캐슈너트 넣었음\\n후첨가 버터는 유통기한 지났음\\n카레 넣기 전에 이미 국물 지렸음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-10-18T04:22:23+0000', 'username': 'yimbapchunguk', 'like_count': 17, 'comments_count': 5}, {'id': '18313440799027884', 'caption': '많이도 해서 먹었습니다', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-08-19T12:06:56+0000', 'username': 'yimbapchunguk', 'like_count': 20, 'comments_count': 7}, {'id': '17910861560562909', 'caption': '간만에 임밥천국이 영업했어요.\\n저희 매장 기본찬인 웨지 감자와 봉골레 두접시, 육전 한사바리 요리했습니다.\\n+와인 6병 , 어 리를빗 꼬냑\\n\\n#봉골레 #봉골레파스타 #봉골레맛집 #육전 #육전맛집 #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-07-01T15:47:18+0000', 'username': 'yimbapchunguk', 'like_count': 19, 'comments_count': 1}, {'id': '17936239340067834', 'caption': '쌀이 세컵밖에 안남아서 냄비밥을 했다. 냄비밥은 정말 맛있다. \\n간만에 소고기먹었다. 소가 맛있는 이유는 자주 못먹어서 인듯ㅋㅋ \\n\\n#백종원레시피 #소불고기 #냄비밥 #최강록레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-06-11T16:23:58+0000', 'username': 'yimbapchunguk', 'like_count': 24, 'comments_count': 7}, {'id': '17928318890256046', 'caption': '오랜만에 손님 맞이로 임밥천국을 개시했읍니다. 꽤나 만족스러운 맛이었읍니다. 문의주세요. 단체 환영.\\n\\n#골뱅이무침 #부추전 #백종원레시피 #아하부장레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-05-08T04:45:29+0000', 'username': 'yimbapchunguk', 'like_count': 12, 'comments_count': 4}, {'id': '17925246794186696', 'caption': '카레는 역시 저 골든 커리가 젤 맛있는듯. 양파 캬라멜라이징 싹 돌리고 소고기 마이야르 촥 굽고 브섯이랑 댕건 스윽 구워서 가니시로 올리면 코코이찌방야 저리 가라야~\\n\\n#카레 #일본식카레 #골든카레 #goldencurry #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-19T12:26:32+0000', 'username': 'yimbapchunguk', 'like_count': 10, 'comments_count': 16}, {'id': '17933571104071046', 'caption': '이번 봉골레 실패했다 면발 무슨 칼국수냐? 애들 앞에서 임밥천국 쪽팔리게 에이씨 빡치네 \\n\\n#봉골레파스타', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-14T11:16:53+0000', 'username': 'yimbapchunguk', 'like_count': 10, 'comments_count': 6}, {'id': '17916218135489138', 'caption': '토맛토맛토 스파게티는 참 간편해~\\n\\n#토마토파스타', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-03-04T09:41:44+0000', 'username': 'yimbapchunguk', 'like_count': 9, 'comments_count': 3}, {'id': '17930629043024490', 'caption': '백종원 선생님의 콩내물 불게기를 해보았다. 설탕을 넣다 쫌 쏟아서 달달했다. 백선생님 오늘도 감사했습니다… 방구석 제자가 되니 기쁘네요 😎\\n\\n#백종원레시피 #콩나물불고기 #임밥천국', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-02-22T04:13:32+0000', 'username': 'yimbapchunguk', 'like_count': 14, 'comments_count': 0}, {'id': '17982896386458231', 'caption': '설날 기념 잡채를 만들어보았다. 편스토랑에서 어남선생의 레시피를 카피했다. 편하고 맛이 좋았다. 신선하게 시금치 대신 알배추를 넣었다. 달고 담백하니 맛이 좋았다. 하이볼 세잔 먹음. \\n\\n#편스토랑레시피 #어남선생레시피 #잡채', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-02-01T13:38:06+0000', 'username': 'yimbapchunguk', 'like_count': 8, 'comments_count': 3}, {'id': '18184518865091131', 'caption': '닭개장… 어렵고 힘들 것 같나? 그렇다. 두시간을 걸려서 만들었다. 이번엔 msg를 안넣고 만들었고 그래도 맛있었다. 나중에 친구들 오면 만들어서 쏘주랑 먹어야지\\n\\n#닭개장 #아하부장 #아하부장레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-27T14:04:40+0000', 'username': 'yimbapchunguk', 'like_count': 5, 'comments_count': 4}, {'id': '17959135507574073', 'caption': '고기 굽기는 아직 서투르다… 코팅 팬을 또 태워버렸어… 설거지는 두렵지만 맛은 아주 맛있었다. 이런 맛이라면 그깟 설거지 백번은 해주마!!!\\n\\n#돼지고기목살구이 #마이야르 #몬트리올시즈닝', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-22T14:09:32+0000', 'username': 'yimbapchunguk', 'like_count': 5, 'comments_count': 4}, {'id': '17916204953221655', 'caption': '소고기 무국\\n깊다… 냄비가 3m쯤 되려나…? 착각하게 하는 맛 \\n백종원이 없는 다른 멀티버스에서 임형빈 너는 뭘 먹고 지내냐?!!!\\n#소고기무국 #백종원 #백종원레시피', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-17T13:39:02+0000', 'username': 'yimbapchunguk', 'like_count': 6, 'comments_count': 2}, {'id': '17949593110651400', 'caption': '제육볶음\\n#공격수셰프 #공격수셰프레시피 #제육볶음', 'media_type': 'CAROUSEL_ALBUM', 'timestamp': '2022-01-17T10:52:03+0000', 'username': 'yimbapchunguk', 'like_count': 2, 'comments_count': 2}], 'paging': {'cursors': {'before': 'QVFIUlJjTEM2Y3U1X2dRcGw4UVNhb0tnTmpvR1F6aUFfZA2RtcVpOZAnNtek5PeFlQRk1jdHFCTVJIaTBVelBmU0pYX0dVa3R0ckduam1CLXctb09lQTRiLUZAR', 'after': 'QVFIUjdhbkp1dmk1RG91bFo2Qm1taGc5R2RHVnc2NEVTQ0hTaExUTlNVR3NyamVwY01YM2xEWHNpX0tmaG9FQW1Ec1REbXpZAWmZAxMTNUbVFlMFp0RE1lLU5B'}}}\n" + ] + } + ], + "source": [ + "# 미디어 조회\n", + "\n", + "import requests\n", + "\n", + "# 액세스 토큰과 Instagram User ID를 입력하세요\n", + "ACCESS_TOKEN = token[\"access_token\"]\n", + "USER_ID = user_id[\"user_id\"] # 보통 'me'로도 가능\n", + "\n", + "# 요청할 필드 지정 (예: id, caption, media_type, media_url, timestamp 등)\n", + "fields = \"id,caption,media_type,timestamp,username,like_count,comments_count\"\n", + "\n", + "url = f\"https://graph.instagram.com/v23.0/{USER_ID}/media?access_token={ACCESS_TOKEN}\"\n", + "params = {\n", + " \"fields\": fields,\n", + " #'access_token': ACCESS_TOKEN\n", + "}\n", + "\n", + "response = requests.get(url, params=params)\n", + "if response.status_code == 200:\n", + " data = response.json()\n", + " print(data)\n", + "else:\n", + " print(\"Error:\", response.status_code, response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': '18007485335525524',\n", + " 'caption': '정호영 쉐프의 냉제육\\n\\n내가 해봤던 요리 중 가장 쉬운 편에 속하지만\\n맛 또한 가장 좋은 편에 속한다.\\n\\n꼭 해먹어 보길 바람처럼 왔다가 이슬처럼 갈 순 없잖아',\n", + " 'media_type': 'IMAGE',\n", + " 'timestamp': '2024-05-18T17:56:11+0000',\n", + " 'username': 'yimbapchunguk',\n", + " 'like_count': 13,\n", + " 'comments_count': 6}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "data[\"data\"][0]" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'id': '18007245821606004', 'text': 'Test', 'timestamp': '2025-06-29T14:03:22+0000'}, {'id': '18022768160476133', 'text': '@mirageee_00 맞아요', 'timestamp': '2024-05-19T02:48:47+0000'}, {'id': '17948944328688519', 'text': '@joo_bob_1212 당신의 못된 심보', 'timestamp': '2024-05-19T02:48:39+0000'}, {'id': '18017927048263258', 'text': '@gangster_joo 아직 제자리 걸음입니다', 'timestamp': '2024-05-19T02:48:28+0000'}, {'id': '17976935447558867', 'text': '왜 이유없이 화가나지 ?', 'timestamp': '2024-05-18T18:57:59+0000'}, {'id': '18047838088664274', 'text': '소금 넣고 끓는 물에 담구는 그거?', 'timestamp': '2024-05-18T18:07:18+0000'}, {'id': '18414185098071553', 'text': '저한테 해줬던 봉골레 파스타는 일취월장 하셨나요?? 임쉪??', 'timestamp': '2024-05-18T18:04:05+0000'}]\n" + ] + } + ], + "source": [ + "# 미디어 댓글 조회\n", + "\n", + "import requests\n", + "\n", + "# IG_MEDIA_ID에 실제 미디어 ID를 입력하세요\n", + "IG_MEDIA_ID = \"18007485335525524\" # data['data'][0]['id']\n", + "# ACCESS_TOKEN = \"IGAA60SLOFBidBZAE03d3VxN3lvaHAyMXJHNVM2bnFGeDhvdWNuOHRtS1c5b3gzOFpWQzZARZAjhBMXBOTlBEOVR1cXMwUG5BSXlWWHZALWUpOVHBna0Rtc2tyVy1ieE5TM3pYa3NiMzlOR3hmOTB4dUFUdy1jZAEFHWmF0R0J5ZA3ZAlZAwZDZD\"\n", + "\n", + "url = f\"https://graph.instagram.com/v23.0/{IG_MEDIA_ID}/comments?access_token={ACCESS_TOKEN}\"\n", + "fields = \"id,text,timestamp\"\n", + "params = {\"fields\": fields}\n", + "\n", + "response = requests.get(url, params=params)\n", + "if response.status_code == 200:\n", + " comments_data = response.json()\n", + " print(comments_data[\"data\"])\n", + "else:\n", + " print(\"Error:\", response.status_code, response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Error: 400 {\"error\":{\"message\":\"Failed to decode\",\"type\":\"OAuthException\",\"code\":190,\"fbtrace_id\":\"AxJF6E45HOHByVELqH4j9yz\"}}\n" + ] + } + ], + "source": [ + "# [POST /{ig-comment-id}/replies](https://developers.facebook.com/docs/instagram-api/reference/ig-comment/replies#create)\n", + "# 미디어 답글\n", + "\n", + "import requests\n", + "\n", + "# IG_MEDIA_ID에 실제 미디어 ID를 입력하세요\n", + "IG_MEDIA_ID = \"18022768160476133\" # data['data'][0]['id']\n", + "# ACCESS_TOKEN = \"IGAA60SLOFBidBZAFB1ZAURpem9lRFVaeS04bXZAvWXhkV0dCSjY1ODVHaVoybERLcnVqOEhvYlNPbXFUNUE0a20weER1aGFXOTF2VXY1Ti1mRmd3WmdGRUh2SDgyeDVxam5BS1RMZAmkzZAWszNDZArZAFoxbXdR\"\n", + "\n", + "url = f\"https://graph.instagram.com/v23.0/{IG_MEDIA_ID}/replies?access_token={ACCESS_TOKEN}?message=test\"\n", + "\n", + "\n", + "response = requests.post(\n", + " url,\n", + " # params=params\n", + ")\n", + "if response.status_code == 200:\n", + " comments_data = response.json()\n", + " print(comments_data[\"data\"])\n", + "else:\n", + " print(\"Error:\", response.status_code, response.text)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[2mUsing Python 3.13.5 environment at: /Users/hyungson/pseudo/Act1-Entertainment/.venv\u001b[0m\n", + "Package Version\n", + "------------------------ -----------\n", + "aiohappyeyeballs 2.6.1\n", + "aiohttp 3.11.16\n", + "aiosignal 1.3.2\n", + "annotated-types 0.7.0\n", + "anyio 4.9.0\n", + "appnope 0.1.4\n", + "asttokens 3.0.0\n", + "attrs 25.3.0\n", + "blockbuster 1.5.24\n", + "certifi 2025.1.31\n", + "cffi 1.17.1\n", + "cfgv 3.4.0\n", + "charset-normalizer 3.4.1\n", + "click 8.1.8\n", + "cloudpickle 3.1.1\n", + "comm 0.2.2\n", + "cryptography 44.0.2\n", + "dataclasses-json 0.6.7\n", + "debugpy 1.8.14\n", + "decorator 5.2.1\n", + "distlib 0.3.9\n", + "distro 1.9.0\n", + "executing 2.2.0\n", + "filelock 3.18.0\n", + "forbiddenfruit 0.1.4\n", + "frozenlist 1.5.0\n", + "h11 0.14.0\n", + "httpcore 1.0.8\n", + "httpx 0.28.1\n", + "httpx-sse 0.4.0\n", + "identify 2.6.9\n", + "idna 3.10\n", + "iniconfig 2.1.0\n", + "ipykernel 6.29.5\n", + "ipython 9.1.0\n", + "ipython-pygments-lexers 1.1.1\n", + "jedi 0.19.2\n", + "jiter 0.9.0\n", + "jsonpatch 1.33\n", + "jsonpointer 3.0.0\n", + "jsonschema-rs 0.29.1\n", + "jupyter-client 8.6.3\n", + "jupyter-core 5.7.2\n", + "langchain 0.3.23\n", + "langchain-community 0.3.21\n", + "langchain-core 0.3.52\n", + "langchain-openai 0.3.13\n", + "langchain-text-splitters 0.3.8\n", + "langgraph 0.3.30\n", + "langgraph-api 0.1.7\n", + "langgraph-checkpoint 2.0.24\n", + "langgraph-cli 0.2.3\n", + "langgraph-prebuilt 0.1.8\n", + "langgraph-runtime-inmem 0.0.4\n", + "langgraph-sdk 0.1.61\n", + "langsmith 0.3.31\n", + "marshmallow 3.26.1\n", + "matplotlib-inline 0.1.7\n", + "multidict 6.4.3\n", + "mypy-extensions 1.0.0\n", + "nest-asyncio 1.6.0\n", + "nodeenv 1.9.1\n", + "numpy 2.2.4\n", + "openai 1.74.0\n", + "orjson 3.10.16\n", + "ormsgpack 1.9.1\n", + "packaging 24.2\n", + "parso 0.8.4\n", + "pexpect 4.9.0\n", + "platformdirs 4.3.7\n", + "pluggy 1.5.0\n", + "pre-commit 4.2.0\n", + "prompt-toolkit 3.0.51\n", + "propcache 0.3.1\n", + "psutil 7.0.0\n", + "ptyprocess 0.7.0\n", + "pure-eval 0.2.3\n", + "pycparser 2.22\n", + "pydantic 2.11.3\n", + "pydantic-core 2.33.1\n", + "pydantic-settings 2.8.1\n", + "pygments 2.19.1\n", + "pyjwt 2.10.1\n", + "pytest 8.3.5\n", + "python-dateutil 2.9.0.post0\n", + "python-dotenv 1.1.0\n", + "pyyaml 6.0.2\n", + "pyzmq 26.4.0\n", + "regex 2024.11.6\n", + "requests 2.32.3\n", + "requests-toolbelt 1.0.0\n", + "six 1.17.0\n", + "sniffio 1.3.1\n", + "sqlalchemy 2.0.40\n", + "sse-starlette 2.1.3\n", + "stack-data 0.6.3\n", + "starlette 0.46.2\n", + "structlog 25.2.0\n", + "tenacity 9.1.2\n", + "tiktoken 0.9.0\n", + "tornado 6.4.2\n", + "tqdm 4.67.1\n", + "traitlets 5.14.3\n", + "typing-extensions 4.13.2\n", + "typing-inspect 0.9.0\n", + "typing-inspection 0.4.0\n", + "urllib3 2.4.0\n", + "uvicorn 0.34.1\n", + "virtualenv 20.30.0\n", + "watchfiles 1.0.5\n", + "wcwidth 0.2.13\n", + "xxhash 3.5.0\n", + "yarl 1.19.0\n", + "zstandard 0.23.0\n" + ] + } + ], + "source": [ + "!uv pip list" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "!source /Users/hyungson/pseudo/Act1-Entertainment/.venv/bin/activate" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "insta_data = pd.read_csv(\"../../../test_comments.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0 rozy.gramVerified 115 w로지 폼, 아니 폰트 미쳤다 🤩 여러분! ...\n", + "1 Your Seoul-inspired visuals are giving major m...\n", + "2 This vibe’s giving me serious ‘final boss but ...\n", + "3 I love you\n", + "4 Adorable❤️\n", + "Name: Ône_ând_ônly_, dtype: object" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "insta_data.iloc[:, 1][:5]" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_google_genai import ChatGoogleGenerativeAI\n", + "import os\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv(override=True)\n", + "api_key = os.environ.get(\"GOOGLE_API_KEY\")\n", + "llm = ChatGoogleGenerativeAI(model=\"gemini-2.5-flash\", google_api_key=api_key)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "response = llm.invoke(\"안녕\")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "content='안녕하세요!' additional_kwargs={} response_metadata={'prompt_feedback': {'block_reason': 0, 'safety_ratings': []}, 'finish_reason': 'STOP', 'model_name': 'gemini-2.5-flash', 'safety_ratings': []} id='run--702ce41a-725d-4557-939f-af0e1b5ce97a-0' usage_metadata={'input_tokens': 3, 'output_tokens': 2, 'total_tokens': 41, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 36}}\n" + ] + } + ], + "source": [ + "print(response)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.read_csv(\"../../../test_comments.csv\")" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Index(['https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/', 'hanr0r0 3 d도망 라이브 클립 봐 주면 안 되낭?'], dtype='object')" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.columns" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "df.columns = [\"source\", \"comments\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "new_row = {\n", + " \"source\": \"https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/\",\n", + " \"comments\": \"hanr0r0 3 d도망 라이브 클립 봐 주면 안 되낭?\",\n", + "}\n", + "\n", + "df = pd.concat([pd.DataFrame([new_row]), df], ignore_index=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "df = df.loc[~df[\"comments\"].str.startswith(\"View\")]" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "# source별로 comments를 리스트로 모으기\n", + "source_comments = df.groupby(\"source\")[\"comments\"].apply(list).to_dict()\n", + "\n", + "# json 파일로 저장\n", + "with open(\"../../../test_comments.json\", \"w\", encoding=\"utf-8\") as f:\n", + " json.dump(source_comments, f, ensure_ascii=False, indent=2)" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'https://www.instagram.com/hanr0r0/p/DI3mmBkSpX8/': ['hanr0r0\\xa011 w@coldplay 🪐🤍🌈',\n", + " '당신은 굿이에요 당신은 그레잇해요',\n", + " '💓💓🥳',\n", + " '로로언니 저시험포기하고언니보러 갓어요 ㅠ넘애쀼',\n", + " '로로가 울지도 않고 노래를 잘해요…',\n", + " '@bird.ybuddy 헤엑 로로 헤메코 몬일이다냐 개입뻐😮',\n", + " '😍😍😍😍😍😍😍😍😍😍😍😍',\n", + " '아기락스타 너무 멋쟁이야 너무 기특해 잘햇다👏👏🔥🔥❤️❤️',\n", + " '고생했다이🔥',\n", + " '와..... 언니 너무 예뻐요 정말',\n", + " '사랑훼❤️',\n", + " '👸🏻so amazing!! Proud of you🥹🤍',\n", + " '우주최고밴드와 우주최고아기락스타🤟🏻',\n", + " '제발…너무이뻐언니사랑해…ㅠㅠ',\n", + " '대로로🔥',\n", + " '입춘 너무 좋아요🌸🌸',\n", + " '25 April'],\n", + " 'https://www.instagram.com/hanr0r0/p/DI59UdoyuRy/': ['hanr0r0\\xa010 w@official_lovesome 💕',\n", + " '누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ',\n", + " '👏🏻👏🏻💕💕💕💕💕',\n", + " '왤케예뻐 ㅁㅊ',\n", + " '로로 사랑해!! 💗 로로가 러브썸 찢음',\n", + " '3일 연속 공연 고생하셨습니다🔥🔥',\n", + " '넘넘아리따우셔라..',\n", + " '[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략)',\n", + " '오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다.',\n", + " '한로로와결혼할래',\n", + " '대로로',\n", + " '대로로',\n", + " \"대—-로—-로( つ '-')╮—̳͟͞ 🍓\",\n", + " '26 April',\n", + " 'hanr0r0\\xa011 w@official_lovesome 💕',\n", + " '누나 제 친구들이 누나 가사가 유치하대요 ㅠㅠ',\n", + " '👏🏻👏🏻💕💕💕💕💕',\n", + " '왤케예뻐 ㅁㅊ',\n", + " '로로 사랑해!! 💗 로로가 러브썸 찢음',\n", + " '넘넘아리따우셔라..',\n", + " '오늘 비긴어게인을 보면서 처음으로 한로로님을 알게되어 입춘이란 노래를 다운로드 받아서 무한반복 듣고있습니다. 앞으로도 좋은 노래 부탁드립니다.',\n", + " '[web발신]너는 나를 존중해야한다나 대로로는 콜드플레이서울공연오프닝아티스트로무대에섰으며 이상비행이란 명반을냈고 다수의 싱글이 스트리밍 100만을달성했고방탄소년단멤버의샤라웃을받으며글로벌인정을받았으며 음악 시작한지몇개월만에국내최대규모스튜디오MOS에정식입사하여뮤지션으로서공식커리어를시작했으며 피식대학에서 1시간가량이나 같은곡 라이브를하였고 남극까지 들릴정도의 라이브 성량을 갖고있으며....(중략)',\n", + " '한로로와결혼할래',\n", + " '대로로',\n", + " \"대—-로—-로( つ '-')╮—̳͟͞ 🍓\",\n", + " '3일 연속 공연 고생하셨습니다🔥🔥',\n", + " '대로로',\n", + " '26 April'],\n", + " 'https://www.instagram.com/hanr0r0/p/DIYAXPWSRJp/': ['hanr0r0\\xa012 w🍓🍓',\n", + " '기여우어기여워기여워기여워❤️',\n", + " '너무 귀여워요 ㅜㅜ',\n", + " '귀여워 딸기공주',\n", + " '@easy_362 이분이 한로로씨구나',\n", + " 'te kiero micho',\n", + " '스트로 로로~',\n", + " '사랑해요',\n", + " '딸기모찌',\n", + " '딸기로로 🍓',\n", + " '대로로',\n", + " '상의 정보좀요 급함',\n", + " '청주 한씨',\n", + " '13 April',\n", + " 'hanr0r0\\xa012 w🍓🍓',\n", + " '기여우어기여워기여워기여워❤️',\n", + " '너무 귀여워요 ㅜㅜ',\n", + " '귀여워 딸기공주',\n", + " '@easy_362 이분이 한로로씨구나',\n", + " 'te kiero micho',\n", + " '스트로 로로~',\n", + " '사랑해요',\n", + " '딸기모찌',\n", + " '딸기로로 🍓',\n", + " '대로로',\n", + " '상의 정보좀요 급함',\n", + " '청주 한씨',\n", + " '13 April'],\n", + " 'https://www.instagram.com/hanr0r0/p/DIn9wrsywdl/': ['hanr0r0\\xa011 w2025 spring live clip !! ❄️☀️🌱',\n", + " '한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라',\n", + " '한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어',\n", + " '한로로 얼굴의 점이 되고싶다……………',\n", + " '이런 선물이..🥹',\n", + " '로로짱 최고!!',\n", + " '입하 입추 입동 안내고 뭐하냐!',\n", + " '한로로 누나 노래 들으면서 열공할게염',\n", + " '스프링 라이브 클립인데 눈이 내려여',\n", + " '한로로가 내 마음에 폭싹 내렸수다',\n", + " '사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니.',\n", + " '19 April',\n", + " 'hanr0r0\\xa012 w2025 spring live clip !! ❄️☀️🌱',\n", + " '한로로는사과해라내가학교도잘못다니고유튜브에서한로로만검색한다한로로는사과해라',\n", + " '한로로사랑해 스카에서 공부 안 하고 당신 노래만 들으면서 멍 때리고 잇어',\n", + " '한로로 얼굴의 점이 되고싶다……………',\n", + " '이런 선물이..🥹',\n", + " '로로짱 최고!!',\n", + " '입하 입추 입동 안내고 뭐하냐!',\n", + " '한로로 누나 노래 들으면서 열공할게염',\n", + " '스프링 라이브 클립인데 눈이 내려여',\n", + " '한로로가 내 마음에 폭싹 내렸수다',\n", + " '사실 나는 작은 서점을 운영 중인데 아침에 보니 카톡도 999,메시지도 999개찍혀 있더라.깜짝 놀라 확인해보니 영한 사전 오류로 환불요청이 가득했다심지어 고소까지 언급하는 사람도 있더라고작 오류 몇 개가 , 고소까지 들먹일 정도로 화날 일인가?투덜대면서 무슨 오류인지 사전을 펼쳐봤다..이런! 한심하기 짝이 없는 오류였다이런 기본적인 단어도 틀린 사전이라니,Wonderful에는 세가지 뜻이 있다1.멋진 2.경이로운 3.한로로사전엔 세번째 뜻이 적혀 있지 않았고, 급히페이지를 넘긴다..Cute에는 세가지 뜻이 있다1.귀여운 2.영리한 3.한로로이번에도! 그 뒤로는 불 보듯 뻔했다gorgeous, lovely, special.오늘은 바쁠 것 같다, 팔았던 사전들을 죄다 환불해줘야 할테니.',\n", + " '19 April'],\n", + " 'https://www.instagram.com/hanr0r0/p/DJV3VeqS7C6/': ['hanr0r0\\xa09 w자연이 좋아 그래서 자연스러운 내가 좋아',\n", + " '안경로로를 사랑해…🪽💕',\n", + " '저도좋아요누나',\n", + " '자연이 좋다는걸 벌써 알았더니 역시 현명로로🌿',\n", + " '사랑해',\n", + " '로로쨩을 좋아하는 내가 좋아♡',\n", + " '자연스럽다 라는 말을 다시 한 번 생각해보게 되는 !!! 🍀',\n", + " '제일 좋아하는 버스자리',\n", + " '안경이 본체',\n", + " '저는 그냥 누나 좋아할래요',\n", + " '@o._.o920 로로가 좋아',\n", + " '나두 초록이 좋아서 초록이 가득한 여름이 좋아~🍃🍃',\n", + " '저도 누나가 좋아요',\n", + " '@nowizmisselehtreven 자연이 좋다',\n", + " '저도언니가좋아요..',\n", + " '7 May',\n", + " '기요미ㅗㅠㅠㅠㅠㅠ',\n", + " '@se_unn 징쨔 긔엽다.......',\n", + " '나도누나가좋아',\n", + " '저도 누나가 좋아요',\n", + " '콜드플레이 영상보고 입덕했습니다',\n", + " '팬입니다 ㅎㅎ 출퇴근, 강아지산책, 야근, 청소, 설거지 등 한로로의 음악과 항상✌🏻',\n", + " '로로ㅠ❤️',\n", + " '7 May'],\n", + " 'https://www.instagram.com/hanr0r0/p/DKE0pWsS0bJ/': ['hanr0r0\\xa06 w@peak_festa 💙',\n", + " '청주한씨 화이팅 👏',\n", + " '귀욥다',\n", + " '언니오늘최고였어욤🔥',\n", + " '로로애기 최고였어 !!!!!! 대로로 !!!! 사랑해요 😭😭🫶🏻🫶🏻',\n", + " '아 진짜... 오늘 너무 행복했어요...😢 너무 고맙고..뿌듯하고..',\n", + " '우리의 락스타 🔥',\n", + " '천사🪽🪽🪽🪽🪽🪽🪽♥️',\n", + " '따랑해',\n", + " '누나 사랑해요',\n", + " '사랑합니다',\n", + " '엥 왜케이쁨',\n", + " '레쭈고 레쮸고',\n", + " '아니..왜이리 귀여워..ㅠ',\n", + " '마이크 드랍마저 귀여운 로로',\n", + " '너무 예뻐요❤️❤️😍',\n", + " '25 May',\n", + " '언니또보고싶어요...💕💕🧎🏻\\u200d♀️',\n", + " '한로로는 전설이다.',\n", + " '결혼해 조요',\n", + " '귀욥다',\n", + " '혹시..진짜 물어볼곳이 없어서 그런데 한로로님 앨범 재발매는 안될수도있는걸까요..?타팬이라 진짜 아는게 없는데..입덕직전이라..답글받으면 댓 지우겠습니다..',\n", + " '기여워😍',\n", + " '공듀 ㅠㅠ',\n", + " '25 May'],\n", + " 'https://www.instagram.com/hanr0r0/p/DKZdlURS6fF/': ['귀요미 로로',\n", + " '사랑해 누나😍😍😍',\n", + " '귀여워 💙💙💙💙💙',\n", + " '@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠...',\n", + " '@joon_wall 사랑하게되',\n", + " '여름로로 너무 조아…😭',\n", + " '생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ',\n", + " '“인스타용“',\n", + " '여름 로로 조아 ☀️🍉☘️',\n", + " '귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로',\n", + " '노래 참 조타',\n", + " '한로로의 화해도 좋아요',\n", + " '사람이 이렇게 귀여울 수가 있냐고오',\n", + " '로로씨 어디강아지에요',\n", + " '로로님 6월도 파이팅이에요 😚',\n", + " '2 June',\n", + " '귀요미 로로',\n", + " '사랑해 누나😍😍😍',\n", + " '귀여워 💙💙💙💙💙',\n", + " '@big.mac__killer 로로님 제주도 오시면 저희한테 말 해주셔야죠...',\n", + " '@joon_wall 사랑하게되',\n", + " '여름로로 너무 조아…😭',\n", + " '생일 최고의 선물 한로로의 새 게시글 알람 ㄷㄷ',\n", + " '“인스타용“',\n", + " '여름 로로 조아 ☀️🍉☘️',\n", + " '귀여움의 대명사, 귀여움의 신, 귀여움의 전설, 귀여움의 권위자, 귀여움의 지배자, 귀여움의 표본, 귀여움의 정석, 귀여움의 군림자, 귀여움의 대명사, 귀여움의 황제, 귀여움의 종결자. 바로 한 로 로',\n", + " '노래 참 조타',\n", + " '한로로의 화해도 좋아요',\n", + " '사람이 이렇게 귀여울 수가 있냐고오',\n", + " '로로씨 어디강아지에요',\n", + " '로로님 6월도 파이팅이에요 😚',\n", + " '2 June'],\n", + " 'https://www.instagram.com/hanr0r0/p/DL1uYy8SRkH/': ['hanr0r0 3 d도망 라이브 클립 봐 주면 안 되낭?',\n", + " '되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지',\n", + " '😍😍😍',\n", + " '기대된당',\n", + " '챙겨볼게요~ 로로님🤍🐰',\n", + " '당장 달려가',\n", + " '더 줘…',\n", + " '한지수씨 브이하는척 눈찌르기 하시면 않되요',\n", + " 'ㅇㅋ접수',\n", + " '도망보러 도망쳐~~🏃🏃',\n", + " '우리 로로가 해달라는데 당연이봐야지^^말만하렴',\n", + " '아 쌉가능이요',\n", + " '당연이 되지.',\n", + " '우리 로로가 봐달라는데 당연히 봐줄 수 있지!',\n", + " '알았당',\n", + " '@khshin_ @kyunnxn_',\n", + " '3 days ago',\n", + " 'hanr0r0\\xa04 d도망 라이브 클립 봐 주면 안 되낭?',\n", + " '되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지되지',\n", + " '😍😍😍',\n", + " '기대된당',\n", + " '챙겨볼게요~ 로로님🤍🐰',\n", + " '당장 달려가',\n", + " '더 줘…',\n", + " '한지수씨 브이하는척 눈찌르기 하시면 않되요',\n", + " '도망보러 도망쳐~~🏃🏃',\n", + " 'ㅇㅋ접수',\n", + " '우리 로로가 해달라는데 당연이봐야지^^말만하렴',\n", + " '아 쌉가능이요',\n", + " '당연이 되지.',\n", + " '우리 로로가 봐달라는데 당연히 봐줄 수 있지!',\n", + " '알았당',\n", + " '제가 봐드릴게요',\n", + " '4 days ago'],\n", + " 'https://www.instagram.com/hanr0r0/p/DLZZJ3kSpa_/': ['hanr0r0\\xa02 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST)',\n", + " '도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다',\n", + " '느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자.',\n", + " '공주님',\n", + " '@yw.c0516 이거디',\n", + " '빛빛빛 3rd EP [빛빛빛빛빛빛]',\n", + " '온다 🔥🔥',\n", + " '드디어 여름선물? 자몽살구클럽 가보자잇❤️',\n", + " '얼굴좀보여조언니',\n", + " '뽀 로 로',\n", + " '빛이나다 못해 빛이 되어버린…',\n", + " '큰거온다🔥🔥',\n", + " 'Omg 다살자...',\n", + " '대 로 로 🔥🔥',\n", + " '결혼해요?',\n", + " '27 June',\n", + " 'hanr0r0\\xa02 w⠀HANRORO 3rd EP [자몽살구클럽] Official Concept PhotoPre-release Single ’도망 (Can I Be Me?)‘2025.7.6. SUN. 12PM (KST)',\n", + " '도망의 영어 부제가 ‘Can I be me’라니.. 너무 좋다',\n", + " '느좋의 대명사, 느좋의 신, 느좋의 전설, 느좋의 권위자, 느좋의 지배자, 느좋의 표본, 느좋의 정석, 느좋의 군림자, 느좋의 대명사, 느좋의 황제, 느좋의 종결자.',\n", + " '@yw.c0516 이거디',\n", + " '공주님',\n", + " '빛빛빛 3rd EP [빛빛빛빛빛빛]',\n", + " '온다 🔥🔥',\n", + " '드디어 여름선물? 자몽살구클럽 가보자잇❤️',\n", + " '얼굴좀보여조언니',\n", + " '뽀 로 로',\n", + " '빛이나다 못해 빛이 되어버린…',\n", + " '큰거온다🔥🔥',\n", + " 'Omg 다살자...',\n", + " '대 로 로 🔥🔥',\n", + " '결혼해요?',\n", + " '27 June']}" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import json\n", + "\n", + "with open(\"../../../test_comments.json\", \"r\", encoding=\"utf-8\") as f:\n", + " data = json.load(f)\n", + "data" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/music/test_workflow.py b/tests/music/test_workflow.py index bd6980b..3efe7a4 100644 --- a/tests/music/test_workflow.py +++ b/tests/music/test_workflow.py @@ -38,11 +38,11 @@ def test_music_workflow_initialization(music_workflow, mock_chatgpt): def test_music_workflow_build(music_workflow): """Workflow 그래프 구축 테스트""" graph = music_workflow.build() - + # 그래프가 성공적으로 생성되었는지 확인 assert graph is not None assert graph.name == "MusicWorkflow" - + # 노드가 올바르게 추가되었는지 확인 assert "generate_music" in graph.nodes @@ -50,21 +50,18 @@ def test_music_workflow_build(music_workflow): def test_music_workflow_execution(music_workflow, mock_chatgpt): """Workflow 실행 테스트""" # 테스트용 입력 상태 - initial_state = { - "query": "여름 바다를 주제로 한 가사", - "response": [] - } - + initial_state = {"query": "여름 바다를 주제로 한 가사", "response": []} + # Workflow 실행 graph = music_workflow.build() result = graph.invoke(initial_state) - + # 결과 검증 assert result is not None assert "response" in result assert "query" in result assert result["query"] == initial_state["query"] - + # ChatGPT가 호출되었는지 확인 mock_chatgpt.generate.assert_called_once() @@ -73,48 +70,44 @@ def test_music_workflow_error_handling(music_workflow, mock_chatgpt): """에러 처리 테스트""" # ChatGPT 에러 시뮬레이션 mock_chatgpt.generate.side_effect = Exception("API Error") - - initial_state = { - "query": "에러 테스트", - "response": [] - } - + + initial_state = {"query": "에러 테스트", "response": []} + # Workflow 실행 및 에러 처리 검증 graph = music_workflow.build() with pytest.raises(Exception) as exc_info: graph.invoke(initial_state) - + assert "API Error" in str(exc_info.value) def test_music_workflow_empty_query(music_workflow): """빈 쿼리 처리 테스트""" - initial_state = { - "query": "", - "response": [] - } - + initial_state = {"query": "", "response": []} + graph = music_workflow.build() result = graph.invoke(initial_state) - + assert result is not None assert result["query"] == "" -@pytest.mark.parametrize("test_input,expected", [ - ("여름 바다", "여름 바다"), - ("겨울 눈", "겨울 눈"), - ("가을 단풍", "가을 단풍"), -]) -def test_music_workflow_different_queries(music_workflow, mock_chatgpt, test_input, expected): +@pytest.mark.parametrize( + "test_input,expected", + [ + ("여름 바다", "여름 바다"), + ("겨울 눈", "겨울 눈"), + ("가을 단풍", "가을 단풍"), + ], +) +def test_music_workflow_different_queries( + music_workflow, mock_chatgpt, test_input, expected +): """다양한 쿼리에 대한 테스트""" - initial_state = { - "query": test_input, - "response": [] - } - + initial_state = {"query": test_input, "response": []} + graph = music_workflow.build() result = graph.invoke(initial_state) - + assert result["query"] == expected - mock_chatgpt.generate.assert_called() \ No newline at end of file + mock_chatgpt.generate.assert_called() diff --git a/uv.lock b/uv.lock index e1662db..780ccfe 100644 --- a/uv.lock +++ b/uv.lock @@ -4,11 +4,8 @@ requires-python = ">=3.13" [manifest] members = [ - "image", - "management", - "music", + "cast-instagram-comment", "pseudo-entertainment", - "text", ] [[package]] @@ -126,6 +123,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c8/57a4c80e5abec29fa9406307a5277527f21210bfc6c2c61c3d8ded36c09b/blockbuster-1.5.24-py3-none-any.whl", hash = "sha256:e703497b55bc72af09d60d1cd746c2f3ba7ce0c446fa256be6ccda5e7d403520", size = 13214, upload-time = "2025-03-18T10:12:04.802Z" }, ] +[[package]] +name = "cachetools" +version = "5.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, +] + +[[package]] +name = "cast-instagram-comment" +version = "0.1.0" +source = { virtual = "casts/cast_instagram_comment" } +dependencies = [ + { name = "langchain-google-genai" }, + { name = "pandas" }, +] + +[package.metadata] +requires-dist = [ + { name = "langchain-google-genai", specifier = ">=2.1.8" }, + { name = "pandas", specifier = ">=2.3.1" }, +] + [[package]] name = "certifi" version = "2025.1.31" @@ -309,15 +330,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "executing" version = "2.2.0" @@ -336,6 +348,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + [[package]] name = "forbiddenfruit" version = "0.1.4" @@ -366,6 +387,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901, upload-time = "2024-10-23T09:48:28.851Z" }, ] +[[package]] +name = "google-ai-generativelanguage" +version = "0.6.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/77/3e89a4c4200135eac74eca2f6c9153127e3719a825681ad55f5a4a58b422/google_ai_generativelanguage-0.6.18.tar.gz", hash = "sha256:274ba9fcf69466ff64e971d565884434388e523300afd468fc8e3033cd8e606e", size = 1444757, upload-time = "2025-04-29T15:45:45.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/77/ca2889903a2d93b3072a49056d48b3f55410219743e338a1d7f94dc6455e/google_ai_generativelanguage-0.6.18-py3-none-any.whl", hash = "sha256:13d8174fea90b633f520789d32df7b422058fd5883b022989c349f1017db7fcf", size = 1372256, upload-time = "2025-04-29T15:45:43.601Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.40.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, +] + [[package]] name = "greenlet" version = "3.2.0" @@ -391,6 +475,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/7b/773a30602234597fc2882091f8e1d1a38ea0b4419d99ca7ed82c827e2c3a/greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", size = 269908, upload-time = "2025-04-15T16:20:33.58Z" }, ] +[[package]] +name = "grpcio" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" }, + { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/22/238c5f01e6837df54494deb08d5c772bc3f5bf5fb80a15dce254892d1a81/grpcio_status-1.74.0.tar.gz", hash = "sha256:c58c1b24aa454e30f1fc6a7e0dbbc194c54a408143971a94b5f4e40bb5831432", size = 13662, upload-time = "2025-07-24T19:01:56.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/aa/1b1fe7d8ab699e1ec26d3a36b91d3df9f83a30abc07d4c881d0296b17b67/grpcio_status-1.74.0-py3-none-any.whl", hash = "sha256:52cdbd759a6760fc8f668098a03f208f493dd5c76bf8e02598bbbaf1f6fc2876", size = 14425, upload-time = "2025-07-24T19:01:19.963Z" }, +] + [[package]] name = "h11" version = "0.14.0" @@ -455,11 +571,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] -[[package]] -name = "image" -version = "0.1.0" -source = { virtual = "agents/image" } - [[package]] name = "iniconfig" version = "2.1.0" @@ -538,29 +649,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, ] -[[package]] -name = "jiter" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/1b/4cd165c362e8f2f520fdb43245e2b414f42a255921248b4f8b9c8d871ff1/jiter-0.9.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2764891d3f3e8b18dce2cff24949153ee30c9239da7c00f032511091ba688ff7", size = 308197, upload-time = "2025-03-10T21:36:03.828Z" }, - { url = "https://files.pythonhosted.org/packages/13/aa/7a890dfe29c84c9a82064a9fe36079c7c0309c91b70c380dc138f9bea44a/jiter-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:387b22fbfd7a62418d5212b4638026d01723761c75c1c8232a8b8c37c2f1003b", size = 318160, upload-time = "2025-03-10T21:36:05.281Z" }, - { url = "https://files.pythonhosted.org/packages/6a/38/5888b43fc01102f733f085673c4f0be5a298f69808ec63de55051754e390/jiter-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d8da8629ccae3606c61d9184970423655fb4e33d03330bcdfe52d234d32f69", size = 341259, upload-time = "2025-03-10T21:36:06.716Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5e/bbdbb63305bcc01006de683b6228cd061458b9b7bb9b8d9bc348a58e5dc2/jiter-0.9.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1be73d8982bdc278b7b9377426a4b44ceb5c7952073dd7488e4ae96b88e1103", size = 363730, upload-time = "2025-03-10T21:36:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/75/85/53a3edc616992fe4af6814c25f91ee3b1e22f7678e979b6ea82d3bc0667e/jiter-0.9.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2228eaaaa111ec54b9e89f7481bffb3972e9059301a878d085b2b449fbbde635", size = 405126, upload-time = "2025-03-10T21:36:10.934Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b3/1ee26b12b2693bd3f0b71d3188e4e5d817b12e3c630a09e099e0a89e28fa/jiter-0.9.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:11509bfecbc319459647d4ac3fd391d26fdf530dad00c13c4dadabf5b81f01a4", size = 393668, upload-time = "2025-03-10T21:36:12.468Z" }, - { url = "https://files.pythonhosted.org/packages/11/87/e084ce261950c1861773ab534d49127d1517b629478304d328493f980791/jiter-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f22238da568be8bbd8e0650e12feeb2cfea15eda4f9fc271d3b362a4fa0604d", size = 352350, upload-time = "2025-03-10T21:36:14.148Z" }, - { url = "https://files.pythonhosted.org/packages/f0/06/7dca84b04987e9df563610aa0bc154ea176e50358af532ab40ffb87434df/jiter-0.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17f5d55eb856597607562257c8e36c42bc87f16bef52ef7129b7da11afc779f3", size = 384204, upload-time = "2025-03-10T21:36:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/16/2f/82e1c6020db72f397dd070eec0c85ebc4df7c88967bc86d3ce9864148f28/jiter-0.9.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6a99bed9fbb02f5bed416d137944419a69aa4c423e44189bc49718859ea83bc5", size = 520322, upload-time = "2025-03-10T21:36:17.016Z" }, - { url = "https://files.pythonhosted.org/packages/36/fd/4f0cd3abe83ce208991ca61e7e5df915aa35b67f1c0633eb7cf2f2e88ec7/jiter-0.9.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e057adb0cd1bd39606100be0eafe742de2de88c79df632955b9ab53a086b3c8d", size = 512184, upload-time = "2025-03-10T21:36:18.47Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3c/8a56f6d547731a0b4410a2d9d16bf39c861046f91f57c98f7cab3d2aa9ce/jiter-0.9.0-cp313-cp313-win32.whl", hash = "sha256:f7e6850991f3940f62d387ccfa54d1a92bd4bb9f89690b53aea36b4364bcab53", size = 206504, upload-time = "2025-03-10T21:36:19.809Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1c/0c996fd90639acda75ed7fa698ee5fd7d80243057185dc2f63d4c1c9f6b9/jiter-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:c8ae3bf27cd1ac5e6e8b7a27487bf3ab5f82318211ec2e1346a5b058756361f7", size = 204943, upload-time = "2025-03-10T21:36:21.536Z" }, - { url = "https://files.pythonhosted.org/packages/78/0f/77a63ca7aa5fed9a1b9135af57e190d905bcd3702b36aca46a01090d39ad/jiter-0.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0b2827fb88dda2cbecbbc3e596ef08d69bda06c6f57930aec8e79505dc17001", size = 317281, upload-time = "2025-03-10T21:36:22.959Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/a3a1571712c2bf6ec4c657f0d66da114a63a2e32b7e4eb8e0b83295ee034/jiter-0.9.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:062b756ceb1d40b0b28f326cba26cfd575a4918415b036464a52f08632731e5a", size = 350273, upload-time = "2025-03-10T21:36:24.414Z" }, - { url = "https://files.pythonhosted.org/packages/ee/47/3729f00f35a696e68da15d64eb9283c330e776f3b5789bac7f2c0c4df209/jiter-0.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6f7838bc467ab7e8ef9f387bd6de195c43bad82a569c1699cb822f6609dd4cdf", size = 206867, upload-time = "2025-03-10T21:36:25.843Z" }, -] - [[package]] name = "jsonpatch" version = "1.33" @@ -670,7 +758,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "0.3.52" +version = "0.3.74" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -681,23 +769,24 @@ dependencies = [ { name = "tenacity" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/5e/55fe9d77fce032191012454297ce19c4fdfb3801f7887a4907e923cd8886/langchain_core-0.3.52.tar.gz", hash = "sha256:f1981ec9efa4fceb11ff5ca57f5f9c8e22859cea3a94f8a044e6de8815afbd57", size = 552963, upload-time = "2025-04-15T16:28:30.174Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/c6/5d755a0f1f4857abbe5ea6f5907ed0e2b5df52bf4dde0a0fd768290e3084/langchain_core-0.3.74.tar.gz", hash = "sha256:ff604441aeade942fbcc0a3860a592daba7671345230c2078ba2eb5f82b6ba76", size = 569553, upload-time = "2025-08-07T20:47:05.094Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/78/782a9e600377ca73339943e96776daf70bed38ddc69313dd769c505dad6f/langchain_core-0.3.52-py3-none-any.whl", hash = "sha256:cd137109c1e3d04f5a582c2cae9539b2cd5e4b795f486b58969dbc3d0387fe7c", size = 433586, upload-time = "2025-04-15T16:28:28.05Z" }, + { url = "https://files.pythonhosted.org/packages/4d/26/545283681ac0379d31c7ad0bac5f195e1982092d76c65ca048db9e3cec0e/langchain_core-0.3.74-py3-none-any.whl", hash = "sha256:088338b5bc2f6a66892f9afc777992c24ee3188f41cbc603d09181e34a228ce7", size = 443453, upload-time = "2025-08-07T20:47:03.853Z" }, ] [[package]] -name = "langchain-openai" -version = "0.3.13" +name = "langchain-google-genai" +version = "2.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "filetype" }, + { name = "google-ai-generativelanguage" }, { name = "langchain-core" }, - { name = "openai" }, - { name = "tiktoken" }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/b6/c8e86fa8fd3c6feab9c976da505e29abb50fc29fd323f869d14645d077a5/langchain_openai-0.3.13.tar.gz", hash = "sha256:75038efbf686f4b5fe2b6bdb75c43790d563ecd61984fd1d51d6d51c53609d64", size = 269759, upload-time = "2025-04-15T18:08:14.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/24/4ad44e9a8ad25682c22b56f0b665eb6d87090f2360355b48095e285a7810/langchain_google_genai-2.1.9.tar.gz", hash = "sha256:cd5d6f644b8dac3e312e30101bb97541aab240e82678e87a4df039ee1dc77531", size = 45866, upload-time = "2025-08-04T18:51:51.42Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/9d/be6d6a15b22a97209060f4f10d7160626bb173c4b986db373972c965b9c8/langchain_openai-0.3.13-py3-none-any.whl", hash = "sha256:2ca3f1865df32d03c3bd85c77f11f0ffd81b157b4e363291741c65c81463606a", size = 61691, upload-time = "2025-04-15T18:08:11.03Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/e1162835d5d6eefaae341c2d1cf750ab53222a421252346905187e53b8a2/langchain_google_genai-2.1.9-py3-none-any.whl", hash = "sha256:8d3aab59706b8f8920a22bcfd63c5000ce430fe61db6ecdec262977d1a0be5b8", size = 49381, upload-time = "2025-08-04T18:51:50.51Z" }, ] [[package]] @@ -828,7 +917,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.3.31" +version = "0.3.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -839,16 +928,11 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d0/59101fe1ad16a914b1ebde82c2b24524872dae5feff2d5b7405ab3b82f47/langsmith-0.3.31.tar.gz", hash = "sha256:8d20bd08fa6c3bce54cb600ddc521cd218a1c3410f90d9266179bf83a7ff0897", size = 343600, upload-time = "2025-04-15T00:44:05.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/86/b941012013260f95af2e90a3d9415af4a76a003a28412033fc4b09f35731/langsmith-0.3.45.tar.gz", hash = "sha256:1df3c6820c73ed210b2c7bc5cdb7bfa19ddc9126cd03fdf0da54e2e171e6094d", size = 348201, upload-time = "2025-06-05T05:10:28.948Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/c3/5db3d0977bb53e16eab834f2eea6e1c68d327e2f5c25b88f6506ef06e692/langsmith-0.3.31-py3-none-any.whl", hash = "sha256:ee780ae3eac69998c336817c0b9f5ccfecaaaa3e67d94b7ef726b58ab3e72a25", size = 358251, upload-time = "2025-04-15T00:44:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f4/c206c0888f8a506404cb4f16ad89593bdc2f70cf00de26a1a0a7a76ad7a3/langsmith-0.3.45-py3-none-any.whl", hash = "sha256:5b55f0518601fa65f3bb6b1a3100379a96aa7b3ed5e9380581615ba9c65ed8ed", size = 363002, upload-time = "2025-06-05T05:10:27.228Z" }, ] -[[package]] -name = "management" -version = "0.1.0" -source = { virtual = "agents/management" } - [[package]] name = "marshmallow" version = "3.26.1" @@ -916,11 +1000,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" }, ] -[[package]] -name = "music" -version = "0.1.0" -source = { virtual = "agents/music" } - [[package]] name = "mypy-extensions" version = "1.0.0" @@ -976,25 +1055,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119, upload-time = "2025-03-16T18:20:03.94Z" }, ] -[[package]] -name = "openai" -version = "1.74.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/86/c605a6e84da0248f2cebfcd864b5a6076ecf78849245af5e11d2a5ec7977/openai-1.74.0.tar.gz", hash = "sha256:592c25b8747a7cad33a841958f5eb859a785caea9ee22b9e4f4a2ec062236526", size = 427571, upload-time = "2025-04-14T16:45:25.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/91/8c150f16a96367e14bd7d20e86e0bbbec3080e3eb593e63f21a7f013f8e4/openai-1.74.0-py3-none-any.whl", hash = "sha256:aff3e0f9fb209836382ec112778667027f4fd6ae38bdb2334bc9e173598b092a", size = 644790, upload-time = "2025-04-14T16:45:23.041Z" }, -] - [[package]] name = "orjson" version = "3.10.16" @@ -1042,6 +1102,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, ] +[[package]] +name = "pandas" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, +] + [[package]] name = "parso" version = "0.8.4" @@ -1150,15 +1237,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" }, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, +] + +[[package]] +name = "protobuf" +version = "6.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/df/fb4a8eeea482eca989b51cffd274aac2ee24e825f0bf3cbce5281fa1567b/protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2", size = 440614, upload-time = "2025-08-14T21:21:25.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/18/df8c87da2e47f4f1dcc5153a81cd6bca4e429803f4069a299e236e4dd510/protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741", size = 424409, upload-time = "2025-08-14T21:21:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/e1/59/0a820b7310f8139bd8d5a9388e6a38e1786d179d6f33998448609296c229/protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e", size = 435735, upload-time = "2025-08-14T21:21:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5b/0d421533c59c789e9c9894683efac582c06246bf24bb26b753b149bd88e4/protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0", size = 426449, upload-time = "2025-08-14T21:21:16.687Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/607764ebe6c7a23dcee06e054fd1de3d5841b7648a90fd6def9a3bb58c5e/protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1", size = 322869, upload-time = "2025-08-14T21:21:18.282Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/2e730bd1c25392fc32e3268e02446f0d77cb51a2c3a8486b1798e34d5805/protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c", size = 322009, upload-time = "2025-08-14T21:21:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f2/80ffc4677aac1bc3519b26bc7f7f5de7fce0ee2f7e36e59e27d8beb32dd1/protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783", size = 169287, upload-time = "2025-08-14T21:21:23.515Z" }, +] + [[package]] name = "pseudo-entertainment" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "ipykernel" }, { name = "langchain" }, { name = "langchain-community" }, + { name = "langchain-google-genai" }, { name = "langgraph" }, + { name = "pandas" }, + { name = "pydantic" }, { name = "python-dotenv" }, + { name = "ruff" }, + { name = "schedule" }, ] [package.dev-dependencies] @@ -1171,10 +1290,16 @@ dev = [ [package.metadata] requires-dist = [ + { name = "ipykernel", specifier = ">=6.29.5" }, { name = "langchain", specifier = ">=0.3.23" }, { name = "langchain-community", specifier = ">=0.2.17" }, + { name = "langchain-google-genai", specifier = ">=2.1.8" }, { name = "langgraph", specifier = ">=0.3.27" }, + { name = "pandas", specifier = ">=2.3.1" }, + { name = "pydantic", specifier = ">=2.0.0" }, { name = "python-dotenv", specifier = ">=1.0.1" }, + { name = "ruff", specifier = ">=0.12.9" }, + { name = "schedule", specifier = ">=1.2.2" }, ] [package.metadata.requires-dev] @@ -1218,6 +1343,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "2.22" @@ -1337,6 +1483,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, ] +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "310" @@ -1394,29 +1549,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/4c/bf3cad0d64c3214ac881299c4562b815f05d503bccc513e3fd4fdc6f67e4/pyzmq-26.4.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:26a2a7451606b87f67cdeca2c2789d86f605da08b4bd616b1a9981605ca3a364", size = 1395540, upload-time = "2025-04-04T12:04:30.562Z" }, ] -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494, upload-time = "2024-11-06T20:12:31.635Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525, upload-time = "2024-11-06T20:10:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324, upload-time = "2024-11-06T20:10:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617, upload-time = "2024-11-06T20:10:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023, upload-time = "2024-11-06T20:10:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072, upload-time = "2024-11-06T20:10:52.926Z" }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130, upload-time = "2024-11-06T20:10:54.828Z" }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857, upload-time = "2024-11-06T20:10:56.634Z" }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006, upload-time = "2024-11-06T20:10:59.369Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650, upload-time = "2024-11-06T20:11:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545, upload-time = "2024-11-06T20:11:03.933Z" }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045, upload-time = "2024-11-06T20:11:06.497Z" }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182, upload-time = "2024-11-06T20:11:09.06Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733, upload-time = "2024-11-06T20:11:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122, upload-time = "2024-11-06T20:11:13.161Z" }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545, upload-time = "2024-11-06T20:11:15Z" }, -] - [[package]] name = "requests" version = "2.32.3" @@ -1444,6 +1576,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "schedule" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/91/b525790063015759f34447d4cf9d2ccb52cdee0f1dd6ff8764e863bcb74c/schedule-1.2.2.tar.gz", hash = "sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7", size = 26452, upload-time = "2024-06-18T20:03:14.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/a7/84c96b61fd13205f2cafbe263cdb2745965974bdf3e0078f121dfeca5f02/schedule-1.2.2-py3-none-any.whl", hash = "sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d", size = 12220, upload-time = "2024-05-25T18:41:59.121Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1541,35 +1720,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] -[[package]] -name = "text" -version = "0.1.0" -source = { virtual = "agents/text" } -dependencies = [ - { name = "langchain-openai" }, -] - -[package.metadata] -requires-dist = [{ name = "langchain-openai", specifier = ">=0.3.12" }] - -[[package]] -name = "tiktoken" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919, upload-time = "2025-02-14T06:02:37.494Z" }, - { url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877, upload-time = "2025-02-14T06:02:39.516Z" }, - { url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095, upload-time = "2025-02-14T06:02:41.791Z" }, - { url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649, upload-time = "2025-02-14T06:02:43Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465, upload-time = "2025-02-14T06:02:45.046Z" }, - { url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" }, -] - [[package]] name = "tornado" version = "6.4.2" @@ -1588,18 +1738,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/cc/58b1adeb1bb46228442081e746fcdbc4540905c87e8add7c277540934edb/tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38", size = 438907, upload-time = "2024-11-22T03:06:36.71Z" }, ] -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, -] - [[package]] name = "traitlets" version = "5.14.3" @@ -1643,6 +1781,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "urllib3" version = "2.4.0"