Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- .gitignore +11 -0
- .python-version +1 -0
- README.md +2 -8
- app.py +460 -0
- etf_database.db +3 -0
- main.py +6 -0
- pyproject.toml +14 -0
- requirements.txt +6 -0
- uv.lock +0 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
etf_database.db filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python-generated files
|
2 |
+
__pycache__/
|
3 |
+
*.py[oc]
|
4 |
+
build/
|
5 |
+
dist/
|
6 |
+
wheels/
|
7 |
+
*.egg-info
|
8 |
+
|
9 |
+
# Virtual environments
|
10 |
+
.venv
|
11 |
+
.env
|
.python-version
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
3.12
|
README.md
CHANGED
@@ -1,12 +1,6 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
|
4 |
-
colorFrom: indigo
|
5 |
-
colorTo: indigo
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.43.1
|
8 |
-
app_file: app.py
|
9 |
-
pinned: false
|
10 |
---
|
11 |
-
|
12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: gradio-demo-test
|
3 |
+
app_file: app.py
|
|
|
|
|
4 |
sdk: gradio
|
5 |
sdk_version: 5.43.1
|
|
|
|
|
6 |
---
|
|
|
|
app.py
ADDED
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from enum import Enum
|
2 |
+
from typing import List, TypedDict, Annotated
|
3 |
+
from pydantic import BaseModel, Field
|
4 |
+
from decimal import Decimal
|
5 |
+
import ast
|
6 |
+
import re
|
7 |
+
|
8 |
+
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
|
9 |
+
from langchain_core.prompts import ChatPromptTemplate
|
10 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
11 |
+
from langchain_core.vectorstores import InMemoryVectorStore
|
12 |
+
from langchain_community.tools import QuerySQLDatabaseTool
|
13 |
+
from langchain_community.utilities import SQLDatabase
|
14 |
+
from langchain_ollama import OllamaEmbeddings
|
15 |
+
from langchain.agents.agent_toolkits import create_retriever_tool
|
16 |
+
|
17 |
+
from langgraph.graph import START, StateGraph
|
18 |
+
|
19 |
+
import gradio as gr
|
20 |
+
|
21 |
+
|
22 |
+
##################################################################
|
23 |
+
# ํ๊ฒฝ ์ค์ / ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
|
24 |
+
##################################################################
|
25 |
+
from dotenv import load_dotenv
|
26 |
+
load_dotenv()
|
27 |
+
|
28 |
+
db = SQLDatabase.from_uri("sqlite:///etf_database.db")
|
29 |
+
|
30 |
+
##################################################################
|
31 |
+
# ๊ณ ์ ๋ช
์ฌ DB ๊ฒ์
|
32 |
+
##################################################################
|
33 |
+
|
34 |
+
def query_as_list(db, query):
|
35 |
+
res = db.run(query)
|
36 |
+
res = [el for sub in ast.literal_eval(res) for el in sub if el]
|
37 |
+
res = [re.sub(r"\b\d+\b", "", string).strip() for string in res]
|
38 |
+
return list(set(res))
|
39 |
+
|
40 |
+
etfs = query_as_list(db, "SELECT DISTINCT ์ข
๋ชฉ๋ช
FROM ETFs")
|
41 |
+
fund_managers = query_as_list(db, "SELECT DISTINCT ์ด์ฉ์ฌ FROM ETFs")
|
42 |
+
underlying_assets = query_as_list(db, "SELECT DISTINCT ๊ธฐ์ด์ง์ FROM ETFs")
|
43 |
+
|
44 |
+
# ์๋ฒ ๋ฉ ๋ชจ๋ธ ์์ฑ
|
45 |
+
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
|
46 |
+
|
47 |
+
# ์๋ฒ ๋ฉ ๋ฒกํฐ ์ ์ฅ์ ์์ฑ
|
48 |
+
vector_store = InMemoryVectorStore(embeddings)
|
49 |
+
|
50 |
+
# ETF ์ข
๋ชฉ๋ช
๊ณผ ์ด์ฉ์ฌ๋ฅผ ์๋ฒ ๋ฉ ๋ฒกํฐ๋ก ๋ณํ
|
51 |
+
_ = vector_store.add_texts(etfs + fund_managers + underlying_assets)
|
52 |
+
retriever = vector_store.as_retriever(search_kwargs={"k": 10})
|
53 |
+
|
54 |
+
# ๊ฒ์ ํ๋กฌํํธ ์์ฑ
|
55 |
+
description = (
|
56 |
+
"Use to look up values to filter on. Input is an approximate spelling "
|
57 |
+
"of the proper noun, output is valid proper nouns. Use the noun most "
|
58 |
+
"similar to the search."
|
59 |
+
)
|
60 |
+
|
61 |
+
# ๊ฒ์ ๋๊ตฌ ์์ฑ
|
62 |
+
entity_retriever_tool = create_retriever_tool(
|
63 |
+
retriever,
|
64 |
+
name="search_proper_nouns",
|
65 |
+
description=description,
|
66 |
+
)
|
67 |
+
|
68 |
+
##################################################################
|
69 |
+
# ์ํ ์ ๋ณด ํ์
์ ์
|
70 |
+
##################################################################
|
71 |
+
class State(TypedDict):
|
72 |
+
question: str # ์ฌ์ฉ์ ์
๋ ฅ ์ง๋ฌธ
|
73 |
+
user_profile: dict # ์ฌ์ฉ์ ํ๋กํ ์ ๋ณด
|
74 |
+
query: str # ์์ฑ๋ SQL ์ฟผ๋ฆฌ
|
75 |
+
candidates: list # ํ๋ณด ETF ๋ชฉ๋ก
|
76 |
+
rankings: list # ์์๊ฐ ๋งค๊ฒจ์ง ETF ๋ชฉ๋ก
|
77 |
+
explanation: str # ์ถ์ฒ ์ด์ ์ค๋ช
|
78 |
+
final_answer: str # ์ต์ข
์ถ์ฒ ๋ต๋ณ
|
79 |
+
|
80 |
+
|
81 |
+
|
82 |
+
##################################################################
|
83 |
+
# ์ฌ์ฉ์ ํ๋กํ ๋ถ์
|
84 |
+
##################################################################
|
85 |
+
class RiskTolerance(str, Enum):
|
86 |
+
CONSERVATIVE = "conservative"
|
87 |
+
MODERATE = "moderate"
|
88 |
+
AGGRESSIVE = "aggressive"
|
89 |
+
|
90 |
+
class InvestmentHorizon(str, Enum):
|
91 |
+
SHORT = "short"
|
92 |
+
MEDIUM = "medium"
|
93 |
+
LONG = "long"
|
94 |
+
|
95 |
+
class InvestmentProfile(BaseModel):
|
96 |
+
risk_tolerance: RiskTolerance = Field(
|
97 |
+
description="ํฌ์์์ ์ํ ์ฑํฅ (conservative/moderate/aggressive)"
|
98 |
+
)
|
99 |
+
investment_horizon: InvestmentHorizon = Field(
|
100 |
+
description="ํฌ์ ๊ธฐ๊ฐ (short/medium/long)"
|
101 |
+
)
|
102 |
+
investment_goal: str = Field(
|
103 |
+
description="ํฌ์์ ์ฃผ์ ๋ชฉ์ ์ค๋ช
"
|
104 |
+
)
|
105 |
+
preferred_sectors: List[str] = Field(
|
106 |
+
description="์ ํธํ๋ ํฌ์ ์นํฐ ๋ชฉ๋ก"
|
107 |
+
)
|
108 |
+
excluded_sectors: List[str] = Field(
|
109 |
+
description="ํฌ์๋ฅผ ์ํ์ง ์๋ ์นํฐ ๋ชฉ๋ก"
|
110 |
+
)
|
111 |
+
monthly_investment: int = Field(
|
112 |
+
description="์ ํฌ์ ๊ฐ๋ฅ ๊ธ์ก (์)"
|
113 |
+
)
|
114 |
+
|
115 |
+
|
116 |
+
# ์ฌ์ฉ์ ํ๋กํ ๋ถ์ ํ๋กฌํํธ
|
117 |
+
PROFILE_TEMPLATE= """
|
118 |
+
์ฌ์ฉ์์ ์ง๋ฌธ์ ๋ถ์ํ์ฌ ํฌ์ ํ๋กํ์ ์์ฑํด์ฃผ์ธ์.
|
119 |
+
|
120 |
+
์ฌ์ฉ์ ์ง๋ฌธ: {question}
|
121 |
+
"""
|
122 |
+
|
123 |
+
profile_prompt = ChatPromptTemplate.from_template(PROFILE_TEMPLATE)
|
124 |
+
|
125 |
+
# ์ฌ์ฉ์ ํ๋กํ ๋ถ์ ๋ชจ๋ธ ์์ฑ
|
126 |
+
llm = ChatOpenAI(model="gpt-4.1-mini")
|
127 |
+
profile_llm = llm.with_structured_output(InvestmentProfile)
|
128 |
+
|
129 |
+
# ์ฌ์ฉ์ ํ๋กํ ๋ถ์ ํจ์
|
130 |
+
def analyze_profile(state: State) -> dict:
|
131 |
+
"""์ฌ์ฉ์ ์ง๋ฌธ์ ๋ถ์ํ์ฌ ํฌ์ ํ๋กํ ์์ฑ"""
|
132 |
+
prompt = profile_prompt.invoke({"question": state["question"]})
|
133 |
+
response = profile_llm.invoke(prompt)
|
134 |
+
return {"user_profile": dict(response)}
|
135 |
+
|
136 |
+
|
137 |
+
##################################################################
|
138 |
+
# SQL ์ฟผ๋ฆฌ ์์ฑ
|
139 |
+
##################################################################
|
140 |
+
|
141 |
+
# SQL Query Generation Template
|
142 |
+
QUERY_TEMPLATE = """
|
143 |
+
Given an input question and investment profile, create a syntactically correct {dialect} query to run. Unless specified, limit your query to at most {top_k} results. Order the results by most relevant columns based on the investment profile.
|
144 |
+
|
145 |
+
Never query for all columns from a specific table, only ask for relevant columns given the question and investment criteria.
|
146 |
+
|
147 |
+
Pay attention to use only the column names you can see in the schema description. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.
|
148 |
+
|
149 |
+
Available tables:
|
150 |
+
{table_info}
|
151 |
+
|
152 |
+
Entity relationships:
|
153 |
+
{entity_info}
|
154 |
+
- Use exact matches when comparing entity names
|
155 |
+
- Check for historical name variations if available
|
156 |
+
- Apply case-sensitive matching for official names
|
157 |
+
- Handle both Korean and English entity names when present
|
158 |
+
|
159 |
+
Investment Profile:
|
160 |
+
{user_profile}
|
161 |
+
|
162 |
+
Question: {input}
|
163 |
+
|
164 |
+
Important:
|
165 |
+
1. Use only existing columns
|
166 |
+
2. Query only necessary columns (no SELECT *)
|
167 |
+
3. Follow correct table relationships
|
168 |
+
4. Consider performance and indexing
|
169 |
+
"""
|
170 |
+
|
171 |
+
# SQL Query Generation Prompt Template
|
172 |
+
query_prompt_template = ChatPromptTemplate.from_template(QUERY_TEMPLATE)
|
173 |
+
|
174 |
+
# SQL Query Output
|
175 |
+
class QueryOutput(TypedDict):
|
176 |
+
"""Generated SQL query."""
|
177 |
+
query: Annotated[str, ..., "Syntactically valid SQL query."]
|
178 |
+
explanation: Annotated[str, ..., "Query explanation and selection criteria (in ํ๊ตญ์ด)"]
|
179 |
+
|
180 |
+
|
181 |
+
def write_query(state: State):
|
182 |
+
"""Generate SQL query to fetch information."""
|
183 |
+
prompt = query_prompt_template.invoke(
|
184 |
+
{
|
185 |
+
"dialect": db.dialect,
|
186 |
+
"top_k": 10,
|
187 |
+
"table_info": db.get_table_info(),
|
188 |
+
"input": state["question"],
|
189 |
+
"entity_info": entity_retriever_tool.invoke(state["question"]),
|
190 |
+
"user_profile": state["user_profile"],
|
191 |
+
}
|
192 |
+
)
|
193 |
+
structured_llm = llm.with_structured_output(QueryOutput)
|
194 |
+
result = structured_llm.invoke(prompt)
|
195 |
+
return {"query": result["query"], "explanation": result["explanation"]}
|
196 |
+
|
197 |
+
|
198 |
+
##################################################################
|
199 |
+
# ํ๋ณด ETF ๊ฒ์
|
200 |
+
##################################################################
|
201 |
+
|
202 |
+
def execute_query(state: State) -> dict:
|
203 |
+
"""SQL ์ฟผ๋ฆฌ ์คํํ์ฌ ํ๋ณด ETF ๊ฒ์"""
|
204 |
+
execute_query_tool = QuerySQLDatabaseTool(db=db)
|
205 |
+
results = execute_query_tool.invoke(state["query"])
|
206 |
+
return {"candidates": results}
|
207 |
+
|
208 |
+
##################################################################
|
209 |
+
# ETF ์์ ๋งค๊ธฐ๊ธฐ
|
210 |
+
##################################################################
|
211 |
+
|
212 |
+
RANKING_TEMPLATE = """
|
213 |
+
Rank the following ETF candidates based on the user's investment profile and return the top 3(three) ETFs.
|
214 |
+
Consider these factors when ranking:
|
215 |
+
|
216 |
+
1. ์์ต๋ฅ
|
217 |
+
2. ๋ณ๋์ฑ
|
218 |
+
3. ์์์ฐ์ด์ก
|
219 |
+
4. ๋ณ๋์ฑ
|
220 |
+
5. User Profile matching score
|
221 |
+
|
222 |
+
User Profile:
|
223 |
+
{user_profile}
|
224 |
+
|
225 |
+
Candidate ETFs:
|
226 |
+
{candidates}
|
227 |
+
"""
|
228 |
+
|
229 |
+
# ETF Ranking Prompt Template
|
230 |
+
ranking_prompt = ChatPromptTemplate.from_template(RANKING_TEMPLATE)
|
231 |
+
|
232 |
+
# ETF Ranking Output
|
233 |
+
class ETFRanking(TypedDict):
|
234 |
+
"""Individual ETF ranking result"""
|
235 |
+
rank: Annotated[int, ..., "Ranking position (1-5)"]
|
236 |
+
etf_code: Annotated[str, ..., "ETF ์ข
๋ชฉ์ฝ๋ (6-digit)"]
|
237 |
+
etf_name: Annotated[str, ..., "ETF ์ข
๋ชฉ๋ช
"]
|
238 |
+
score: Annotated[float, ..., "Composite score (0-100)"]
|
239 |
+
ranking_reason: Annotated[str, ..., "Explanation for the ranking (in ํ๊ตญ์ด)"]
|
240 |
+
|
241 |
+
class ETFRankingResult(TypedDict):
|
242 |
+
"""Ranked ETFs"""
|
243 |
+
rankings: List[ETFRanking]
|
244 |
+
|
245 |
+
# ETF Ranking Function
|
246 |
+
def rank_etfs(state: State) -> dict:
|
247 |
+
"""Rank ETF candidates based on user's investment profile"""
|
248 |
+
prompt = ranking_prompt.invoke(
|
249 |
+
{
|
250 |
+
"user_profile": state["user_profile"],
|
251 |
+
"candidates": state["candidates"],
|
252 |
+
}
|
253 |
+
)
|
254 |
+
structured_llm = llm.with_structured_output(ETFRankingResult)
|
255 |
+
results = structured_llm.invoke(prompt)
|
256 |
+
|
257 |
+
return {"rankings": results}
|
258 |
+
|
259 |
+
|
260 |
+
|
261 |
+
##################################################################
|
262 |
+
# ์ถ์ฒ ์ด์ ์ค๋ช
|
263 |
+
##################################################################
|
264 |
+
|
265 |
+
EXPLANATION_TEMPLATE = """
|
266 |
+
Please provide a comprehensive explanation for the recommended ETFs based on the user's investment profile.
|
267 |
+
|
268 |
+
|
269 |
+
[GUIDELINES]
|
270 |
+
1. ETF Characteristics
|
271 |
+
- Investment strategy and approach
|
272 |
+
- Historical performance overview
|
273 |
+
- Fee structure and efficiency
|
274 |
+
- Underlying assets and diversification
|
275 |
+
|
276 |
+
2. Profile Fit Analysis
|
277 |
+
- Alignment with risk tolerance
|
278 |
+
- Match with investment horizon
|
279 |
+
- Sector preference compatibility
|
280 |
+
- Investment goal contribution
|
281 |
+
|
282 |
+
3. Portfolio Construction
|
283 |
+
- Recommended allocation percentages
|
284 |
+
- Diversification benefits
|
285 |
+
- Rebalancing considerations
|
286 |
+
- Implementation strategy
|
287 |
+
|
288 |
+
4. Risk Considerations
|
289 |
+
- Market risk factors
|
290 |
+
- Specific ETF risks
|
291 |
+
- Economic scenario impacts
|
292 |
+
- Monitoring requirements
|
293 |
+
|
294 |
+
--------------------------------------------
|
295 |
+
|
296 |
+
[User Profile]
|
297 |
+
{user_profile}
|
298 |
+
|
299 |
+
[Selected ETFs]
|
300 |
+
{rankings}
|
301 |
+
"""
|
302 |
+
|
303 |
+
# ์ถ์ฒ ์ค๋ช
ํ๋กฌํํธ
|
304 |
+
explanation_prompt = ChatPromptTemplate.from_template(EXPLANATION_TEMPLATE)
|
305 |
+
|
306 |
+
|
307 |
+
# ๏ฟฝ๏ฟฝ์ฒ ์ค๋ช
์ถ๋ ฅ ์คํค๋ง
|
308 |
+
class ETFRecommendation(BaseModel):
|
309 |
+
"""Individual ETF recommendation details"""
|
310 |
+
etf_code: str = Field(..., description="ETF ์ข
๋ชฉ์ฝ๋ (6-digit)")
|
311 |
+
etf_name: str = Field(..., description="ETF ์ข
๋ชฉ๋ช
")
|
312 |
+
allocation: Decimal = Field(..., description="Recommended allocation % (0-100)")
|
313 |
+
description: str = Field(..., description="ETF description and investment strategy (in ํ๊ตญ์ด)")
|
314 |
+
key_points: List[str] = Field(..., description="Key investment points (in ํ๊ตญ์ด)")
|
315 |
+
risks: List[str] = Field(..., description="Risk factors to consider (in ํ๊ตญ์ด)")
|
316 |
+
|
317 |
+
class RecommendationExplanation(BaseModel):
|
318 |
+
"""ETF recommendation explanation with markdown formatting"""
|
319 |
+
overview: str = Field(..., description="Overall strategy explanation (in ํ๊ตญ์ด)")
|
320 |
+
recommendations: List[ETFRecommendation] = Field(..., description="ETF details")
|
321 |
+
considerations: List[str] = Field(..., description="Important considerations (in ํ๊ตญ์ด)")
|
322 |
+
|
323 |
+
# ๋งํฌ๋ค์ด ํฌ๋งท์ผ๋ก ์ถ๋ ฅ
|
324 |
+
def to_markdown(self) -> str:
|
325 |
+
"""Convert explanation to markdown format"""
|
326 |
+
markdown = [
|
327 |
+
"# ETF ํฌํธํด๋ฆฌ์ค ์ถ์ฒ",
|
328 |
+
"",
|
329 |
+
"## ํฌ์ ์ ๋ต ๊ฐ์",
|
330 |
+
self.overview,
|
331 |
+
"",
|
332 |
+
"## ์ถ์ฒ ETF ํฌํธํด๋ฆฌ์ค",
|
333 |
+
""
|
334 |
+
]
|
335 |
+
|
336 |
+
# ํฌํธํด๋ฆฌ์ค ๊ตฌ์ฑ ๋น์จ
|
337 |
+
markdown.extend([
|
338 |
+
"| ETF | ์ข
๋ชฉ์ฝ๋ | ์ถ์ฒ๋น์ค |",
|
339 |
+
"|-----|----------|----------|"
|
340 |
+
])
|
341 |
+
|
342 |
+
for rec in self.recommendations:
|
343 |
+
markdown.append(
|
344 |
+
f"| {rec.etf_name} | {rec.etf_code} | {rec.allocation}% |"
|
345 |
+
)
|
346 |
+
|
347 |
+
# ETF ์์ธ ์ค๋ช
|
348 |
+
markdown.append("\n## ETF ์์ธ ์ค๋ช
\n")
|
349 |
+
|
350 |
+
for rec in self.recommendations:
|
351 |
+
markdown.extend([
|
352 |
+
f"### {rec.etf_name} ({rec.etf_code})",
|
353 |
+
rec.description,
|
354 |
+
"",
|
355 |
+
"**์ฃผ์ ํฌ์ ํฌ์ธํธ:**",
|
356 |
+
"".join([f"\n* {point}" for point in rec.key_points]),
|
357 |
+
"",
|
358 |
+
"**ํฌ์ ์ํ:**",
|
359 |
+
"".join([f"\n* {risk}" for risk in rec.risks]),
|
360 |
+
""
|
361 |
+
])
|
362 |
+
|
363 |
+
# ํฌ์ ๋ฆฌ์คํฌ ๊ณ ๋ ค์ฌํญ
|
364 |
+
markdown.extend([
|
365 |
+
"## ํฌ์ ์ ๊ณ ๋ ค์ฌํญ",
|
366 |
+
"".join([f"\n* {item}" for item in self.considerations]),
|
367 |
+
""
|
368 |
+
])
|
369 |
+
|
370 |
+
return "\n".join(markdown)
|
371 |
+
|
372 |
+
|
373 |
+
# ์ถ์ฒ ์ค๋ช
์์ฑ ํจ์
|
374 |
+
def generate_explanation(state: dict) -> dict:
|
375 |
+
"""Generate structured ETF recommendation explanation"""
|
376 |
+
# ํ๋กฌํํธ ์์ฑ
|
377 |
+
prompt = explanation_prompt.invoke({
|
378 |
+
"rankings": state["rankings"],
|
379 |
+
"user_profile": state["user_profile"]
|
380 |
+
})
|
381 |
+
|
382 |
+
# ๊ตฌ์กฐํ๋ ์ถ๋ ฅ ์์ฑ
|
383 |
+
structured_llm = llm.with_structured_output(RecommendationExplanation)
|
384 |
+
response = structured_llm.invoke(prompt)
|
385 |
+
|
386 |
+
return {"final_answer": {
|
387 |
+
"explanation": response.model_dump(),
|
388 |
+
"markdown": response.to_markdown()
|
389 |
+
}}
|
390 |
+
|
391 |
+
|
392 |
+
##################################################################
|
393 |
+
# ETF ์ถ์ฒ ๋ด - ์ํ ๊ทธ๋ํ ์์ฑ
|
394 |
+
##################################################################
|
395 |
+
|
396 |
+
# ์ํ ๊ทธ๋ํ ์์ฑ
|
397 |
+
graph_builder = StateGraph(State).add_sequence(
|
398 |
+
[analyze_profile, write_query, execute_query, rank_etfs, generate_explanation]
|
399 |
+
)
|
400 |
+
|
401 |
+
graph_builder.add_edge(START, "analyze_profile")
|
402 |
+
graph = graph_builder.compile()
|
403 |
+
|
404 |
+
|
405 |
+
##################################################################
|
406 |
+
# ETF ์ถ์ฒ ๋ด - ๋ฉ์ธ ํจ์
|
407 |
+
##################################################################
|
408 |
+
|
409 |
+
def process_message(message: str) -> str:
|
410 |
+
|
411 |
+
try:
|
412 |
+
etf_recommendation = graph.invoke(
|
413 |
+
{"question": message}
|
414 |
+
)
|
415 |
+
return etf_recommendation["final_answer"]["markdown"]
|
416 |
+
|
417 |
+
except Exception as e:
|
418 |
+
return f"""
|
419 |
+
# ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค
|
420 |
+
์ฃ์กํฉ๋๋ค. ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
|
421 |
+
|
422 |
+
์ค๋ฅ ๋ด์ฉ: {str(e)}
|
423 |
+
|
424 |
+
๋ค์ ์๋ํด์ฃผ์๊ฑฐ๋, ์ง๋ฌธ์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์์ฑํด์ฃผ์ธ์.
|
425 |
+
"""
|
426 |
+
|
427 |
+
|
428 |
+
def answer_invoke(message: str, history: List) -> str:
|
429 |
+
return process_message(message) # ๋ฉ์์ง ์ฒ๋ฆฌ ํจ์ ํธ์ถ - ๋ํ ์ด๋ ฅ ๋ฏธ์ฌ์ฉ
|
430 |
+
|
431 |
+
# Create Gradio interface
|
432 |
+
demo = gr.ChatInterface(
|
433 |
+
fn=answer_invoke,
|
434 |
+
title="๋ง์ถคํ ETF ์ถ์ฒ ์ด์์คํดํธ",
|
435 |
+
description="""
|
436 |
+
ํฌ์ ์ฑํฅ๊ณผ ๋ชฉํ์ ๋ง๋ ETF๋ฅผ ์ถ์ฒํด๋๋ฆฝ๋๋ค.
|
437 |
+
|
438 |
+
๋ค์๊ณผ ๊ฐ์ ์ ๋ณด๋ฅผ ํฌํจํ์ฌ ์ง๋ฌธํด์ฃผ์ธ์:
|
439 |
+
- ํฌ์ ๋ชฉ์
|
440 |
+
- ํฌ์ ๊ธฐ๊ฐ
|
441 |
+
- ์ํ ์ฑํฅ
|
442 |
+
- ์ ํธ/์ ์ธ ์นํฐ
|
443 |
+
- ์ ํฌ์ ๊ฐ๋ฅ ๊ธ์ก
|
444 |
+
|
445 |
+
์์) "์ 100๋ง์ ์ ๋๋ฅผ 3๋
์ด์ ์ฅ๊ธฐ ํฌ์ํ๊ณ ์ถ๊ณ , IT์ ํฌ์ค์ผ์ด ์นํฐ๋ฅผ ์ ํธํฉ๋๋ค.
|
446 |
+
๋ณด์์ ์ธ ํฌ์๋ฅผ ์ ํธํ๋ฉฐ, ๋ด๋ฐฐ ๊ด๋ จ ๊ธฐ์
์ ์ ์ธํ๊ณ ์ถ์ต๋๋ค."
|
447 |
+
""",
|
448 |
+
examples=[
|
449 |
+
"""20๋ ๏ฟฝ๏ฟฝ๏ฟฝ๋ฐ์ ๋ํ์์
๋๋ค.
|
450 |
+
์ 50๋ง์ ์ ๋๋ฅผ 1๋
์ด์ ์ฅ๊ธฐ ํฌ์ํ๊ณ ์ถ๊ณ ,
|
451 |
+
ํ์จ๊ณผ ๊ธ๋ฆฌ์ ๊ด์ฌ์ด ์์ต๋๋ค.
|
452 |
+
๊ณ ์ํ ๊ณ ์์ต์ ์ถ๊ตฌํ๋ฉฐ, ESG ์์๋ ๊ณ ๋ คํ๊ณ ์ถ์ต๋๋ค.
|
453 |
+
์ ํฉํ ETF๋ฅผ ์ถ์ฒํด์ฃผ์ธ์."""
|
454 |
+
],
|
455 |
+
type="messages",
|
456 |
+
)
|
457 |
+
|
458 |
+
# ์ธํฐํ์ด์ค ์คํ
|
459 |
+
if __name__ == "__main__":
|
460 |
+
demo.launch()
|
etf_database.db
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:4e98d2abd9af54e6fe59c970fc6121db97c01f8ea1a3603df0e3558aae885868
|
3 |
+
size 258048
|
main.py
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
def main():
|
2 |
+
print("Hello from gradio-demo-test!")
|
3 |
+
|
4 |
+
|
5 |
+
if __name__ == "__main__":
|
6 |
+
main()
|
pyproject.toml
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
[project]
|
2 |
+
name = "gradio-demo-test"
|
3 |
+
version = "0.1.0"
|
4 |
+
description = "Add your description here"
|
5 |
+
readme = "README.md"
|
6 |
+
requires-python = ">=3.12"
|
7 |
+
dependencies = [
|
8 |
+
"gradio>=5.34.2",
|
9 |
+
"langchain>=0.3.25",
|
10 |
+
"langchain-openai>=0.3.21",
|
11 |
+
"langchain-community>=0.3.24",
|
12 |
+
"langgraph>=0.4.8",
|
13 |
+
"python-dotenv>=1.1.0",
|
14 |
+
]
|
requirements.txt
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=5.34.2
|
2 |
+
langchain>=0.3.25
|
3 |
+
langchain-openai>=0.3.21
|
4 |
+
langchain-community>=0.3.24
|
5 |
+
langgraph>=0.4.8
|
6 |
+
python-dotenv>=1.1.0
|
uv.lock
ADDED
The diff for this file is too large to render.
See raw diff
|
|