refactor(core): 重构配置与入口模块,统一配置结构并调整导入
- 扁平化应用配置类,合并数据库、LLM、爬虫和通知配置 - 重新实现配置加载,统一环境变量前缀和字段命名 - 入口脚本调整,增加源码路径处理,支持模块绝对导入和直接运行 - HRAgentApplication中使用新配置字段访问方式 - 优化通知渠道注册逻辑,适配新的配置字段重命名 - 模块路径统一由ylhp_hr_2.0改为ylhp_hr_2_0,确保导入一致性 - 删除旧配置模块,避免配置重复和混淆 - service.analysis包暴露MockLLMClient,完善LLM客户端选项 - 保留主入口运行示例,演示系统初始化与功能打印
This commit is contained in:
78
main.py
Normal file
78
main.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
"""Resume Intelligence Agent - Entry Point
|
||||||
|
|
||||||
|
简历智能体系统入口
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# 运行应用
|
||||||
|
uv run python main.py
|
||||||
|
|
||||||
|
# 或使用模块方式
|
||||||
|
uv run python -m src.main.python.cn.yinlihupo.ylhp_hr_2.0.main
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
# 数据库配置
|
||||||
|
DB_URL=mysql+pymysql://root:123456@10.200.8.25:3306/hr_agent
|
||||||
|
|
||||||
|
# LLM 配置
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
LLM_API_KEY=your_api_key
|
||||||
|
|
||||||
|
# 爬虫配置
|
||||||
|
CRAWLER_BOSS_WT_TOKEN=your_boss_token
|
||||||
|
|
||||||
|
# 通知配置
|
||||||
|
NOTIFY_WECHAT_WORK_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/...
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 添加源码路径到 sys.path
|
||||||
|
src_path = Path(__file__).parent / "src" / "main" / "python"
|
||||||
|
if str(src_path) not in sys.path:
|
||||||
|
sys.path.insert(0, str(src_path))
|
||||||
|
|
||||||
|
# 导入应用
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.main import HRAgentApplication, get_app
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.domain.candidate import CandidateSource
|
||||||
|
|
||||||
|
|
||||||
|
async def demo():
|
||||||
|
"""演示:使用 HR Agent 进行简历处理"""
|
||||||
|
print("=" * 50)
|
||||||
|
print("简历智能体系统 - 演示")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 初始化应用
|
||||||
|
app = get_app()
|
||||||
|
|
||||||
|
print("\n已注册爬虫:")
|
||||||
|
for source in app.crawler_factory.get_registered_sources():
|
||||||
|
print(f" - {source.value}")
|
||||||
|
|
||||||
|
print("\n已配置通知渠道:")
|
||||||
|
if app.notification_service:
|
||||||
|
for channel_type in app.notification_service.get_configured_channels():
|
||||||
|
print(f" - {channel_type.value}")
|
||||||
|
else:
|
||||||
|
print(" - 无")
|
||||||
|
|
||||||
|
print("\n可用的评价方案:")
|
||||||
|
schemas = app.analyzer.schema_service.list_schemas()
|
||||||
|
for schema in schemas:
|
||||||
|
default_mark = " (默认)" if schema.is_default else ""
|
||||||
|
print(f" - {schema.name}{default_mark}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("系统初始化完成")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# 示例:爬取并入库(需要配置 CRAWLER_BOSS_WT_TOKEN)
|
||||||
|
# await app.crawl_and_ingest(
|
||||||
|
# source=CandidateSource.BOSS,
|
||||||
|
# job_id="your_job_id"
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(demo())
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
"""Application settings"""
|
|
||||||
from typing import Optional
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
from pydantic import Field
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseSettings(BaseSettings):
|
|
||||||
"""数据库配置"""
|
|
||||||
url: str = Field(default="sqlite:///./hr_agent.db", description="数据库连接URL")
|
|
||||||
echo: bool = Field(default=False, description="是否打印SQL语句")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_prefix = "DB_"
|
|
||||||
|
|
||||||
|
|
||||||
class LLMSettings(BaseSettings):
|
|
||||||
"""LLM 配置"""
|
|
||||||
provider: str = Field(default="openai", description="LLM提供商: openai, claude, mock")
|
|
||||||
api_key: Optional[str] = Field(default=None, description="API密钥")
|
|
||||||
base_url: Optional[str] = Field(default=None, description="自定义API地址")
|
|
||||||
model: str = Field(default="gpt-4", description="模型名称")
|
|
||||||
temperature: float = Field(default=0.7, description="温度参数")
|
|
||||||
max_tokens: int = Field(default=2000, description="最大token数")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_prefix = "LLM_"
|
|
||||||
|
|
||||||
|
|
||||||
class NotificationSettings(BaseSettings):
|
|
||||||
"""通知配置"""
|
|
||||||
# 企业微信
|
|
||||||
wechat_work_webhook: Optional[str] = Field(default=None, description="企业微信Webhook")
|
|
||||||
wechat_work_mentioned: Optional[str] = Field(default=None, description="@提醒列表,逗号分隔")
|
|
||||||
|
|
||||||
# 钉钉
|
|
||||||
dingtalk_webhook: Optional[str] = Field(default=None, description="钉钉Webhook")
|
|
||||||
dingtalk_secret: Optional[str] = Field(default=None, description="钉钉加签密钥")
|
|
||||||
dingtalk_at_mobiles: Optional[str] = Field(default=None, description="@手机号列表,逗号分隔")
|
|
||||||
|
|
||||||
# 邮件
|
|
||||||
email_smtp_host: Optional[str] = Field(default=None, description="SMTP服务器")
|
|
||||||
email_smtp_port: int = Field(default=587, description="SMTP端口")
|
|
||||||
email_username: Optional[str] = Field(default=None, description="邮箱用户名")
|
|
||||||
email_password: Optional[str] = Field(default=None, description="邮箱密码")
|
|
||||||
email_from: Optional[str] = Field(default=None, description="发件人地址")
|
|
||||||
email_to: Optional[str] = Field(default=None, description="收件人地址,逗号分隔")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_prefix = "NOTIFY_"
|
|
||||||
|
|
||||||
|
|
||||||
class CrawlerSettings(BaseSettings):
|
|
||||||
"""爬虫配置"""
|
|
||||||
boss_wt_token: Optional[str] = Field(default=None, description="Boss直聘WT Token")
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_prefix = "CRAWLER_"
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
"""应用配置"""
|
|
||||||
|
|
||||||
# 应用信息
|
|
||||||
app_name: str = Field(default="ylhp_hr_2.0", description="应用名称")
|
|
||||||
app_version: str = Field(default="0.1.0", description="应用版本")
|
|
||||||
debug: bool = Field(default=False, description="调试模式")
|
|
||||||
|
|
||||||
# 子配置
|
|
||||||
database: DatabaseSettings = Field(default_factory=DatabaseSettings)
|
|
||||||
llm: LLMSettings = Field(default_factory=LLMSettings)
|
|
||||||
notification: NotificationSettings = Field(default_factory=NotificationSettings)
|
|
||||||
crawler: CrawlerSettings = Field(default_factory=CrawlerSettings)
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
env_file_encoding = "utf-8"
|
|
||||||
|
|
||||||
|
|
||||||
# 全局配置实例
|
|
||||||
_settings: Optional[Settings] = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_settings() -> Settings:
|
|
||||||
"""获取配置实例(单例)"""
|
|
||||||
global _settings
|
|
||||||
if _settings is None:
|
|
||||||
_settings = Settings()
|
|
||||||
return _settings
|
|
||||||
|
|
||||||
|
|
||||||
def reload_settings() -> Settings:
|
|
||||||
"""重新加载配置"""
|
|
||||||
global _settings
|
|
||||||
_settings = Settings()
|
|
||||||
return _settings
|
|
||||||
69
src/main/python/cn/yinlihupo/ylhp_hr_2_0/config/settings.py
Normal file
69
src/main/python/cn/yinlihupo/ylhp_hr_2_0/config/settings.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
"""Application settings"""
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""应用配置 - 扁平化结构,所有配置项都在此类中"""
|
||||||
|
|
||||||
|
# 应用信息
|
||||||
|
app_name: str = Field(default="ylhp_hr_2.0", description="应用名称")
|
||||||
|
app_version: str = Field(default="0.1.0", description="应用版本")
|
||||||
|
debug: bool = Field(default=False, description="调试模式")
|
||||||
|
|
||||||
|
# 数据库配置 (前缀: DB_)
|
||||||
|
db_url: str = Field(default="sqlite:///./hr_agent.db", description="数据库连接URL")
|
||||||
|
db_echo: bool = Field(default=False, description="是否打印SQL语句")
|
||||||
|
|
||||||
|
# LLM 配置 (前缀: LLM_)
|
||||||
|
llm_provider: str = Field(default="mock", description="LLM提供商: openai, claude, mock")
|
||||||
|
llm_api_key: Optional[str] = Field(default=None, description="API密钥")
|
||||||
|
llm_base_url: Optional[str] = Field(default=None, description="自定义API地址")
|
||||||
|
llm_model: str = Field(default="gpt-4", description="模型名称")
|
||||||
|
llm_temperature: float = Field(default=0.7, description="温度参数")
|
||||||
|
llm_max_tokens: int = Field(default=2000, description="最大token数")
|
||||||
|
|
||||||
|
# 爬虫配置 (前缀: CRAWLER_)
|
||||||
|
crawler_boss_wt_token: Optional[str] = Field(default=None, description="Boss直聘WT Token")
|
||||||
|
|
||||||
|
# 通知配置 - 企业微信 (前缀: NOTIFY_)
|
||||||
|
notify_wechat_work_webhook: Optional[str] = Field(default=None, description="企业微信Webhook")
|
||||||
|
notify_wechat_work_mentioned: Optional[str] = Field(default=None, description="@提醒列表")
|
||||||
|
|
||||||
|
# 通知配置 - 钉钉 (前缀: NOTIFY_)
|
||||||
|
notify_dingtalk_webhook: Optional[str] = Field(default=None, description="钉钉Webhook")
|
||||||
|
notify_dingtalk_secret: Optional[str] = Field(default=None, description="钉钉加签密钥")
|
||||||
|
notify_dingtalk_at_mobiles: Optional[str] = Field(default=None, description="@手机号列表")
|
||||||
|
|
||||||
|
# 通知配置 - 邮件 (前缀: NOTIFY_)
|
||||||
|
notify_email_smtp_host: Optional[str] = Field(default=None, description="SMTP服务器")
|
||||||
|
notify_email_smtp_port: int = Field(default=587, description="SMTP端口")
|
||||||
|
notify_email_username: Optional[str] = Field(default=None, description="邮箱用户名")
|
||||||
|
notify_email_password: Optional[str] = Field(default=None, description="邮箱密码")
|
||||||
|
notify_email_from: Optional[str] = Field(default=None, description="发件人地址")
|
||||||
|
notify_email_to: Optional[str] = Field(default=None, description="收件人地址")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
extra = "ignore" # 忽略额外的环境变量
|
||||||
|
|
||||||
|
|
||||||
|
# 全局配置实例
|
||||||
|
_settings: Optional[Settings] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
"""获取配置实例(单例)"""
|
||||||
|
global _settings
|
||||||
|
if _settings is None:
|
||||||
|
_settings = Settings()
|
||||||
|
return _settings
|
||||||
|
|
||||||
|
|
||||||
|
def reload_settings() -> Settings:
|
||||||
|
"""重新加载配置"""
|
||||||
|
global _settings
|
||||||
|
_settings = Settings()
|
||||||
|
return _settings
|
||||||
@@ -1,29 +1,57 @@
|
|||||||
"""Application entry point"""
|
"""Application entry point"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .config.settings import get_settings
|
# 处理直接运行时的导入问题
|
||||||
from .domain.candidate import CandidateSource
|
try:
|
||||||
from .service.crawler import CrawlerFactory, BossCrawler
|
from .config.settings import get_settings
|
||||||
from .service.ingestion import (
|
from .domain.candidate import CandidateSource
|
||||||
UnifiedIngestionService,
|
from .service.crawler import CrawlerFactory, BossCrawler
|
||||||
DataNormalizer,
|
from .service.ingestion import (
|
||||||
DataValidator,
|
UnifiedIngestionService,
|
||||||
DeduplicationService
|
DataNormalizer,
|
||||||
)
|
DataValidator,
|
||||||
from .service.analysis import (
|
DeduplicationService
|
||||||
ResumeAnalyzer,
|
)
|
||||||
EvaluationSchemaService,
|
from .service.analysis import (
|
||||||
LLMClient,
|
ResumeAnalyzer,
|
||||||
OpenAIClient,
|
EvaluationSchemaService,
|
||||||
MockLLMClient
|
LLMClient,
|
||||||
)
|
OpenAIClient,
|
||||||
from .service.notification import (
|
MockLLMClient
|
||||||
NotificationService,
|
)
|
||||||
WeChatWorkChannel,
|
from .service.notification import (
|
||||||
DingTalkChannel,
|
NotificationService,
|
||||||
EmailChannel
|
WeChatWorkChannel,
|
||||||
)
|
DingTalkChannel,
|
||||||
|
EmailChannel
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
# 直接运行时,使用绝对导入
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.config.settings import get_settings
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.domain.candidate import CandidateSource
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.service.crawler import CrawlerFactory, BossCrawler
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.service.ingestion import (
|
||||||
|
UnifiedIngestionService,
|
||||||
|
DataNormalizer,
|
||||||
|
DataValidator,
|
||||||
|
DeduplicationService
|
||||||
|
)
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.service.analysis import (
|
||||||
|
ResumeAnalyzer,
|
||||||
|
EvaluationSchemaService,
|
||||||
|
LLMClient,
|
||||||
|
OpenAIClient,
|
||||||
|
MockLLMClient
|
||||||
|
)
|
||||||
|
from cn.yinlihupo.ylhp_hr_2_0.service.notification import (
|
||||||
|
NotificationService,
|
||||||
|
WeChatWorkChannel,
|
||||||
|
DingTalkChannel,
|
||||||
|
EmailChannel
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class HRAgentApplication:
|
class HRAgentApplication:
|
||||||
@@ -65,8 +93,8 @@ class HRAgentApplication:
|
|||||||
def _init_crawlers(self):
|
def _init_crawlers(self):
|
||||||
"""初始化爬虫"""
|
"""初始化爬虫"""
|
||||||
# Boss 爬虫
|
# Boss 爬虫
|
||||||
if self.settings.crawler.boss_wt_token:
|
if self.settings.crawler_boss_wt_token:
|
||||||
boss_crawler = BossCrawler(wt_token=self.settings.crawler.boss_wt_token)
|
boss_crawler = BossCrawler(wt_token=self.settings.crawler_boss_wt_token)
|
||||||
self.crawler_factory.register(CandidateSource.BOSS, boss_crawler)
|
self.crawler_factory.register(CandidateSource.BOSS, boss_crawler)
|
||||||
print("Boss crawler registered")
|
print("Boss crawler registered")
|
||||||
|
|
||||||
@@ -91,16 +119,16 @@ class HRAgentApplication:
|
|||||||
|
|
||||||
def _create_llm_client(self) -> LLMClient:
|
def _create_llm_client(self) -> LLMClient:
|
||||||
"""创建 LLM 客户端"""
|
"""创建 LLM 客户端"""
|
||||||
provider = self.settings.llm.provider.lower()
|
provider = self.settings.llm_provider.lower()
|
||||||
|
|
||||||
if provider == "openai":
|
if provider == "openai":
|
||||||
if self.settings.llm.api_key:
|
if self.settings.llm_api_key:
|
||||||
return OpenAIClient(
|
return OpenAIClient(
|
||||||
api_key=self.settings.llm.api_key,
|
api_key=self.settings.llm_api_key,
|
||||||
model=self.settings.llm.model,
|
model=self.settings.llm_model,
|
||||||
base_url=self.settings.llm.base_url,
|
base_url=self.settings.llm_base_url,
|
||||||
temperature=self.settings.llm.temperature,
|
temperature=self.settings.llm_temperature,
|
||||||
max_tokens=self.settings.llm.max_tokens
|
max_tokens=self.settings.llm_max_tokens
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("Warning: OpenAI API key not configured, using mock client")
|
print("Warning: OpenAI API key not configured, using mock client")
|
||||||
@@ -118,55 +146,55 @@ class HRAgentApplication:
|
|||||||
notification_service = NotificationService()
|
notification_service = NotificationService()
|
||||||
|
|
||||||
# 企业微信
|
# 企业微信
|
||||||
if self.settings.notification.wechat_work_webhook:
|
if self.settings.notify_wechat_work_webhook:
|
||||||
mentioned_list = None
|
mentioned_list = None
|
||||||
if self.settings.notification.wechat_work_mentioned:
|
if self.settings.notify_wechat_work_mentioned:
|
||||||
mentioned_list = [
|
mentioned_list = [
|
||||||
m.strip()
|
m.strip()
|
||||||
for m in self.settings.notification.wechat_work_mentioned.split(",")
|
for m in self.settings.notify_wechat_work_mentioned.split(",")
|
||||||
]
|
]
|
||||||
|
|
||||||
channel = WeChatWorkChannel(
|
channel = WeChatWorkChannel(
|
||||||
webhook_url=self.settings.notification.wechat_work_webhook,
|
webhook_url=self.settings.notify_wechat_work_webhook,
|
||||||
mentioned_list=mentioned_list
|
mentioned_list=mentioned_list
|
||||||
)
|
)
|
||||||
notification_service.register_channel(channel)
|
notification_service.register_channel(channel)
|
||||||
print("WeChat Work channel registered")
|
print("WeChat Work channel registered")
|
||||||
|
|
||||||
# 钉钉
|
# 钉钉
|
||||||
if self.settings.notification.dingtalk_webhook:
|
if self.settings.notify_dingtalk_webhook:
|
||||||
at_mobiles = None
|
at_mobiles = None
|
||||||
if self.settings.notification.dingtalk_at_mobiles:
|
if self.settings.notify_dingtalk_at_mobiles:
|
||||||
at_mobiles = [
|
at_mobiles = [
|
||||||
m.strip()
|
m.strip()
|
||||||
for m in self.settings.notification.dingtalk_at_mobiles.split(",")
|
for m in self.settings.notify_dingtalk_at_mobiles.split(",")
|
||||||
]
|
]
|
||||||
|
|
||||||
channel = DingTalkChannel(
|
channel = DingTalkChannel(
|
||||||
webhook_url=self.settings.notification.dingtalk_webhook,
|
webhook_url=self.settings.notify_dingtalk_webhook,
|
||||||
secret=self.settings.notification.dingtalk_secret,
|
secret=self.settings.notify_dingtalk_secret,
|
||||||
at_mobiles=at_mobiles
|
at_mobiles=at_mobiles
|
||||||
)
|
)
|
||||||
notification_service.register_channel(channel)
|
notification_service.register_channel(channel)
|
||||||
print("DingTalk channel registered")
|
print("DingTalk channel registered")
|
||||||
|
|
||||||
# 邮件
|
# 邮件
|
||||||
if (self.settings.notification.email_smtp_host and
|
if (self.settings.notify_email_smtp_host and
|
||||||
self.settings.notification.email_username):
|
self.settings.notify_email_username):
|
||||||
to_addrs = []
|
to_addrs = []
|
||||||
if self.settings.notification.email_to:
|
if self.settings.notify_email_to:
|
||||||
to_addrs = [
|
to_addrs = [
|
||||||
addr.strip()
|
addr.strip()
|
||||||
for addr in self.settings.notification.email_to.split(",")
|
for addr in self.settings.notify_email_to.split(",")
|
||||||
]
|
]
|
||||||
|
|
||||||
if to_addrs:
|
if to_addrs:
|
||||||
channel = EmailChannel(
|
channel = EmailChannel(
|
||||||
smtp_host=self.settings.notification.email_smtp_host,
|
smtp_host=self.settings.notify_email_smtp_host,
|
||||||
smtp_port=self.settings.notification.email_smtp_port,
|
smtp_port=self.settings.notify_email_smtp_port,
|
||||||
username=self.settings.notification.email_username,
|
username=self.settings.notify_email_username,
|
||||||
password=self.settings.notification.email_password or "",
|
password=self.settings.notify_email_password or "",
|
||||||
from_addr=self.settings.notification.email_from or self.settings.notification.email_username,
|
from_addr=self.settings.notify_email_from or self.settings.notify_email_username,
|
||||||
to_addrs=to_addrs
|
to_addrs=to_addrs
|
||||||
)
|
)
|
||||||
notification_service.register_channel(channel)
|
notification_service.register_channel(channel)
|
||||||
@@ -4,7 +4,7 @@ from .evaluation_schema import EvaluationSchemaService
|
|||||||
from .resume_analyzer import ResumeAnalyzer
|
from .resume_analyzer import ResumeAnalyzer
|
||||||
from .scoring_engine import ScoringEngine
|
from .scoring_engine import ScoringEngine
|
||||||
from .prompt_builder import PromptBuilder
|
from .prompt_builder import PromptBuilder
|
||||||
from .llm_client import LLMClient, OpenAIClient
|
from .llm_client import LLMClient, OpenAIClient, MockLLMClient
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"EvaluationSchemaService",
|
"EvaluationSchemaService",
|
||||||
@@ -13,4 +13,5 @@ __all__ = [
|
|||||||
"PromptBuilder",
|
"PromptBuilder",
|
||||||
"LLMClient",
|
"LLMClient",
|
||||||
"OpenAIClient",
|
"OpenAIClient",
|
||||||
|
"MockLLMClient",
|
||||||
]
|
]
|
||||||
Reference in New Issue
Block a user