fromdk commited on
Commit
c8b9067
ยท
verified ยท
1 Parent(s): ca0ad5d

Upload folder using huggingface_hub

Browse files
Files changed (10) hide show
  1. .gitattributes +1 -0
  2. .gitignore +11 -0
  3. .python-version +1 -0
  4. README.md +2 -8
  5. app.py +460 -0
  6. etf_database.db +3 -0
  7. main.py +6 -0
  8. pyproject.toml +14 -0
  9. requirements.txt +6 -0
  10. 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: Gradio Demo Test
3
- emoji: ๐Ÿ‘
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